blob: 345fed9f61e3956e4c62aed28e9906160794be88 [file] [log] [blame]
// Copyright 2018 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.
mod errors;
use lazy_static::lazy_static;
use regex::Regex;
use std::fmt;
use url::percent_encoding::percent_decode;
use url::Url;
pub use crate::errors::ParseError;
lazy_static! {
static ref NAME_RE: Regex = Regex::new(r"^[0-9a-z\-\._]{1,100}$").unwrap();
static ref HASH_RE: Regex = Regex::new(r"^[0-9a-z]{64}$").unwrap();
}
/// Decoded representation of a fuchsia-pkg URI.
///
/// Depending on which segments are included, the URI may identify a package
/// repository, a package within a repository (with optional variant and hash),
/// or a resource within a package.
///
/// Repository identifier:
/// - fuchsia-pkg://example.com/
///
/// Package identifier:
/// - fuchsia-pkg://example.com/some-package
/// - fuchsia-pkg://example.com/some-package/some-variant
/// - fuchsia-pkg://example.com/some-package/some-variant/<some-hash>
///
/// Resource identifier:
/// - fuchsia-pkg://example.com/some-package#path/to/resource
/// - fuchsia-pkg://example.com/some-package/some-variant#path/to/resource
/// - fuchsia-pkg://example.com/some-package/some-variant/<some-hash>#path/to/resource
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct PackageUri {
host: String,
name: Option<String>,
variant: Option<String>,
hash: Option<String>,
resource: Option<String>,
}
impl PackageUri {
pub fn parse(input: &str) -> Result<Self, ParseError> {
let uri = Url::parse(input)?;
let scheme = uri.scheme();
if scheme != "fuchsia-pkg" {
return Err(ParseError::InvalidScheme);
}
let host = if let Some(host) = uri.host() {
host.to_string()
} else {
return Err(ParseError::InvalidHost);
};
if host.is_empty() {
return Err(ParseError::InvalidHost);
}
if uri.port().is_some() {
return Err(ParseError::CannotContainPort);
}
if !uri.username().is_empty() {
return Err(ParseError::CannotContainUsername);
}
if uri.password().is_some() {
return Err(ParseError::CannotContainPassword);
}
let (name, variant, hash) = parse_path(uri.path())?;
if uri.query().is_some() {
return Err(ParseError::CannotContainQueryParameters);
}
let resource = if let Some(resource) = uri.fragment() {
let resource = match percent_decode(resource.as_bytes()).decode_utf8() {
Ok(resource) => resource,
Err(_) => {
return Err(ParseError::InvalidResourcePath);
}
};
if resource.is_empty() {
None
} else if check_resource(&resource) {
Some(resource.to_string())
} else {
return Err(ParseError::InvalidResourcePath);
}
} else {
None
};
Ok(PackageUri {
host,
name,
variant,
hash,
resource,
})
}
pub fn host(&self) -> &str {
&self.host
}
pub fn name(&self) -> Option<&str> {
self.name.as_ref().map(|s| &**s)
}
pub fn variant(&self) -> Option<&str> {
self.variant.as_ref().map(|s| &**s)
}
pub fn hash(&self) -> Option<&str> {
self.hash.as_ref().map(|s| &**s)
}
pub fn resource(&self) -> Option<&str> {
self.resource.as_ref().map(|s| &**s)
}
pub fn new_repository(host: String) -> Result<PackageUri, ParseError> {
if host.is_empty() {
return Err(ParseError::InvalidHost);
}
Ok(PackageUri {
host,
name: None,
variant: None,
hash: None,
resource: None,
})
}
pub fn new_package(
host: String, name: String, variant: Option<String>, hash: Option<String>,
) -> Result<PackageUri, ParseError> {
let mut uri = PackageUri::new_repository(host)?;
if !NAME_RE.is_match(&name) {
return Err(ParseError::InvalidName);
}
if let Some(ref v) = variant {
if !NAME_RE.is_match(v) {
return Err(ParseError::InvalidVariant);
}
}
if let Some(ref h) = hash {
if variant.is_none() {
return Err(ParseError::InvalidVariant);
}
if !HASH_RE.is_match(h) {
return Err(ParseError::InvalidHash);
}
}
uri.name = Some(name);
uri.variant = variant;
uri.hash = hash;
Ok(uri)
}
pub fn new_resource(
host: String, name: String, variant: Option<String>, hash: Option<String>, resource: String,
) -> Result<PackageUri, ParseError> {
let mut uri = PackageUri::new_package(host, name, variant, hash)?;
if resource.is_empty() || !check_resource(&resource) {
return Err(ParseError::InvalidResourcePath);
}
uri.resource = Some(resource);
Ok(uri)
}
}
impl fmt::Display for PackageUri {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "fuchsia-pkg://{}", self.host)?;
if let Some(ref name) = self.name {
write!(f, "/{}", name)?;
if let Some(ref variant) = self.variant {
write!(f, "/{}", variant)?;
if let Some(ref hash) = self.hash {
write!(f, "/{}", hash)?;
}
}
}
if let Some(ref resource) = self.resource {
write!(
f,
"#{}",
url::percent_encoding::utf8_percent_encode(
resource,
url::percent_encoding::DEFAULT_ENCODE_SET
)
)?;
}
Ok(())
}
}
fn parse_path(
mut path: &str,
) -> Result<(Option<String>, Option<String>, Option<String>), ParseError> {
let mut name = None;
let mut variant = None;
let mut hash = None;
if path.starts_with('/') {
path = &path[1..];
if !path.is_empty() {
let mut iter = path.split('/').fuse();
if let Some(s) = iter.next() {
if NAME_RE.is_match(s) {
name = Some(s.to_string());
} else {
return Err(ParseError::InvalidName);
}
}
if let Some(s) = iter.next() {
if NAME_RE.is_match(s) {
variant = Some(s.to_string());
} else {
return Err(ParseError::InvalidVariant);
}
}
if let Some(s) = iter.next() {
if HASH_RE.is_match(s) {
hash = Some(s.to_string());
} else {
return Err(ParseError::InvalidHash);
}
}
if let Some(_) = iter.next() {
return Err(ParseError::ExtraPathSegments);
}
}
}
Ok((name, variant, hash))
}
fn check_resource(input: &str) -> bool {
for segment in input.split('/') {
if segment.is_empty() || segment == "." || segment == ".." {
return false;
}
if segment.bytes().find(|c| *c == b'\x00').is_some() {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
macro_rules! test_parse_ok {
(
$(
$test_name:ident => {
uri = $pkg_uri:expr,
host = $pkg_host:expr,
name = $pkg_name:expr,
variant = $pkg_variant:expr,
hash = $pkg_hash:expr,
resource = $pkg_resource:expr,
}
)+
) => {
$(
#[test]
fn $test_name() {
let pkg_uri = $pkg_uri.to_string();
assert_eq!(
PackageUri::parse(&pkg_uri),
Ok(PackageUri {
host: $pkg_host,
name: $pkg_name,
variant: $pkg_variant,
hash: $pkg_hash,
resource: $pkg_resource,
})
);
}
)+
}
}
macro_rules! test_parse_err {
(
$(
$test_name:ident => {
uris = $uris:expr,
err = $err:expr,
}
)+
) => {
$(
#[test]
fn $test_name() {
for uri in &$uris {
assert_eq!(
PackageUri::parse(uri),
Err($err),
);
}
}
)+
}
}
macro_rules! test_format {
(
$(
$test_name:ident => {
parsed = $parsed:expr,
formatted = $formatted:expr,
}
)+
) => {
$(
#[test]
fn $test_name() {
assert_eq!(
format!("{}", $parsed),
$formatted
);
}
)+
}
}
test_parse_ok! {
test_parse_host => {
uri = "fuchsia-pkg://fuchsia.com",
host = "fuchsia.com".to_string(),
name = None,
variant = None,
hash = None,
resource = None,
}
test_parse_host_name => {
uri = "fuchsia-pkg://fuchsia.com/fonts",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = None,
hash = None,
resource = None,
}
test_parse_host_name_special_chars => {
uri = "fuchsia-pkg://fuchsia.com/abc123-._",
host = "fuchsia.com".to_string(),
name = Some("abc123-._".to_string()),
variant = None,
hash = None,
resource = None,
}
test_parse_host_name_variant => {
uri = "fuchsia-pkg://fuchsia.com/fonts/stable",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = Some("stable".to_string()),
hash = None,
resource = None,
}
test_parse_host_name_variant_hash => {
uri = "fuchsia-pkg://fuchsia.com/fonts/stable/80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = Some("stable".to_string()),
hash = Some("80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a".to_string()),
resource = None,
}
test_parse_ignoring_empty_resource => {
uri = "fuchsia-pkg://fuchsia.com/fonts#",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = None,
hash = None,
resource = None,
}
test_parse_resource => {
uri = "fuchsia-pkg://fuchsia.com/fonts#foo/bar",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = None,
hash = None,
resource = Some("foo/bar".to_string()),
}
test_parse_resource_decodes_percent_encoding => {
uri = "fuchsia-pkg://fuchsia.com/fonts#foo%23bar",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = None,
hash = None,
resource = Some("foo#bar".to_string()),
}
test_parse_resource_ignores_nul_chars => {
uri = "fuchsia-pkg://fuchsia.com/fonts#foo\x00bar",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = None,
hash = None,
resource = Some("foobar".to_string()),
}
test_parse_resource_allows_encoded_control_chars => {
uri = "fuchsia-pkg://fuchsia.com/fonts#foo%09bar",
host = "fuchsia.com".to_string(),
name = Some("fonts".to_string()),
variant = None,
hash = None,
resource = Some("foo\tbar".to_string()),
}
}
test_parse_err! {
test_parse_host_cannot_be_absent => {
uris = [
"fuchsia-pkg://",
],
err = ParseError::InvalidHost,
}
test_parse_host_cannot_be_empty => {
uris = [
"fuchsia-pkg:///",
],
err = ParseError::InvalidHost,
}
test_parse_name_cannot_be_empty => {
uris = [
"fuchsia-pkg://fuchsia.com//",
],
err = ParseError::InvalidName,
}
test_parse_name_cannot_be_longer_than_100_chars => {
uris = [
"fuchsia-pkg://fuchsia.com/12345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901/",
],
err = ParseError::InvalidName,
}
test_parse_name_cannot_have_invalid_characters => {
uris = [
"fuchsia-pkg://fuchsia.com/$",
"fuchsia-pkg://fuchsia.com/foo$bar",
],
err = ParseError::InvalidName,
}
test_parse_variant_cannot_have_invalid_characters => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts/$",
"fuchsia-pkg://fuchsia.com/fonts/foo$bar",
],
err = ParseError::InvalidVariant,
}
test_parse_hash_cannot_be_empty => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts/stable/",
],
err = ParseError::InvalidHash,
}
test_parse_hash_cannot_have_invalid_characters => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts/stable/8$e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a",
"fuchsia-pkg://fuchsia.com/fonts/stable/80E8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a",
],
err = ParseError::InvalidHash,
}
test_parse_hash_must_be_64_chars => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts/stable/80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4",
"fuchsia-pkg://fuchsia.com/fonts/stable/80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4aa",
],
err = ParseError::InvalidHash,
}
test_parse_hash_cannot_have_extra_segments => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts/stable/80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a/foo",
],
err = ParseError::ExtraPathSegments,
}
test_parse_resource_cannot_be_slash => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts#/",
],
err = ParseError::InvalidResourcePath,
}
test_parse_resource_cannot_start_with_slash => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts#/foo",
"fuchsia-pkg://fuchsia.com/fonts#/foo/bar",
],
err = ParseError::InvalidResourcePath,
}
test_parse_resource_cannot_end_with_slash => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts#foo/",
"fuchsia-pkg://fuchsia.com/fonts#foo/bar/",
],
err = ParseError::InvalidResourcePath,
}
test_parse_resource_cannot_contain_dot_dot => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts#foo/../bar",
],
err = ParseError::InvalidResourcePath,
}
test_parse_resource_cannot_contain_empty_segments => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts#foo//bar",
],
err = ParseError::InvalidResourcePath,
}
test_parse_resource_cannot_contain_percent_encoded_nul_chars => {
uris = [
"fuchsia-pkg://fuchsia.com/fonts#foo%00bar",
],
err = ParseError::InvalidResourcePath,
}
test_parse_resource_rejects_port => {
uris = [
"fuchsia-pkg://fuchsia.com:1234",
],
err = ParseError::CannotContainPort,
}
test_parse_resource_rejects_username => {
uris = [
"fuchsia-pkg://user@fuchsia.com",
"fuchsia-pkg://user:password@fuchsia.com",
],
err = ParseError::CannotContainUsername,
}
test_parse_resource_rejects_password => {
uris = [
"fuchsia-pkg://:password@fuchsia.com",
],
err = ParseError::CannotContainPassword,
}
test_parse_rejects_query_params => {
uris = [
"fuchsia-pkg://fuchsia.com?foo=bar",
],
err = ParseError::CannotContainQueryParameters,
}
}
test_format! {
test_format_repository_uri => {
parsed = PackageUri::new_repository("fuchsia.com".to_string()).unwrap(),
formatted = "fuchsia-pkg://fuchsia.com",
}
test_format_package_uri => {
parsed = PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
None,
None,
).unwrap(),
formatted = "fuchsia-pkg://fuchsia.com/fonts",
}
test_format_package_uri_with_variant => {
parsed = PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("stable".to_string()),
None,
).unwrap(),
formatted = "fuchsia-pkg://fuchsia.com/fonts/stable",
}
test_format_package_uri_with_hash => {
parsed = PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("stable".to_string()),
Some("80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a".to_string()),
).unwrap(),
formatted = "fuchsia-pkg://fuchsia.com/fonts/stable/80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a",
}
test_format_resource_uri => {
parsed = PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
None,
None,
"foo#bar".to_string(),
).unwrap(),
formatted = "fuchsia-pkg://fuchsia.com/fonts#foo%23bar",
}
}
#[test]
fn test_new_repository() {
let uri = PackageUri::new_repository("fuchsia.com".to_string()).unwrap();
assert_eq!("fuchsia.com", uri.host());
assert_eq!(None, uri.name());
assert_eq!(None, uri.variant());
assert_eq!(None, uri.hash());
assert_eq!(None, uri.resource());
assert_eq!(
PackageUri::new_repository("".to_string()),
Err(ParseError::InvalidHost)
)
}
#[test]
fn test_new_package() {
let uri = PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("stable".to_string()),
Some("80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a".to_string()),
)
.unwrap();
assert_eq!("fuchsia.com", uri.host());
assert_eq!(Some("fonts"), uri.name());
assert_eq!(Some("stable"), uri.variant());
assert_eq!(
Some("80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a"),
uri.hash()
);
assert_eq!(None, uri.resource());
assert_eq!(
PackageUri::new_package("".to_string(), "fonts".to_string(), None, None),
Err(ParseError::InvalidHost)
);
assert_eq!(
PackageUri::new_package("fuchsia.com".to_string(), "".to_string(), None, None),
Err(ParseError::InvalidName)
);
assert_eq!(
PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("$".to_string()),
None
),
Err(ParseError::InvalidVariant)
);
assert_eq!(
PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
None,
Some(
"80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a".to_string()
)
),
Err(ParseError::InvalidVariant)
);
assert_eq!(
PackageUri::new_package(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("stable".to_string()),
Some("$".to_string())
),
Err(ParseError::InvalidHash)
);
}
#[test]
fn test_new_resource() {
let uri = PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("stable".to_string()),
Some("80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a".to_string()),
"foo/bar".to_string(),
)
.unwrap();
assert_eq!("fuchsia.com", uri.host());
assert_eq!(Some("fonts"), uri.name());
assert_eq!(Some("stable"), uri.variant());
assert_eq!(
Some("80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a"),
uri.hash()
);
assert_eq!(Some("foo/bar"), uri.resource());
assert_eq!(
PackageUri::new_resource(
"".to_string(),
"fonts".to_string(),
None,
None,
"foo/bar".to_string()
),
Err(ParseError::InvalidHost)
);
assert_eq!(
PackageUri::new_resource(
"fuchsia.com".to_string(),
"".to_string(),
None,
None,
"foo/bar".to_string()
),
Err(ParseError::InvalidName)
);
assert_eq!(
PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("$".to_string()),
None,
"foo/bar".to_string()
),
Err(ParseError::InvalidVariant)
);
assert_eq!(
PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
None,
Some(
"80e8721f4eba5437c8b6e1604f6ee384f42aed2b6dfbfd0b616a864839cd7b4a".to_string()
),
"foo/bar".to_string()
),
Err(ParseError::InvalidVariant)
);
assert_eq!(
PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
Some("stable".to_string()),
Some("$".to_string()),
"foo/bar".to_string()
),
Err(ParseError::InvalidHash)
);
assert_eq!(
PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
None,
None,
"".to_string()
),
Err(ParseError::InvalidResourcePath)
);
assert_eq!(
PackageUri::new_resource(
"fuchsia.com".to_string(),
"fonts".to_string(),
None,
None,
"a//b".to_string()
),
Err(ParseError::InvalidResourcePath)
);
}
}