fuchsia_url/
absolute_component_url.rs

1// Copyright 2022 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use crate::errors::ParseError;
6use crate::parse::{validate_resource_path, PackageName, PackageVariant};
7use crate::{AbsolutePackageUrl, RepositoryUrl, UrlParts};
8use fuchsia_hash::Hash;
9
10/// A URL locating a Fuchsia component.
11/// Has the form "fuchsia-pkg://<repository>/<name>[/variant][?hash=<hash>]#<resource>" where:
12///   * "repository" is a valid hostname
13///   * "name" is a valid package name
14///   * "variant" is an optional valid package variant
15///   * "hash" is an optional valid package hash
16///   * "resource" is a valid resource path
17/// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url
18#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub struct AbsoluteComponentUrl {
20    package: AbsolutePackageUrl,
21    resource: String,
22}
23
24impl AbsoluteComponentUrl {
25    /// Create an AbsoluteComponentUrl from its component parts.
26    pub fn new(
27        repo: RepositoryUrl,
28        name: PackageName,
29        variant: Option<PackageVariant>,
30        hash: Option<Hash>,
31        resource: String,
32    ) -> Result<Self, ParseError> {
33        let () = validate_resource_path(&resource).map_err(ParseError::InvalidResourcePath)?;
34        Ok(Self { package: AbsolutePackageUrl::new(repo, name, variant, hash), resource })
35    }
36
37    pub(crate) fn from_parts(parts: UrlParts) -> Result<Self, ParseError> {
38        let UrlParts { scheme, host, path, hash, resource } = parts;
39        let repo = RepositoryUrl::new(
40            scheme.ok_or(ParseError::MissingScheme)?,
41            host.ok_or(ParseError::MissingHost)?,
42        )?;
43        let package = AbsolutePackageUrl::new_with_path(repo, &path, hash)?;
44        let resource = resource.ok_or(ParseError::MissingResource)?;
45        Ok(Self { package, resource })
46    }
47
48    /// Parse a "fuchsia-pkg://" URL that locates a component.
49    pub fn parse(url: &str) -> Result<Self, ParseError> {
50        Self::from_parts(UrlParts::parse(url)?)
51    }
52
53    /// Create an `AbsoluteComponentUrl` from a package URL and a resource path.
54    pub fn from_package_url_and_resource(
55        package: AbsolutePackageUrl,
56        resource: String,
57    ) -> Result<Self, ParseError> {
58        let () = validate_resource_path(&resource).map_err(ParseError::InvalidResourcePath)?;
59        Ok(Self { package, resource })
60    }
61
62    /// The resource path of this URL.
63    pub fn resource(&self) -> &str {
64        &self.resource
65    }
66
67    /// The package URL of this URL (this URL without the resource path).
68    pub fn package_url(&self) -> &AbsolutePackageUrl {
69        &self.package
70    }
71
72    pub(crate) fn into_package_and_resource(self) -> (AbsolutePackageUrl, String) {
73        let Self { package, resource } = self;
74        (package, resource)
75    }
76}
77
78// AbsoluteComponentUrl does not maintain any invariants on its `package` field in addition to those
79// already maintained by AbsolutePackageUrl so this is safe.
80impl std::ops::Deref for AbsoluteComponentUrl {
81    type Target = AbsolutePackageUrl;
82
83    fn deref(&self) -> &Self::Target {
84        &self.package
85    }
86}
87
88// AbsoluteComponentUrl does not maintain any invariants on its `package` field in addition to those
89// already maintained by AbsolutePackageUrl so this is safe.
90impl std::ops::DerefMut for AbsoluteComponentUrl {
91    fn deref_mut(&mut self) -> &mut Self::Target {
92        &mut self.package
93    }
94}
95
96impl std::str::FromStr for AbsoluteComponentUrl {
97    type Err = ParseError;
98
99    fn from_str(url: &str) -> Result<Self, Self::Err> {
100        Self::parse(url)
101    }
102}
103
104impl std::convert::TryFrom<&str> for AbsoluteComponentUrl {
105    type Error = ParseError;
106
107    fn try_from(value: &str) -> Result<Self, Self::Error> {
108        Self::parse(value)
109    }
110}
111
112impl std::fmt::Display for AbsoluteComponentUrl {
113    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
114        write!(
115            f,
116            "{}#{}",
117            self.package,
118            percent_encoding::utf8_percent_encode(&self.resource, crate::FRAGMENT)
119        )
120    }
121}
122
123impl serde::Serialize for AbsoluteComponentUrl {
124    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
125        self.to_string().serialize(ser)
126    }
127}
128
129impl<'de> serde::Deserialize<'de> for AbsoluteComponentUrl {
130    fn deserialize<D>(de: D) -> Result<Self, D::Error>
131    where
132        D: serde::Deserializer<'de>,
133    {
134        let url = String::deserialize(de)?;
135        Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142    use crate::errors::{PackagePathSegmentError, ResourcePathError};
143    use assert_matches::assert_matches;
144    use std::convert::TryFrom as _;
145
146    #[test]
147    fn parse_err() {
148        for (url, err) in [
149            ("example.org/name#resource", ParseError::MissingScheme),
150            ("//example.org/name#resource", ParseError::MissingScheme),
151            ("///name#resource", ParseError::MissingScheme),
152            ("/name#resource", ParseError::MissingScheme),
153            ("name#resource", ParseError::MissingScheme),
154            ("fuchsia-boot://example.org/name#resource", ParseError::InvalidScheme),
155            ("fuchsia-pkg:///name#resource", ParseError::MissingHost),
156            ("fuchsia-pkg://exaMple.org/name#resource", ParseError::InvalidHost),
157            ("fuchsia-pkg://example.org#resource", ParseError::MissingName),
158            (
159                "fuchsia-pkg://example.org//#resource",
160                ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
161            ),
162            (
163                "fuchsia-pkg://example.org/name/variant/extra#resource",
164                ParseError::ExtraPathSegments,
165            ),
166            ("fuchsia-pkg://example.org/name#", ParseError::MissingResource),
167            (
168                "fuchsia-pkg://example.org/name#/",
169                ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
170            ),
171            (
172                "fuchsia-pkg://example.org/name#resource/",
173                ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
174            ),
175        ] {
176            assert_matches!(
177                AbsoluteComponentUrl::parse(url),
178                Err(e) if e == err,
179                "the url {:?}", url
180            );
181            assert_matches!(
182                url.parse::<AbsoluteComponentUrl>(),
183                Err(e) if e == err,
184                "the url {:?}", url
185            );
186            assert_matches!(
187                AbsoluteComponentUrl::try_from(url),
188                Err(e) if e == err,
189                "the url {:?}", url
190            );
191            assert_matches!(
192                serde_json::from_str::<AbsoluteComponentUrl>(url),
193                Err(_),
194                "the url {:?}",
195                url
196            );
197        }
198    }
199
200    #[test]
201    fn parse_ok() {
202        for (url, variant, hash, resource) in [
203            ("fuchsia-pkg://example.org/name#resource", None, None, "resource"),
204            (
205                "fuchsia-pkg://example.org/name/variant#resource",
206                Some("variant"),
207                None,
208                "resource"
209            ),
210            ("fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000#resource", None, Some("0000000000000000000000000000000000000000000000000000000000000000"), "resource"),
211            ("fuchsia-pkg://example.org/name#%E2%98%BA", None, None, "☺"),
212        ] {
213            let json_url = format!("\"{url}\"");
214            let host = "example.org";
215            let name = "name";
216
217            // Creation
218            let name = name.parse::<crate::PackageName>().unwrap();
219            let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
220            let hash = hash.map(|h| h.parse::<Hash>().unwrap());
221            let validate = |parsed: &AbsoluteComponentUrl| {
222                assert_eq!(parsed.host(), host);
223                assert_eq!(parsed.name(), &name);
224                assert_eq!(parsed.variant(), variant.as_ref());
225                assert_eq!(parsed.hash(), hash);
226                assert_eq!(parsed.resource(), resource);
227            };
228            validate(&AbsoluteComponentUrl::parse(url).unwrap());
229            validate(&url.parse::<AbsoluteComponentUrl>().unwrap());
230            validate(&AbsoluteComponentUrl::try_from(url).unwrap());
231            validate(&serde_json::from_str::<AbsoluteComponentUrl>(&json_url).unwrap());
232
233            // Stringification
234            assert_eq!(
235                AbsoluteComponentUrl::parse(url).unwrap().to_string(),
236                url,
237                "the url {:?}",
238                url
239            );
240            assert_eq!(
241                serde_json::to_string(&AbsoluteComponentUrl::parse(url).unwrap()).unwrap(),
242                json_url,
243                "the url {:?}",
244                url
245            );
246        }
247    }
248
249    #[test]
250    // Verify that resource path is validated at all, exhaustive testing of resource path
251    // validation is performed by the tests on `validate_resource_path`.
252    fn from_package_url_and_resource_err() {
253        for (resource, err) in [
254            ("", ParseError::InvalidResourcePath(ResourcePathError::PathIsEmpty)),
255            ("/", ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash)),
256        ] {
257            let package = "fuchsia-pkg://example.org/name".parse::<AbsolutePackageUrl>().unwrap();
258            assert_eq!(
259                AbsoluteComponentUrl::from_package_url_and_resource(package, resource.into()),
260                Err(err),
261                "the resource {:?}",
262                resource
263            );
264        }
265    }
266
267    #[test]
268    fn from_package_url_and_resource_ok() {
269        let package = "fuchsia-pkg://example.org/name".parse::<AbsolutePackageUrl>().unwrap();
270
271        let component =
272            AbsoluteComponentUrl::from_package_url_and_resource(package.clone(), "resource".into())
273                .unwrap();
274        assert_eq!(component.resource(), "resource");
275
276        let component =
277            AbsoluteComponentUrl::from_package_url_and_resource(package.clone(), "☺".into())
278                .unwrap();
279        assert_eq!(component.resource(), "☺");
280    }
281}