diff --git a/tuftool/src/source.rs b/tuftool/src/source.rs index 45556852..6ef69861 100644 --- a/tuftool/src/source.rs +++ b/tuftool/src/source.rs @@ -34,10 +34,9 @@ //! "aws-ssm:///a/key" (notice the 3 slashes after the colon) use crate::error::{self, Result}; -use snafu::{OptionExt, ResultExt}; -use std::path::Path; +use snafu::ResultExt; +use std::path::PathBuf; use tough::key_source::{KeySource, LocalKeySource}; -use tough::SafeUrlPath; use tough_kms::{KmsKeySource, KmsSigningAlgorithm}; use tough_ssm::SsmKeySource; use url::Url; @@ -51,60 +50,173 @@ use url::Url; /// the `KeySource` trait in the `tough` library. A user can then add /// to this parser to support them in `tuftool`. pub(crate) fn parse_key_source(input: &str) -> Result> { - let input_as_path = Path::new(input); - let url = if input_as_path.exists() { - Url::from_file_path(input) - .ok() // dump unhelpful `()` error - .context(error::FileUrlSnafu { - path: input_as_path.to_owned(), - })? - } else { - Url::parse(input).context(error::UrlParseSnafu { url: input })? - }; - match url.scheme() { - "file" => Ok(Box::new(LocalKeySource { - path: url.safe_url_filepath(), - })), - #[cfg(any(feature = "aws-sdk-rust-native-tls", feature = "aws-sdk-rust-rustls"))] - "aws-ssm" => Ok(Box::new(SsmKeySource { - profile: url.host_str().and_then(|s| { - if s.is_empty() { - None - } else { - Some(s.to_owned()) - } - }), - parameter_name: url.path().to_owned(), - // If a key ID isn't provided, the system uses the default key - // associated with your AWS account. - key_id: url.query_pairs().find_map(|(k, v)| { - if k == "kms-key-id" { - Some(v.into_owned()) - } else { - None - } - }), - })), - "aws-kms" => Ok(Box::new(KmsKeySource { - profile: url.host_str().and_then(|s| { - if s.is_empty() { - None - } else { - Some(s.to_owned()) + let path_or_url = parse_path_or_url(input)?; + match path_or_url { + PathOrUrl::Path(path) => Ok(Box::new(LocalKeySource { path })), + PathOrUrl::Url(url) => { + match url.scheme() { + #[cfg(any(feature = "aws-sdk-rust-native-tls", feature = "aws-sdk-rust-rustls"))] + "aws-ssm" => Ok(Box::new(SsmKeySource { + profile: url.host_str().and_then(|s| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + }), + parameter_name: url.path().to_owned(), + // If a key ID isn't provided, the system uses the default key + // associated with your AWS account. + key_id: url.query_pairs().find_map(|(k, v)| { + if k == "kms-key-id" { + Some(v.into_owned()) + } else { + None + } + }), + })), + "aws-kms" => Ok(Box::new(KmsKeySource { + profile: url.host_str().and_then(|s| { + if s.is_empty() { + None + } else { + Some(s.to_owned()) + } + }), + // remove first '/' from the path to get the key_id + key_id: if url.path().is_empty() { + String::new() + } else { + url.path()[1..].to_string() + }, + client: None, + signing_algorithm: KmsSigningAlgorithm::RsassaPssSha256, + })), + _ => error::UnrecognizedSchemeSnafu { + scheme: url.scheme(), } - }), - // remove first '/' from the path to get the key_id - key_id: if url.path().is_empty() { - String::new() - } else { - url.path()[1..].to_string() - }, - client: None, - signing_algorithm: KmsSigningAlgorithm::RsassaPssSha256, - })), - _ => error::UnrecognizedSchemeSnafu { - scheme: url.scheme(), + .fail(), + } } - .fail(), } } + +/// The `Url` crate does not handle relative file paths. We will only use `Url`` for known schemes. +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] +enum PathOrUrl { + Path(PathBuf), + Url(Url), +} + +fn parse_path_or_url(s: &str) -> Result { + if s.starts_with("file://") { + // It's a file path but we don't want to use Url. Strip the scheme and parse as a path. + Ok(PathOrUrl::Path(PathBuf::from( + s.chars().skip(7).collect::(), + ))) + } else if s.starts_with("aws-ssm://") | s.starts_with("aws-kms://") { + // One of our know-supported schemes, parse as a Url. + Ok(PathOrUrl::Url( + Url::parse(s).context(error::UrlParseSnafu { url: s })?, + )) + } else { + // It's not one of our known schemes and it's not a file:// scheme, treat is as a path. + Ok(PathOrUrl::Path(PathBuf::from(s))) + } +} + +#[test] +fn test_parse_path_or_url_path_1() { + let input = "/foo/bar"; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_2() { + let input = "/foo/bar/"; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_3() { + let input = "./x"; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_4() { + let input = "../x"; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_5() { + let input = "C:"; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_6() { + let input = r#"C:\foo.txt"#; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_8() { + let input = r#".\"#; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_9() { + let input = "aws-kms:/"; + let expected = PathOrUrl::Path(PathBuf::from(input)); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_10() { + let input = "aws-kms://"; + let expected = PathOrUrl::Url(Url::parse(input).unwrap()); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_11() { + let input = "aws-kms://x"; + let expected = PathOrUrl::Url(Url::parse(input).unwrap()); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_12() { + let input = "aws-ssm://"; + let expected = PathOrUrl::Url(Url::parse(input).unwrap()); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +} + +#[test] +fn test_parse_path_or_url_path_13() { + let input = "aws-ssm://x"; + let expected = PathOrUrl::Url(Url::parse(input).unwrap()); + let actual = parse_path_or_url(input).unwrap(); + assert_eq!(expected, actual); +}