use argh::FromArgs;
use fidl_fuchsia_pkg_ext::BlobId;
use fidl_fuchsia_pkg_rewrite_ext::RuleConfig;
use std::path::PathBuf;
#[derive(FromArgs, Debug, PartialEq)]
pub struct Args {
#[argh(subcommand)]
pub command: Command,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum Command {
Resolve(ResolveCommand),
Open(OpenCommand),
Repo(RepoCommand),
Rule(RuleCommand),
Gc(GcCommand),
GetHash(GetHashCommand),
PkgStatus(PkgStatusCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "resolve")]
pub struct ResolveCommand {
#[argh(positional)]
pub pkg_url: String,
#[argh(switch, short = 'v')]
pub verbose: bool,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "open")]
pub struct OpenCommand {
#[argh(positional)]
pub meta_far_blob_id: BlobId,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "repo",
note = "A fuchsia package URL contains a repository hostname to identify the package's source.\n",
note = "Without any arguments the command outputs the list of configured repository URLs.\n",
note = "Note that repo commands expect the full repository URL, not just the hostname, e.g:",
note = "$ pkgctl repo rm fuchsia-pkg://example.com"
)]
pub struct RepoCommand {
#[argh(switch, short = 'v')]
pub verbose: bool,
#[argh(subcommand)]
pub subcommand: Option<RepoSubCommand>,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RepoSubCommand {
Add(RepoAddCommand),
Remove(RepoRemoveCommand),
Show(RepoShowCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "add")]
pub struct RepoAddCommand {
#[argh(subcommand)]
pub subcommand: RepoAddSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RepoAddSubCommand {
File(RepoAddFileCommand),
Url(RepoAddUrlCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "file")]
pub struct RepoAddFileCommand {
#[argh(switch, short = 'p')]
pub persist: bool,
#[argh(option, short = 'n')]
pub name: Option<String>,
#[argh(positional)]
pub file: PathBuf,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "url")]
pub struct RepoAddUrlCommand {
#[argh(switch, short = 'p')]
pub persist: bool,
#[argh(option, short = 'n')]
pub name: Option<String>,
#[argh(positional)]
pub repo_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "rm")]
pub struct RepoRemoveCommand {
#[argh(positional)]
pub repo_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "show")]
pub struct RepoShowCommand {
#[argh(positional)]
pub repo_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "rule")]
pub struct RuleCommand {
#[argh(subcommand)]
pub subcommand: RuleSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RuleSubCommand {
Clear(RuleClearCommand),
DumpDynamic(RuleDumpDynamicCommand),
List(RuleListCommand),
Replace(RuleReplaceCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "clear")]
pub struct RuleClearCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "list")]
pub struct RuleListCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "dump-dynamic")]
pub struct RuleDumpDynamicCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "replace")]
pub struct RuleReplaceCommand {
#[argh(subcommand)]
pub subcommand: RuleReplaceSubCommand,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand)]
pub enum RuleReplaceSubCommand {
File(RuleReplaceFileCommand),
Json(RuleReplaceJsonCommand),
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "file")]
pub struct RuleReplaceFileCommand {
#[argh(positional)]
pub file: PathBuf,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "json")]
pub struct RuleReplaceJsonCommand {
#[argh(positional, from_str_fn(parse_rule_config))]
pub config: RuleConfig,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "gc",
note = "This deletes any cached packages that are not present in the static and dynamic index.",
note = "Any blobs associated with these packages will be removed if they are not referenced by another component or package.",
note = "The static index currently is located at /system/data/static_packages, but this location is likely to change.",
note = "The dynamic index is dynamically calculated, and cannot easily be queried at this time."
)]
pub struct GcCommand {}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(subcommand, name = "get-hash")]
pub struct GetHashCommand {
#[argh(positional)]
pub pkg_url: String,
}
#[derive(FromArgs, Debug, PartialEq)]
#[argh(
subcommand,
name = "pkg-status",
note = "Exit codes:",
note = " 0 - pkg in tuf repo and on disk",
note = " 2 - pkg in tuf repo but not on disk",
note = " 3 - pkg not in tuf repo",
note = " 1 - any other misc application error"
)]
pub struct PkgStatusCommand {
#[argh(positional)]
pub pkg_url: String,
}
fn parse_rule_config(config: &str) -> Result<RuleConfig, String> {
serde_json::from_str(config).map_err(|e| e.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use assert_matches::assert_matches;
const REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
const CONFIG_JSON: &str = r#"{"version": "1", "content": []}"#;
const CMD_NAME: &[&str] = &["pkgctl"];
#[test]
fn resolve() {
fn check(args: &[&str], expected_pkg_url: &str, expected_verbose: bool) {
assert_eq!(
Args::from_args(CMD_NAME, args),
Ok(Args {
command: Command::Resolve(ResolveCommand {
pkg_url: expected_pkg_url.to_string(),
verbose: expected_verbose,
})
})
);
}
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
check(&["resolve", url], url, false);
check(&["resolve", "--verbose", url], url, true);
check(&["resolve", "-v", url], url, true);
}
#[test]
fn open() {
fn check(args: &[&str], expected_blob_id: &str) {
assert_eq!(
Args::from_args(CMD_NAME, args),
Ok(Args {
command: Command::Open(OpenCommand {
meta_far_blob_id: expected_blob_id.parse().unwrap(),
})
})
)
}
let blob_id = "1111111111111111111111111111111111111111111111111111111111111111";
check(&["open", blob_id], blob_id);
check(&["open", blob_id], blob_id);
}
#[test]
fn open_reject_malformed_blobs() {
match Args::from_args(CMD_NAME, &["open", "bad_id"]) {
Err(argh::EarlyExit { output: _, status: _ }) => {}
result => panic!("unexpected result {result:?}"),
}
}
#[test]
fn repo() {
fn check(args: &[&str], expected: RepoCommand) {
assert_eq!(
Args::from_args(CMD_NAME, args),
Ok(Args { command: Command::Repo(expected) })
)
}
check(&["repo"], RepoCommand { verbose: false, subcommand: None });
check(&["repo", "-v"], RepoCommand { verbose: true, subcommand: None });
check(&["repo", "--verbose"], RepoCommand { verbose: true, subcommand: None });
check(
&["repo", "add", "file", "foo"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
persist: false,
name: None,
file: "foo".into(),
}),
})),
},
);
check(
&["repo", "add", "file", "-p", "foo"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
persist: true,
name: None,
file: "foo".into(),
}),
})),
},
);
check(
&["repo", "add", "file", "-n", "devhost", "foo"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::File(RepoAddFileCommand {
persist: false,
name: Some("devhost".to_string()),
file: "foo".into(),
}),
})),
},
);
check(
&["repo", "add", "url", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
persist: false,
name: Some("devhost".to_string()),
repo_url: "http://foo.tld/fuchsia/config.json".into(),
}),
})),
},
);
check(
&["repo", "add", "url", "-p", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
persist: true,
name: Some("devhost".to_string()),
repo_url: "http://foo.tld/fuchsia/config.json".into(),
}),
})),
},
);
check(
&["repo", "add", "url", "-p", "-n", "devhost", "http://foo.tld/fuchsia/config.json"],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Add(RepoAddCommand {
subcommand: RepoAddSubCommand::Url(RepoAddUrlCommand {
persist: true,
name: Some("devhost".to_string()),
repo_url: "http://foo.tld/fuchsia/config.json".into(),
}),
})),
},
);
check(
&["repo", "rm", REPO_URL],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Remove(RepoRemoveCommand {
repo_url: REPO_URL.to_string(),
})),
},
);
check(
&["repo", "show", REPO_URL],
RepoCommand {
verbose: false,
subcommand: Some(RepoSubCommand::Show(RepoShowCommand {
repo_url: REPO_URL.to_string(),
})),
},
);
}
#[test]
fn rule() {
fn check(args: &[&str], expected: RuleCommand) {
match Args::from_args(CMD_NAME, args).unwrap() {
Args { command: Command::Rule(cmd) } => {
assert_eq!(cmd, expected);
}
result => panic!("unexpected result {result:?}"),
}
}
check(
&["rule", "list"],
RuleCommand { subcommand: RuleSubCommand::List(RuleListCommand {}) },
);
check(
&["rule", "clear"],
RuleCommand { subcommand: RuleSubCommand::Clear(RuleClearCommand {}) },
);
check(
&["rule", "dump-dynamic"],
RuleCommand { subcommand: RuleSubCommand::DumpDynamic(RuleDumpDynamicCommand {}) },
);
check(
&["rule", "replace", "file", "foo"],
RuleCommand {
subcommand: RuleSubCommand::Replace(RuleReplaceCommand {
subcommand: RuleReplaceSubCommand::File(RuleReplaceFileCommand {
file: "foo".into(),
}),
}),
},
);
check(
&["rule", "replace", "json", CONFIG_JSON],
RuleCommand {
subcommand: RuleSubCommand::Replace(RuleReplaceCommand {
subcommand: RuleReplaceSubCommand::Json(RuleReplaceJsonCommand {
config: RuleConfig::Version1(vec![]),
}),
}),
},
);
}
#[test]
fn rule_replace_json_rejects_malformed_json() {
assert_matches!(
Args::from_args(CMD_NAME, &["rule", "replace", "json", "{"]),
Err(argh::EarlyExit { output: _, status: _ })
);
}
#[test]
fn gc() {
match Args::from_args(CMD_NAME, &["gc"]).unwrap() {
Args { command: Command::Gc(GcCommand {}) } => {}
result => panic!("unexpected result {result:?}"),
}
}
#[test]
fn get_hash() {
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
match Args::from_args(CMD_NAME, &["get-hash", url]).unwrap() {
Args { command: Command::GetHash(GetHashCommand { pkg_url }) } if pkg_url == url => {}
result => panic!("unexpected result {result:?}"),
}
}
#[test]
fn pkg_status() {
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
match Args::from_args(CMD_NAME, &["pkg-status", url]).unwrap() {
Args { command: Command::PkgStatus(PkgStatusCommand { pkg_url }) }
if pkg_url == url => {}
result => panic!("unexpected result {result:?}"),
}
}
}