pkgctl/
main.rs

1// Copyright 2018 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#![allow(clippy::let_unit_value)]
6
7use crate::args::{
8    Args, Command, GcCommand, GetHashCommand, OpenCommand, PkgStatusCommand, RepoAddCommand,
9    RepoAddFileCommand, RepoAddSubCommand, RepoAddUrlCommand, RepoCommand, RepoRemoveCommand,
10    RepoShowCommand, RepoSubCommand, ResolveCommand, RuleClearCommand, RuleCommand,
11    RuleDumpDynamicCommand, RuleListCommand, RuleReplaceCommand, RuleReplaceFileCommand,
12    RuleReplaceJsonCommand, RuleReplaceSubCommand, RuleSubCommand,
13};
14use anyhow::{bail, format_err, Context as _};
15use fidl_fuchsia_net_http::{self as http};
16use fidl_fuchsia_pkg_rewrite::EngineMarker;
17use fidl_fuchsia_pkg_rewrite_ext::{do_transaction, Rule as RewriteRule, RuleConfig};
18use fidl_fuchsia_space::ManagerMarker as SpaceManagerMarker;
19use fuchsia_component::client::connect_to_protocol;
20use fuchsia_url::RepositoryUrl;
21use futures::io::copy;
22use futures::stream::TryStreamExt;
23use std::fs::File;
24use std::io;
25use std::process::exit;
26use {fidl_fuchsia_pkg as fpkg, fidl_fuchsia_pkg_ext as pkg, fuchsia_async as fasync};
27
28mod args;
29
30pub fn main() -> Result<(), anyhow::Error> {
31    let mut executor = fasync::LocalExecutor::new();
32    let Args { command } = argh::from_env();
33    exit(executor.run_singlethreaded(main_helper(command))?)
34}
35
36async fn main_helper(command: Command) -> Result<i32, anyhow::Error> {
37    match command {
38        Command::Resolve(ResolveCommand { pkg_url, verbose }) => {
39            let resolver = connect_to_protocol::<fpkg::PackageResolverMarker>()
40                .context("Failed to connect to resolver service")?;
41            println!("resolving {pkg_url}");
42
43            let (dir, dir_server_end) = fidl::endpoints::create_proxy();
44
45            let _: fpkg::ResolutionContext = resolver
46                .resolve(&pkg_url, dir_server_end)
47                .await?
48                .map_err(fidl_fuchsia_pkg_ext::ResolveError::from)
49                .with_context(|| format!("Failed to resolve {pkg_url}"))?;
50
51            if verbose {
52                println!("package contents:");
53                let mut stream =
54                    fuchsia_fs::directory::readdir_recursive(&dir, /*timeout=*/ None);
55                while let Some(entry) = stream.try_next().await? {
56                    println!("/{}", entry.name);
57                }
58            }
59
60            Ok(0)
61        }
62        Command::GetHash(GetHashCommand { pkg_url }) => {
63            let resolver = connect_to_protocol::<fpkg::PackageResolverMarker>()
64                .context("Failed to connect to resolver service")?;
65            let blob_id =
66                resolver.get_hash(&fpkg::PackageUrl { url: pkg_url }).await?.map_err(|i| {
67                    format_err!(
68                        "Failed to get package hash with error: {}",
69                        zx::Status::from_raw(i)
70                    )
71                })?;
72            println!("{}", pkg::BlobId::from(blob_id));
73            Ok(0)
74        }
75        Command::PkgStatus(PkgStatusCommand { pkg_url }) => {
76            let resolver = connect_to_protocol::<fpkg::PackageResolverMarker>()
77                .context("Failed to connect to resolver service")?;
78            let blob_id = match resolver.get_hash(&fpkg::PackageUrl { url: pkg_url }).await? {
79                Ok(blob_id) => pkg::BlobId::from(blob_id),
80                Err(status) => match zx::Status::from_raw(status) {
81                    zx::Status::NOT_FOUND => {
82                        println!("Package in registered TUF repo: no");
83                        println!("Package on disk: unknown (did not check since not in tuf repo)");
84                        return Ok(3);
85                    }
86                    other_failure_status => {
87                        bail!("Cannot determine pkg status. Failed fuchsia.pkg.PackageResolver.GetHash with unexpected status: {:?}",
88                          other_failure_status
89                          );
90                    }
91                },
92            };
93            println!("Package in registered TUF repo: yes (merkle={blob_id})");
94
95            let cache = pkg::cache::Client::from_proxy(
96                connect_to_protocol::<fpkg::PackageCacheMarker>()
97                    .context("Failed to connect to cache service")?,
98            );
99
100            match cache.get_already_cached(blob_id).await {
101                Ok(_) => {}
102                Err(e) if e.was_not_cached() => {
103                    println!("Package on disk: no");
104                    return Ok(2);
105                }
106                Err(e) => {
107                    bail!(
108                        "Cannot determine pkg status. Failed fuchsia.pkg.PackageCache.Get: {:?}",
109                        e
110                    );
111                }
112            }
113            println!("Package on disk: yes");
114            Ok(0)
115        }
116        Command::Open(OpenCommand { meta_far_blob_id }) => {
117            let cache = pkg::cache::Client::from_proxy(
118                connect_to_protocol::<fpkg::PackageCacheMarker>()
119                    .context("Failed to connect to cache service")?,
120            );
121            println!("opening {meta_far_blob_id}");
122
123            let dir = cache.get_already_cached(meta_far_blob_id).await?.into_proxy();
124            let entries = fuchsia_fs::directory::readdir_recursive(&dir, /*timeout=*/ None)
125                .try_collect::<Vec<_>>()
126                .await?;
127            println!("package contents:");
128            for entry in entries {
129                println!("/{}", entry.name);
130            }
131
132            Ok(0)
133        }
134        Command::Repo(RepoCommand { verbose, subcommand }) => {
135            let repo_manager = connect_to_protocol::<fpkg::RepositoryManagerMarker>()
136                .context("Failed to connect to resolver service")?;
137
138            match subcommand {
139                None => {
140                    if !verbose {
141                        // with no arguments, list available repos
142                        let repos = fetch_repos(repo_manager).await?;
143
144                        let mut urls =
145                            repos.into_iter().map(|r| r.repo_url().to_string()).collect::<Vec<_>>();
146                        urls.sort_unstable();
147                        urls.into_iter().for_each(|url| println!("{url}"));
148                    } else {
149                        let repos = fetch_repos(repo_manager).await?;
150
151                        let s = serde_json::to_string_pretty(&repos).expect("valid json");
152                        println!("{s}");
153                    }
154                    Ok(0)
155                }
156                Some(RepoSubCommand::Add(RepoAddCommand { subcommand })) => {
157                    match subcommand {
158                        RepoAddSubCommand::File(RepoAddFileCommand { persist, name, file }) => {
159                            let mut repo: pkg::RepositoryConfig =
160                                serde_json::from_reader(io::BufReader::new(File::open(file)?))?;
161                            // If a name is specified via the command line, override the
162                            // automatically derived name.
163                            if let Some(n) = name {
164                                repo = pkg::RepositoryConfigBuilder::from(repo)
165                                    .repo_url(RepositoryUrl::parse_host(n)?)
166                                    .build();
167                            }
168                            // The storage type can be overridden to persistent via the
169                            // command line.
170                            if persist {
171                                repo = pkg::RepositoryConfigBuilder::from(repo)
172                                    .repo_storage_type(pkg::RepositoryStorageType::Persistent)
173                                    .build();
174                            }
175
176                            let res = repo_manager.add(&repo.into()).await?;
177                            let () = res.map_err(zx::Status::from_raw)?;
178                        }
179                        RepoAddSubCommand::Url(RepoAddUrlCommand { persist, name, repo_url }) => {
180                            let res = fetch_url(repo_url).await?;
181                            let mut repo: pkg::RepositoryConfig = serde_json::from_slice(&res)?;
182                            // If a name is specified via the command line, override the
183                            // automatically derived name.
184                            if let Some(n) = name {
185                                repo = pkg::RepositoryConfigBuilder::from(repo)
186                                    .repo_url(RepositoryUrl::parse_host(n)?)
187                                    .build();
188                            }
189                            // The storage type can be overridden to persistent via the
190                            // command line.
191                            if persist {
192                                repo = pkg::RepositoryConfigBuilder::from(repo)
193                                    .repo_storage_type(pkg::RepositoryStorageType::Persistent)
194                                    .build();
195                            }
196
197                            let res = repo_manager.add(&repo.into()).await?;
198                            let () = res.map_err(zx::Status::from_raw)?;
199                        }
200                    }
201
202                    Ok(0)
203                }
204
205                Some(RepoSubCommand::Remove(RepoRemoveCommand { repo_url })) => {
206                    let res = repo_manager.remove(&repo_url).await?;
207                    let () = res.map_err(zx::Status::from_raw)?;
208
209                    Ok(0)
210                }
211
212                Some(RepoSubCommand::Show(RepoShowCommand { repo_url })) => {
213                    let repos = fetch_repos(repo_manager).await?;
214                    for repo in repos.into_iter() {
215                        if repo.repo_url().to_string() == repo_url {
216                            let s = serde_json::to_string_pretty(&repo).expect("valid json");
217                            println!("{s}");
218                            return Ok(0);
219                        }
220                    }
221
222                    println!("Package repository not found: {repo_url:?}");
223                    Ok(1)
224                }
225            }
226        }
227        Command::Rule(RuleCommand { subcommand }) => {
228            let engine = connect_to_protocol::<EngineMarker>()
229                .context("Failed to connect to rewrite engine service")?;
230
231            match subcommand {
232                RuleSubCommand::List(RuleListCommand {}) => {
233                    let (iter, iter_server_end) = fidl::endpoints::create_proxy();
234                    engine.list(iter_server_end)?;
235
236                    let mut rules = Vec::new();
237                    loop {
238                        let more = iter.next().await?;
239                        if more.is_empty() {
240                            break;
241                        }
242                        rules.extend(more);
243                    }
244                    let rules = rules.into_iter().map(|rule| rule.try_into()).collect::<Result<
245                        Vec<RewriteRule>,
246                        _,
247                    >>(
248                    )?;
249
250                    for rule in rules {
251                        println!("{rule:#?}");
252                    }
253                }
254                RuleSubCommand::Clear(RuleClearCommand {}) => {
255                    do_transaction(&engine, |transaction| async move {
256                        transaction.reset_all()?;
257                        Ok(transaction)
258                    })
259                    .await?;
260                }
261                RuleSubCommand::DumpDynamic(RuleDumpDynamicCommand {}) => {
262                    let (transaction, transaction_server_end) = fidl::endpoints::create_proxy();
263                    let () = engine.start_edit_transaction(transaction_server_end)?;
264                    let (iter, iter_server_end) = fidl::endpoints::create_proxy();
265                    transaction.list_dynamic(iter_server_end)?;
266                    let mut rules = Vec::new();
267                    loop {
268                        let more = iter.next().await?;
269                        if more.is_empty() {
270                            break;
271                        }
272                        rules.extend(more);
273                    }
274                    let rules = rules.into_iter().map(|rule| rule.try_into()).collect::<Result<
275                        Vec<RewriteRule>,
276                        _,
277                    >>(
278                    )?;
279                    let rule_configs = RuleConfig::Version1(rules);
280                    let dynamic_rules = serde_json::to_string_pretty(&rule_configs)?;
281                    println!("{dynamic_rules}");
282                }
283                RuleSubCommand::Replace(RuleReplaceCommand { subcommand }) => {
284                    let RuleConfig::Version1(ref rules) = match subcommand {
285                        RuleReplaceSubCommand::File(RuleReplaceFileCommand { file }) => {
286                            serde_json::from_reader(io::BufReader::new(File::open(file)?))?
287                        }
288                        RuleReplaceSubCommand::Json(RuleReplaceJsonCommand { config }) => config,
289                    };
290
291                    do_transaction(&engine, |transaction| {
292                        async move {
293                            transaction.reset_all()?;
294                            // add() inserts rules as highest priority, so iterate over our
295                            // prioritized list of rules so they end up in the right order.
296                            for rule in rules.iter().rev() {
297                                let () = transaction.add(rule.clone()).await?;
298                            }
299                            Ok(transaction)
300                        }
301                    })
302                    .await?;
303                }
304            }
305
306            Ok(0)
307        }
308        Command::Gc(GcCommand {}) => {
309            let space_manager = connect_to_protocol::<SpaceManagerMarker>()
310                .context("Failed to connect to space manager service")?;
311            space_manager
312                .gc()
313                .await?
314                .map_err(|err| format_err!("Garbage collection failed with error: {:?}", err))
315                .map(|_| 0i32)
316        }
317    }
318}
319
320async fn fetch_repos(
321    repo_manager: fpkg::RepositoryManagerProxy,
322) -> Result<Vec<pkg::RepositoryConfig>, anyhow::Error> {
323    let (iter, server_end) = fidl::endpoints::create_proxy();
324    repo_manager.list(server_end)?;
325    let mut repos = vec![];
326
327    loop {
328        let chunk = iter.next().await?;
329        if chunk.is_empty() {
330            break;
331        }
332        repos.extend(chunk);
333    }
334
335    repos
336        .into_iter()
337        .map(|repo| pkg::RepositoryConfig::try_from(repo).map_err(anyhow::Error::from))
338        .collect()
339}
340
341async fn fetch_url<T: Into<String>>(url_string: T) -> Result<Vec<u8>, anyhow::Error> {
342    let http_svc = connect_to_protocol::<http::LoaderMarker>()
343        .context("Unable to connect to fuchsia.net.http.Loader")?;
344
345    let url_request = http::Request {
346        url: Some(url_string.into()),
347        method: Some(String::from("GET")),
348        headers: None,
349        body: None,
350        deadline: None,
351        ..Default::default()
352    };
353
354    let response =
355        http_svc.fetch(url_request).await.context("Error while calling Loader::Fetch")?;
356
357    if let Some(e) = response.error {
358        return Err(format_err!("LoaderProxy error - {:?}", e));
359    }
360
361    let socket = match response.body {
362        Some(s) => fasync::Socket::from_socket(s),
363        _ => {
364            return Err(format_err!("failed to read UrlBody from the stream"));
365        }
366    };
367
368    let mut body = Vec::new();
369    let bytes_received =
370        copy(socket, &mut body).await.context("Failed to read bytes from the socket")?;
371
372    if bytes_received < 1 {
373        return Err(format_err!(
374            "Failed to download data from url! bytes_received = {}",
375            bytes_received
376        ));
377    }
378
379    Ok(body)
380}