Skip to main content

pkgctl/
args.rs

1// Copyright 2019 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
5use argh::FromArgs;
6use fidl_fuchsia_pkg_rewrite_ext::RuleConfig;
7use std::path::PathBuf;
8
9#[derive(FromArgs, Debug, PartialEq)]
10/// Various operations on packages, package repositories, and the package cache.
11pub struct Args {
12    #[argh(subcommand)]
13    pub command: Command,
14}
15
16#[derive(FromArgs, Debug, PartialEq)]
17#[argh(subcommand)]
18pub enum Command {
19    Resolve(ResolveCommand),
20    Repo(RepoCommand),
21    Rule(RuleCommand),
22    Gc(GcCommand),
23    GetHash(GetHashCommand),
24}
25
26#[derive(FromArgs, Debug, PartialEq)]
27#[argh(subcommand, name = "resolve")]
28/// Resolve a package.
29pub struct ResolveCommand {
30    #[argh(positional)]
31    pub pkg_url: String,
32
33    /// prints the contents of the resolved package, which can be slow for large packages.
34    #[argh(switch, short = 'v')]
35    pub verbose: bool,
36}
37
38#[derive(FromArgs, Debug, PartialEq)]
39#[argh(
40    subcommand,
41    name = "repo",
42    note = "A fuchsia package URL contains a repository hostname to identify the package's source.\n",
43    note = "Without any arguments the command outputs the list of configured repository URLs.\n",
44    note = "Note that repo commands expect the full repository URL, not just the hostname, e.g:",
45    note = "$ pkgctl repo rm fuchsia-pkg://example.com"
46)]
47/// Manage one or more known repositories.
48pub struct RepoCommand {
49    /// verbose output
50    #[argh(switch, short = 'v')]
51    pub verbose: bool,
52
53    #[argh(subcommand)]
54    pub subcommand: Option<RepoSubCommand>,
55}
56
57#[derive(FromArgs, Debug, PartialEq)]
58#[argh(subcommand)]
59pub enum RepoSubCommand {
60    Add(RepoAddCommand),
61    Remove(RepoRemoveCommand),
62    Show(RepoShowCommand),
63}
64
65#[derive(FromArgs, Debug, PartialEq)]
66#[argh(subcommand, name = "add")]
67/// Add a source repository.
68pub struct RepoAddCommand {
69    #[argh(subcommand)]
70    pub subcommand: RepoAddSubCommand,
71}
72
73#[derive(FromArgs, Debug, PartialEq)]
74#[argh(subcommand)]
75pub enum RepoAddSubCommand {
76    File(RepoAddFileCommand),
77    Url(RepoAddUrlCommand),
78}
79
80#[derive(FromArgs, Debug, PartialEq)]
81#[argh(subcommand, name = "file")]
82/// Add a repository config from a local file, in JSON format, which contains the different repository metadata and URLs.
83pub struct RepoAddFileCommand {
84    /// persist TUF metadata for repositories provided to the RepoManager.
85    #[argh(switch, short = 'p')]
86    pub persist: bool,
87    /// name of the source (a name from the URL will be derived if not provided).
88    #[argh(option, short = 'n')]
89    pub name: Option<String>,
90    /// repository config file, in JSON format, which contains the different repository metadata and URLs.
91    #[argh(positional)]
92    pub file: PathBuf,
93}
94
95#[derive(FromArgs, Debug, PartialEq)]
96#[argh(subcommand, name = "url")]
97/// Add a repository config via http, in JSON format, which contains the different repository metadata and URLs.
98pub struct RepoAddUrlCommand {
99    /// persist TUF metadata for repositories provided to the RepoManager.
100    #[argh(switch, short = 'p')]
101    pub persist: bool,
102    /// name of the source (a name from the URL will be derived if not provided).
103    #[argh(option, short = 'n')]
104    pub name: Option<String>,
105    /// http(s) URL pointing to a repository config file, in JSON format, which contains the different repository metadata and URLs.
106    #[argh(positional)]
107    pub repo_url: String,
108}
109
110#[derive(FromArgs, Debug, PartialEq)]
111#[argh(subcommand, name = "rm")]
112/// Remove a configured source repository.
113pub struct RepoRemoveCommand {
114    #[argh(positional)]
115    pub repo_url: String,
116}
117
118#[derive(FromArgs, Debug, PartialEq)]
119#[argh(subcommand, name = "show")]
120/// Show JSON-formatted details of a configured source repository.
121pub struct RepoShowCommand {
122    #[argh(positional)]
123    pub repo_url: String,
124}
125
126#[derive(FromArgs, Debug, PartialEq)]
127#[argh(subcommand, name = "rule")]
128/// Manage URL rewrite rules applied to package URLs during package resolution.
129pub struct RuleCommand {
130    #[argh(subcommand)]
131    pub subcommand: RuleSubCommand,
132}
133
134#[derive(FromArgs, Debug, PartialEq)]
135#[argh(subcommand)]
136pub enum RuleSubCommand {
137    Clear(RuleClearCommand),
138    DumpDynamic(RuleDumpDynamicCommand),
139    List(RuleListCommand),
140    Replace(RuleReplaceCommand),
141}
142
143#[derive(FromArgs, Debug, PartialEq)]
144#[argh(subcommand, name = "clear")]
145/// Clear all URL rewrite rules.
146pub struct RuleClearCommand {}
147
148#[derive(FromArgs, Debug, PartialEq)]
149#[argh(subcommand, name = "list")]
150/// List all URL rewrite rules.
151pub struct RuleListCommand {}
152
153#[derive(FromArgs, Debug, PartialEq)]
154#[argh(subcommand, name = "dump-dynamic")]
155/// Dumps all dynamic rewrite rules.
156pub struct RuleDumpDynamicCommand {}
157
158#[derive(FromArgs, Debug, PartialEq)]
159#[argh(subcommand, name = "replace")]
160/// Replace all dynamic rules with the provided rules.
161pub struct RuleReplaceCommand {
162    #[argh(subcommand)]
163    pub subcommand: RuleReplaceSubCommand,
164}
165
166#[derive(FromArgs, Debug, PartialEq)]
167#[argh(subcommand)]
168pub enum RuleReplaceSubCommand {
169    File(RuleReplaceFileCommand),
170    Json(RuleReplaceJsonCommand),
171}
172
173#[derive(FromArgs, Debug, PartialEq)]
174#[argh(subcommand, name = "file")]
175/// Replace all rewrite rules with ones specified in a file
176pub struct RuleReplaceFileCommand {
177    #[argh(positional)]
178    pub file: PathBuf,
179}
180
181#[derive(FromArgs, Debug, PartialEq)]
182#[argh(subcommand, name = "json")]
183/// Replace all rewrite rules with JSON from the command line
184pub struct RuleReplaceJsonCommand {
185    #[argh(positional, from_str_fn(parse_rule_config))]
186    pub config: RuleConfig,
187}
188
189#[derive(FromArgs, Debug, PartialEq)]
190#[argh(
191    subcommand,
192    name = "gc",
193    note = "This deletes any cached packages that are not present in the static and dynamic index.",
194    note = "Any blobs associated with these packages will be removed if they are not referenced by another component or package.",
195    note = "The static index currently is located at /system/data/static_packages, but this location is likely to change.",
196    note = "The dynamic index is dynamically calculated, and cannot easily be queried at this time."
197)]
198/// Trigger a manual garbage collection of the package cache.
199pub struct GcCommand {}
200
201#[derive(FromArgs, Debug, PartialEq)]
202#[argh(subcommand, name = "get-hash")]
203/// Get the hash of a package.
204pub struct GetHashCommand {
205    #[argh(positional)]
206    pub pkg_url: String,
207}
208
209fn parse_rule_config(config: &str) -> Result<RuleConfig, String> {
210    serde_json::from_str(config).map_err(|e| e.to_string())
211}
212
213#[cfg(test)]
214mod tests {
215    use super::*;
216    use assert_matches::assert_matches;
217
218    const REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
219    const CONFIG_JSON: &str = r#"{"version": "1", "content": []}"#;
220    const CMD_NAME: &[&str] = &["pkgctl"];
221
222    #[test]
223    fn resolve() {
224        fn check(args: &[&str], expected_pkg_url: &str, expected_verbose: bool) {
225            assert_eq!(
226                Args::from_args(CMD_NAME, args),
227                Ok(Args {
228                    command: Command::Resolve(ResolveCommand {
229                        pkg_url: expected_pkg_url.to_string(),
230                        verbose: expected_verbose,
231                    })
232                })
233            );
234        }
235
236        let url = "fuchsia-pkg://fuchsia.com/foo/bar";
237
238        check(&["resolve", url], url, false);
239        check(&["resolve", "--verbose", url], url, true);
240        check(&["resolve", "-v", url], url, true);
241    }
242
243    #[test]
244    fn repo() {
245        fn check(args: &[&str], expected: RepoCommand) {
246            assert_eq!(
247                Args::from_args(CMD_NAME, args),
248                Ok(Args { command: Command::Repo(expected) })
249            )
250        }
251
252        check(&["repo"], RepoCommand { verbose: false, subcommand: None });
253        check(&["repo", "-v"], RepoCommand { verbose: true, subcommand: None });
254        check(&["repo", "--verbose"], RepoCommand { verbose: true, subcommand: None });
255        check(
256            &["repo", "add", "file", "foo"],
257            RepoCommand {
258                verbose: false,
259                subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
260                    subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
261                        persist: false,
262                        name: None,
263                        file: "foo".into(),
264                    }),
265                })),
266            },
267        );
268        check(
269            &["repo", "add", "file", "-p", "foo"],
270            RepoCommand {
271                verbose: false,
272                subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
273                    subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
274                        persist: true,
275                        name: None,
276                        file: "foo".into(),
277                    }),
278                })),
279            },
280        );
281        check(
282            &["repo", "add", "file", "-n", "devhost", "foo"],
283            RepoCommand {
284                verbose: false,
285                subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
286                    subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
287                        persist: false,
288                        name: Some("devhost".to_string()),
289                        file: "foo".into(),
290                    }),
291                })),
292            },
293        );
294        check(
295            &["repo", "add", "url", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
296            RepoCommand {
297                verbose: false,
298                subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
299                    subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
300                        persist: false,
301                        name: Some("devhost".to_string()),
302                        repo_url: "http://foo.tld/fuchsia/config.json".into(),
303                    }),
304                })),
305            },
306        );
307        check(
308            &["repo", "add", "url", "-p", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
309            RepoCommand {
310                verbose: false,
311                subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
312                    subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
313                        persist: true,
314                        name: Some("devhost".to_string()),
315                        repo_url: "http://foo.tld/fuchsia/config.json".into(),
316                    }),
317                })),
318            },
319        );
320        check(
321            &["repo", "add", "url", "-p", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
322            RepoCommand {
323                verbose: false,
324                subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
325                    subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
326                        persist: true,
327                        name: Some("devhost".to_string()),
328                        repo_url: "http://foo.tld/fuchsia/config.json".into(),
329                    }),
330                })),
331            },
332        );
333        check(
334            &["repo", "rm", REPO_URL],
335            RepoCommand {
336                verbose: false,
337                subcommand: Some(RepoSubCommand::Remove(RepoRemoveCommand {
338                    repo_url: REPO_URL.to_string(),
339                })),
340            },
341        );
342        check(
343            &["repo", "show", REPO_URL],
344            RepoCommand {
345                verbose: false,
346                subcommand: Some(RepoSubCommand::Show(RepoShowCommand {
347                    repo_url: REPO_URL.to_string(),
348                })),
349            },
350        );
351    }
352
353    #[test]
354    fn rule() {
355        fn check(args: &[&str], expected: RuleCommand) {
356            match Args::from_args(CMD_NAME, args).unwrap() {
357                Args { command: Command::Rule(cmd) } => {
358                    assert_eq!(cmd, expected);
359                }
360                result => panic!("unexpected result {result:?}"),
361            }
362        }
363
364        check(
365            &["rule", "list"],
366            RuleCommand { subcommand: RuleSubCommand::List(RuleListCommand {}) },
367        );
368        check(
369            &["rule", "clear"],
370            RuleCommand { subcommand: RuleSubCommand::Clear(RuleClearCommand {}) },
371        );
372        check(
373            &["rule", "dump-dynamic"],
374            RuleCommand { subcommand: RuleSubCommand::DumpDynamic(RuleDumpDynamicCommand {}) },
375        );
376        check(
377            &["rule", "replace", "file", "foo"],
378            RuleCommand {
379                subcommand: RuleSubCommand::Replace(RuleReplaceCommand {
380                    subcommand: RuleReplaceSubCommand::File(RuleReplaceFileCommand {
381                        file: "foo".into(),
382                    }),
383                }),
384            },
385        );
386        check(
387            &["rule", "replace", "json", CONFIG_JSON],
388            RuleCommand {
389                subcommand: RuleSubCommand::Replace(RuleReplaceCommand {
390                    subcommand: RuleReplaceSubCommand::Json(RuleReplaceJsonCommand {
391                        config: RuleConfig::Version1(vec![]),
392                    }),
393                }),
394            },
395        );
396    }
397
398    #[test]
399    fn rule_replace_json_rejects_malformed_json() {
400        assert_matches!(
401            Args::from_args(CMD_NAME, &["rule", "replace", "json", "{"]),
402            Err(argh::EarlyExit { output: _, status: _ })
403        );
404    }
405
406    #[test]
407    fn gc() {
408        match Args::from_args(CMD_NAME, &["gc"]).unwrap() {
409            Args { command: Command::Gc(GcCommand {}) } => {}
410            result => panic!("unexpected result {result:?}"),
411        }
412    }
413
414    #[test]
415    fn get_hash() {
416        let url = "fuchsia-pkg://fuchsia.com/foo/bar";
417        match Args::from_args(CMD_NAME, &["get-hash", url]).unwrap() {
418            Args { command: Command::GetHash(GetHashCommand { pkg_url }) } if pkg_url == url => {}
419            result => panic!("unexpected result {result:?}"),
420        }
421    }
422}