blob: 2379a13863f136f6e9ae06ee2cb1bb9f06463144 [file]
// Copyright 2019 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use {
crate::error::Error, fidl_fuchsia_pkg_ext::BlobId, fuchsia_url_rewrite::RuleConfig, serde_json,
std::path::PathBuf,
};
const HELP: &str = "\
USAGE:
pkgctl <SUBCOMMAND>
FLAGS:
-h, --help prints help information
SUBCOMMANDS:
help prints this message or the help of the given subcommand(s)
open open a package by merkle root
repo repo subcommands
resolve resolve a package
rule manage URL rewrite rules
";
const HELP_OPEN: &str = "\
open a package by merkle root
USAGE:
pkgctl open <meta_far_blob_id> [selectors]...
ARGS:
<meta_far_blob_id> Merkle root of package's meta.far to cache
<selectors>... Package selectors
";
const HELP_REPO: &str = "\
repo subcommands
USAGE:
pkgctl repo <SUBCOMMAND>
SUBCOMMANDS:
add add a repository
help prints this message or the help of the given subcommand(s)
list list repositories
remove remove a repository
";
const HELP_REPO_ADD: &str = "\
add a repository
USAGE:
pkgctl repo add <file>
OPTIONS:
-f, --file <file> path to a repository config file
";
const HELP_REPO_LIST: &str = "\
list repositories
USAGE:
pkgctl repo list
";
const HELP_REPO_REMOVE: &str = "\
remove a repository
USAGE:
pkgctl repo remove --repo-url <repo_url>
OPTIONS:
--repo-url <repo_url> the repository url to remove
";
const HELP_RESOLVE: &str = "\
resolve a package
USAGE:
pkgctl resolve <pkg_url> [selectors]...
ARGS:
<pkg_url> URL of package to cache
<selectors>... Package selectors
";
const HELP_RULE: &str = "\
manage URL rewrite rules
USAGE:
pkgctl rule <SUBCOMMAND>
SUBCOMMANDS:
clear clear all rules
help prints this message or the help of the given subcommand(s)
list list all rules
replace replace all dynamic rules with the provided rules
";
const HELP_RULE_CLEAR: &str = "\
clear all rules
USAGE:
pkgctl rule clear
";
const HELP_RULE_LIST: &str = "\
list all rules
USAGE:
pkgctl rule list
";
const HELP_RULE_REPLACE: &str = "\
replace all dynamic rules with the provided rules
USAGE:
pkgctl rule replace <SUBCOMMAND>
SUBCOMMANDS:
file
json
";
const HELP_RULE_REPLACE_FILE: &str = "\
USAGE:
pkgctl rule replace file <path>
ARGS:
<path> path to rewrite rule config file
";
const HELP_RULE_REPLACE_JSON: &str = "\
USAGE:
pkgctl rule replace json <config>
ARGS:
<config> JSON encoded rewrite rule config
";
#[derive(Debug, PartialEq)]
pub enum Command {
Resolve { pkg_url: String, selectors: Vec<String> },
Open { meta_far_blob_id: BlobId, selectors: Vec<String> },
Repo(RepoCommand),
Rule(RuleCommand),
}
#[derive(Debug, PartialEq)]
pub enum RepoCommand {
Add { file: PathBuf },
Remove { repo_url: String },
List,
}
#[derive(Debug, PartialEq)]
pub enum RuleCommand {
List,
Clear,
Replace { input_type: RuleConfigInputType },
}
#[derive(Debug, PartialEq)]
pub enum RuleConfigInputType {
File { path: PathBuf },
Json { config: RuleConfig },
}
impl From<RepoCommand> for Command {
fn from(repo: RepoCommand) -> Command {
Command::Repo(repo)
}
}
impl From<RuleCommand> for Command {
fn from(rule: RuleCommand) -> Command {
Command::Rule(rule)
}
}
impl From<RuleConfigInputType> for Command {
fn from(rule: RuleConfigInputType) -> Command {
Command::Rule(RuleCommand::Replace { input_type: rule })
}
}
pub fn parse_args<'a, I>(mut iter: I) -> Result<Command, Error>
where
I: Iterator<Item = &'a str>,
{
macro_rules! unrecognized {
($arg:expr) => {
return Err(Error::UnrecognizedArgument($arg.to_string()));
};
}
macro_rules! done {
($e:expr) => {
match iter.next() {
None => $e,
Some(arg) => unrecognized!(arg),
}
};
}
match iter.next().ok_or_else(|| Error::Help(HELP))? {
"-h" | "--help" => done!(Err(Error::Help(HELP))),
"help" => {
let help = match iter.next() {
None => HELP,
Some("repo") => match iter.next() {
None => HELP_REPO,
Some("add") => done!(HELP_REPO_ADD),
Some("list") => done!(HELP_REPO_LIST),
Some("remove") => done!(HELP_REPO_REMOVE),
Some(arg) => unrecognized!(arg),
},
Some("resolve") => done!(HELP_RESOLVE),
Some("rule") => match iter.next() {
None => HELP_RULE,
Some("clear") => done!(HELP_RULE_CLEAR),
Some("list") => done!(HELP_RULE_LIST),
Some("replace") => match iter.next() {
None => HELP_RULE_REPLACE,
Some("file") => done!(HELP_RULE_REPLACE_FILE),
Some("json") => done!(HELP_RULE_REPLACE_JSON),
Some(arg) => unrecognized!(arg),
},
Some(arg) => unrecognized!(arg),
},
Some("open") => done!(HELP_OPEN),
Some(arg) => unrecognized!(arg),
};
Err(Error::Help(help))
}
"open" => {
let meta_far_blob_id =
iter.next().ok_or_else(|| Error::MissingArgument("meta_far_blob_id"))?;
let selectors = iter.map(|s| s.to_string()).collect();
Ok(Command::Open { meta_far_blob_id: meta_far_blob_id.parse()?, selectors })
}
"resolve" => {
let pkg_url = iter.next().ok_or_else(|| Error::MissingArgument("pkg_url"))?;
let selectors = iter.map(|s| s.to_string()).collect();
Ok(Command::Resolve { pkg_url: pkg_url.to_string(), selectors })
}
"repo" => match iter.next() {
None => Err(Error::MissingCommand),
Some("list") => done!(Ok(RepoCommand::List.into())),
Some("add") => match iter.next() {
None => Err(Error::MissingArgument("file")),
Some("-f") | Some("--file") => {
let file = iter.next().ok_or_else(|| Error::MissingArgument("file"))?;
done!(Ok(RepoCommand::Add { file: file.into() }.into()))
}
Some(arg) => unrecognized!(arg),
},
Some("remove") => match iter.next() {
None => Err(Error::MissingArgument("repo-url")),
Some("--repo-url") => {
let repo_url = iter.next().ok_or_else(|| Error::MissingArgument("repo-url"))?;
done!(Ok(RepoCommand::Remove { repo_url: repo_url.to_string() }.into()))
}
Some(arg) => unrecognized!(arg),
},
Some(arg) => unrecognized!(arg),
},
"rule" => match iter.next() {
None => Err(Error::MissingCommand),
Some("clear") => done!(Ok(RuleCommand::Clear.into())),
Some("list") => done!(Ok(RuleCommand::List.into())),
Some("replace") => match iter.next() {
None => Err(Error::MissingCommand),
Some("file") => {
let path = iter.next().ok_or_else(|| Error::MissingArgument("path"))?;
done!(Ok(RuleConfigInputType::File { path: path.into() }.into()))
}
Some("json") => {
let config = iter.next().ok_or_else(|| Error::MissingArgument("config"))?;
let config = serde_json::from_str(&config)?;
done!(Ok(RuleConfigInputType::Json { config }.into()))
}
Some(arg) => unrecognized!(arg),
},
Some(arg) => unrecognized!(arg),
},
arg => unrecognized!(arg),
}
}
#[cfg(test)]
mod tests {
use super::*;
const REPO_URL: &str = "fuchsia-pkg://fuchsia.com";
const CONFIG_JSON: &str = r#"{"version": "1", "content": []}"#;
#[test]
fn test_unrecognized_argument() {
fn check(args: &[&str]) {
for expected in &["foo", "--foo"] {
let args = args.iter().chain(Some(expected)).collect::<Vec<_>>();
match parse_args(args.iter().map(|s| &***s)) {
Err(Error::UnrecognizedArgument(arg)) => {
assert_eq!(&arg, expected, "arg: {:?}", args);
}
result => panic!("unexpected result {:?} for {:?}", result, args),
}
}
}
// note, specifically leaving out "open" and "resolve" since they accept arbitrary
// selectors.
let cases: &[&[&str]] = &[
&[],
&["help"],
&["help", "repo"],
&["help", "repo", "add"],
&["help", "repo", "list"],
&["help", "repo", "remove"],
&["help", "rule"],
&["help", "rule", "clear"],
&["help", "rule", "list"],
&["help", "rule", "replace"],
&["help", "rule", "replace", "file"],
&["help", "rule", "replace", "json"],
&["help", "open"],
&["repo", "add"],
&["repo", "add", "-f", "foo"],
&["repo", "add", "--file", "foo"],
&["repo", "list"],
&["repo", "remove"],
&["repo", "remove", "--repo-url", REPO_URL],
&["repo"],
&["rule", "clear"],
&["rule", "list"],
&["rule", "replace"],
&["rule", "replace", "file", "foo"],
&["rule", "replace", "json", CONFIG_JSON],
&["rule"],
];
for case in cases {
check(case);
}
}
#[test]
fn test_help() {
fn check(args: &[&str], expected: &str) {
match parse_args(args.into_iter().map(|s| *s)) {
Err(Error::Help(msg)) => {
assert_eq!(msg, expected);
}
result => panic!("unexpected result {:?}", result),
};
}
check(&[], HELP);
check(&["-h"], HELP);
check(&["--help"], HELP);
check(&["help"], HELP);
check(&["help", "open"], HELP_OPEN);
check(&["help", "repo"], HELP_REPO);
check(&["help", "repo", "add"], HELP_REPO_ADD);
check(&["help", "repo", "list"], HELP_REPO_LIST);
check(&["help", "repo", "remove"], HELP_REPO_REMOVE);
check(&["help", "resolve"], HELP_RESOLVE);
check(&["help", "rule"], HELP_RULE);
check(&["help", "rule", "clear"], HELP_RULE_CLEAR);
check(&["help", "rule", "replace"], HELP_RULE_REPLACE);
check(&["help", "rule", "replace", "file"], HELP_RULE_REPLACE_FILE);
check(&["help", "rule", "replace", "json"], HELP_RULE_REPLACE_JSON);
}
#[test]
fn test_resolve() {
fn check(args: &[&str], expected_pkg_url: &str, expected_selectors: &[String]) {
match parse_args(args.into_iter().map(|s| *s)) {
Ok(Command::Resolve { pkg_url, selectors }) => {
assert_eq!(&pkg_url, expected_pkg_url);
assert_eq!(selectors, expected_selectors);
}
result => panic!("unexpected result {:?}", result),
};
}
let url = "fuchsia-pkg://fuchsia.com/foo/bar";
check(&["resolve", url], url, &[]);
check(
&["resolve", url, "selector1", "selector2"],
url,
&["selector1".to_string(), "selector2".to_string()],
);
}
#[test]
fn test_open() {
fn check(args: &[&str], expected_blob_id: &str, expected_selectors: &[String]) {
match parse_args(args.into_iter().map(|s| *s)) {
Ok(Command::Open { meta_far_blob_id, selectors }) => {
let expected_blob_id: BlobId = expected_blob_id.parse().unwrap();
assert_eq!(meta_far_blob_id, expected_blob_id);
assert_eq!(selectors, expected_selectors);
}
result => panic!("unexpected result {:?}", result),
};
}
let blob_id = "1111111111111111111111111111111111111111111111111111111111111111";
check(&["open", blob_id], blob_id, &[]);
check(
&["open", blob_id, "selector1", "selector2"],
blob_id,
&["selector1".to_string(), "selector2".to_string()],
);
}
#[test]
fn test_open_reject_malformed_blobs() {
match parse_args(["open", "bad_id"].iter().map(|a| *a)) {
Err(Error::BlobId(_)) => {}
result => panic!("unexpected result {:?}", result),
}
}
#[test]
fn test_repo() {
fn check(args: &[&str], expected: RepoCommand) {
match parse_args(args.into_iter().map(|s| *s)) {
Ok(Command::Repo(cmd)) => {
assert_eq!(cmd, expected);
}
result => panic!("unexpected result {:?}", result),
};
}
check(&["repo", "list"], RepoCommand::List);
check(&["repo", "add", "-f", "foo"], RepoCommand::Add { file: "foo".into() });
check(&["repo", "add", "--file", "foo"], RepoCommand::Add { file: "foo".into() });
check(
&["repo", "remove", "--repo-url", REPO_URL],
RepoCommand::Remove { repo_url: REPO_URL.to_string() },
);
}
#[test]
fn test_rule() {
fn check(args: &[&str], expected: RuleCommand) {
match parse_args(args.into_iter().map(|s| *s)) {
Ok(Command::Rule(cmd)) => {
assert_eq!(cmd, expected);
}
result => panic!("unexpected result {:?}", result),
};
}
check(&["rule", "list"], RuleCommand::List);
check(&["rule", "clear"], RuleCommand::Clear);
check(
&["rule", "replace", "file", "foo"],
RuleCommand::Replace { input_type: RuleConfigInputType::File { path: "foo".into() } },
);
check(
&["rule", "replace", "json", CONFIG_JSON],
RuleCommand::Replace {
input_type: RuleConfigInputType::Json { config: RuleConfig::Version1(vec![]) },
},
);
}
#[test]
fn test_rule_replace_json_rejects_malformed_json() {
match parse_args(["rule", "replace", "json", "{"].iter().map(|a| *a)) {
Err(Error::Json(_)) => {}
result => panic!("unexpected result {:?}", result),
}
}
}