1use argh::FromArgs;
6use fidl_fuchsia_pkg_rewrite_ext::RuleConfig;
7use std::path::PathBuf;
8
9#[derive(FromArgs, Debug, PartialEq)]
10pub 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")]
28pub struct ResolveCommand {
30 #[argh(positional)]
31 pub pkg_url: String,
32
33 #[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)]
47pub struct RepoCommand {
49 #[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")]
67pub 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")]
82pub struct RepoAddFileCommand {
84 #[argh(switch, short = 'p')]
86 pub persist: bool,
87 #[argh(option, short = 'n')]
89 pub name: Option<String>,
90 #[argh(positional)]
92 pub file: PathBuf,
93}
94
95#[derive(FromArgs, Debug, PartialEq)]
96#[argh(subcommand, name = "url")]
97pub struct RepoAddUrlCommand {
99 #[argh(switch, short = 'p')]
101 pub persist: bool,
102 #[argh(option, short = 'n')]
104 pub name: Option<String>,
105 #[argh(positional)]
107 pub repo_url: String,
108}
109
110#[derive(FromArgs, Debug, PartialEq)]
111#[argh(subcommand, name = "rm")]
112pub struct RepoRemoveCommand {
114 #[argh(positional)]
115 pub repo_url: String,
116}
117
118#[derive(FromArgs, Debug, PartialEq)]
119#[argh(subcommand, name = "show")]
120pub struct RepoShowCommand {
122 #[argh(positional)]
123 pub repo_url: String,
124}
125
126#[derive(FromArgs, Debug, PartialEq)]
127#[argh(subcommand, name = "rule")]
128pub 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")]
145pub struct RuleClearCommand {}
147
148#[derive(FromArgs, Debug, PartialEq)]
149#[argh(subcommand, name = "list")]
150pub struct RuleListCommand {}
152
153#[derive(FromArgs, Debug, PartialEq)]
154#[argh(subcommand, name = "dump-dynamic")]
155pub struct RuleDumpDynamicCommand {}
157
158#[derive(FromArgs, Debug, PartialEq)]
159#[argh(subcommand, name = "replace")]
160pub 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")]
175pub struct RuleReplaceFileCommand {
177 #[argh(positional)]
178 pub file: PathBuf,
179}
180
181#[derive(FromArgs, Debug, PartialEq)]
182#[argh(subcommand, name = "json")]
183pub 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)]
198pub struct GcCommand {}
200
201#[derive(FromArgs, Debug, PartialEq)]
202#[argh(subcommand, name = "get-hash")]
203pub 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}