blob: 024393a2ebdb9ba1b22a83568c32f415af284e17 [file] [log] [blame]
// Copyright 2022 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::{
errors::ParseError,
parse::{validate_resource_path, PackageName, PackageVariant},
AbsolutePackageUrl, RepositoryUrl, UrlParts,
},
fuchsia_hash::Hash,
};
/// A URL locating a Fuchsia component.
/// Has the form "fuchsia-pkg://<repository>/<name>[/variant][?hash=<hash>]#<resource>" where:
/// * "repository" is a valid hostname
/// * "name" is a valid package name
/// * "variant" is an optional valid package variant
/// * "hash" is an optional valid package hash
/// * "resource" is a valid resource path
/// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct AbsoluteComponentUrl {
package: AbsolutePackageUrl,
resource: String,
}
impl AbsoluteComponentUrl {
/// Create an AbsoluteComponentUrl from its component parts.
pub fn new(
repo: RepositoryUrl,
name: PackageName,
variant: Option<PackageVariant>,
hash: Option<Hash>,
resource: String,
) -> Result<Self, ParseError> {
let () = validate_resource_path(&resource).map_err(ParseError::InvalidResourcePath)?;
Ok(Self { package: AbsolutePackageUrl::new(repo, name, variant, hash), resource })
}
pub(crate) fn from_parts(parts: UrlParts) -> Result<Self, ParseError> {
let UrlParts { scheme, host, path, hash, resource } = parts;
let repo = RepositoryUrl::new(
scheme.ok_or(ParseError::MissingScheme)?,
host.ok_or(ParseError::MissingHost)?,
)?;
let package = AbsolutePackageUrl::new_with_path(repo, &path, hash)?;
let resource = resource.ok_or(ParseError::MissingResource)?;
Ok(Self { package, resource })
}
/// Parse a "fuchsia-pkg://" URL that locates a component.
pub fn parse(url: &str) -> Result<Self, ParseError> {
Self::from_parts(UrlParts::parse(url)?)
}
/// Create an `AbsoluteComponentUrl` from a package URL and a resource path.
pub fn from_package_url_and_resource(
package: AbsolutePackageUrl,
resource: String,
) -> Result<Self, ParseError> {
let () = validate_resource_path(&resource).map_err(ParseError::InvalidResourcePath)?;
Ok(Self { package, resource })
}
/// The resource path of this URL.
pub fn resource(&self) -> &str {
&self.resource
}
/// The package URL of this URL (this URL without the resource path).
pub fn package_url(&self) -> &AbsolutePackageUrl {
&self.package
}
pub(crate) fn into_package_and_resource(self) -> (AbsolutePackageUrl, String) {
let Self { package, resource } = self;
(package, resource)
}
}
// AbsoluteComponentUrl does not maintain any invariants on its `package` field in addition to those
// already maintained by AbsolutePackageUrl so this is safe.
impl std::ops::Deref for AbsoluteComponentUrl {
type Target = AbsolutePackageUrl;
fn deref(&self) -> &Self::Target {
&self.package
}
}
// AbsoluteComponentUrl does not maintain any invariants on its `package` field in addition to those
// already maintained by AbsolutePackageUrl so this is safe.
impl std::ops::DerefMut for AbsoluteComponentUrl {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.package
}
}
impl std::str::FromStr for AbsoluteComponentUrl {
type Err = ParseError;
fn from_str(url: &str) -> Result<Self, Self::Err> {
Self::parse(url)
}
}
impl std::convert::TryFrom<&str> for AbsoluteComponentUrl {
type Error = ParseError;
fn try_from(value: &str) -> Result<Self, Self::Error> {
Self::parse(value)
}
}
impl std::fmt::Display for AbsoluteComponentUrl {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}#{}",
self.package,
percent_encoding::utf8_percent_encode(&self.resource, crate::FRAGMENT)
)
}
}
impl serde::Serialize for AbsoluteComponentUrl {
fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
self.to_string().serialize(ser)
}
}
impl<'de> serde::Deserialize<'de> for AbsoluteComponentUrl {
fn deserialize<D>(de: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let url = String::deserialize(de)?;
Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
crate::errors::{PackagePathSegmentError, ResourcePathError},
assert_matches::assert_matches,
std::convert::TryFrom as _,
};
#[test]
fn parse_err() {
for (url, err) in [
("example.org/name#resource", ParseError::MissingScheme),
("//example.org/name#resource", ParseError::MissingScheme),
("///name#resource", ParseError::MissingScheme),
("/name#resource", ParseError::MissingScheme),
("name#resource", ParseError::MissingScheme),
("fuchsia-boot://example.org/name#resource", ParseError::InvalidScheme),
("fuchsia-pkg:///name#resource", ParseError::MissingHost),
("fuchsia-pkg://exaMple.org/name#resource", ParseError::InvalidHost),
("fuchsia-pkg://example.org#resource", ParseError::MissingName),
(
"fuchsia-pkg://example.org//#resource",
ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
),
(
"fuchsia-pkg://example.org/name/variant/extra#resource",
ParseError::ExtraPathSegments,
),
("fuchsia-pkg://example.org/name#", ParseError::MissingResource),
(
"fuchsia-pkg://example.org/name#/",
ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
),
(
"fuchsia-pkg://example.org/name#resource/",
ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
),
] {
assert_matches!(
AbsoluteComponentUrl::parse(url),
Err(e) if e == err,
"the url {:?}", url
);
assert_matches!(
url.parse::<AbsoluteComponentUrl>(),
Err(e) if e == err,
"the url {:?}", url
);
assert_matches!(
AbsoluteComponentUrl::try_from(url),
Err(e) if e == err,
"the url {:?}", url
);
assert_matches!(
serde_json::from_str::<AbsoluteComponentUrl>(url),
Err(_),
"the url {:?}",
url
);
}
}
#[test]
fn parse_ok() {
for (url, variant, hash, resource) in [
("fuchsia-pkg://example.org/name#resource", None, None, "resource"),
(
"fuchsia-pkg://example.org/name/variant#resource",
Some("variant"),
None,
"resource"
),
("fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000#resource", None, Some("0000000000000000000000000000000000000000000000000000000000000000"), "resource"),
("fuchsia-pkg://example.org/name#%E2%98%BA", None, None, "☺"),
] {
let json_url = format!("\"{url}\"");
let host = "example.org";
let name = "name";
// Creation
let name = name.parse::<crate::PackageName>().unwrap();
let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
let hash = hash.map(|h| h.parse::<Hash>().unwrap());
let validate = |parsed: &AbsoluteComponentUrl| {
assert_eq!(parsed.host(), host);
assert_eq!(parsed.name(), &name);
assert_eq!(parsed.variant(), variant.as_ref());
assert_eq!(parsed.hash(), hash);
assert_eq!(parsed.resource(), resource);
};
validate(&AbsoluteComponentUrl::parse(url).unwrap());
validate(&url.parse::<AbsoluteComponentUrl>().unwrap());
validate(&AbsoluteComponentUrl::try_from(url).unwrap());
validate(&serde_json::from_str::<AbsoluteComponentUrl>(&json_url).unwrap());
// Stringification
assert_eq!(
AbsoluteComponentUrl::parse(url).unwrap().to_string(),
url,
"the url {:?}",
url
);
assert_eq!(
serde_json::to_string(&AbsoluteComponentUrl::parse(url).unwrap()).unwrap(),
json_url,
"the url {:?}",
url
);
}
}
#[test]
// Verify that resource path is validated at all, exhaustive testing of resource path
// validation is performed by the tests on `validate_resource_path`.
fn from_package_url_and_resource_err() {
for (resource, err) in [
("", ParseError::InvalidResourcePath(ResourcePathError::PathIsEmpty)),
("/", ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash)),
] {
let package = "fuchsia-pkg://example.org/name".parse::<AbsolutePackageUrl>().unwrap();
assert_eq!(
AbsoluteComponentUrl::from_package_url_and_resource(package, resource.into()),
Err(err),
"the resource {:?}",
resource
);
}
}
#[test]
fn from_package_url_and_resource_ok() {
let package = "fuchsia-pkg://example.org/name".parse::<AbsolutePackageUrl>().unwrap();
let component =
AbsoluteComponentUrl::from_package_url_and_resource(package.clone(), "resource".into())
.unwrap();
assert_eq!(component.resource(), "resource");
let component =
AbsoluteComponentUrl::from_package_url_and_resource(package.clone(), "☺".into())
.unwrap();
assert_eq!(component.resource(), "☺");
}
}