Skip to main content

fuchsia_url/
fuchsia_pkg_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::{PackageName, PackageVariant};
7use crate::{FuchsiaPkgAbsolutePackageUrl, RepositoryUrl, Resource, 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 FuchsiaPkgAbsoluteComponentUrl {
20    package: FuchsiaPkgAbsolutePackageUrl,
21    resource: Resource,
22}
23
24impl FuchsiaPkgAbsoluteComponentUrl {
25    /// Create an FuchsiaPkgAbsoluteComponentUrl 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 resource = Resource::try_from(resource).map_err(ParseError::InvalidResourcePath)?;
34        Ok(Self { package: FuchsiaPkgAbsolutePackageUrl::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 Some(path) = path else {
44            return Err(ParseError::MissingName)?;
45        };
46        let package = FuchsiaPkgAbsolutePackageUrl::new_with_path(repo, path.as_ref(), hash)?;
47        let resource = resource.ok_or(ParseError::MissingResource)?;
48        Ok(Self { package, resource })
49    }
50
51    /// Parse a "fuchsia-pkg://" URL that locates a component.
52    pub fn parse(url: &str) -> Result<Self, ParseError> {
53        Self::from_parts(UrlParts::parse(url)?)
54    }
55
56    /// Create an `FuchsiaPkgAbsoluteComponentUrl` from a package URL and a resource path.
57    pub fn from_package_url_and_resource(
58        package: FuchsiaPkgAbsolutePackageUrl,
59        resource: String,
60    ) -> Result<Self, ParseError> {
61        let resource = Resource::try_from(resource).map_err(ParseError::InvalidResourcePath)?;
62        Ok(Self { package, resource })
63    }
64
65    /// The resource path of this URL.
66    pub fn resource(&self) -> &crate::Resource {
67        &self.resource
68    }
69
70    /// The file stem of the resource path of this URL, if present.
71    pub fn file_stem(&self) -> Option<&str> {
72        std::path::Path::new(self.resource.as_ref()).file_stem()?.to_str()
73    }
74
75    /// The package URL of this URL (this URL without the resource path).
76    pub fn package_url(&self) -> &FuchsiaPkgAbsolutePackageUrl {
77        &self.package
78    }
79
80    /// Split this component URL into a package URL and a resource.
81    pub fn into_package_and_resource(self) -> (FuchsiaPkgAbsolutePackageUrl, Resource) {
82        let Self { package, resource } = self;
83        (package, resource)
84    }
85}
86
87// FuchsiaPkgAbsoluteComponentUrl does not maintain any invariants on its `package` field in
88// addition to those already maintained by FuchsiaPkgAbsolutePackageUrl so this is safe.
89impl std::ops::Deref for FuchsiaPkgAbsoluteComponentUrl {
90    type Target = FuchsiaPkgAbsolutePackageUrl;
91
92    fn deref(&self) -> &Self::Target {
93        &self.package
94    }
95}
96
97// FuchsiaPkgAbsoluteComponentUrl does not maintain any invariants on its `package` field in
98// addition to those already maintained by FuchsiaPkgAbsolutePackageUrl so this is safe.
99impl std::ops::DerefMut for FuchsiaPkgAbsoluteComponentUrl {
100    fn deref_mut(&mut self) -> &mut Self::Target {
101        &mut self.package
102    }
103}
104
105impl std::str::FromStr for FuchsiaPkgAbsoluteComponentUrl {
106    type Err = ParseError;
107
108    fn from_str(url: &str) -> Result<Self, Self::Err> {
109        Self::parse(url)
110    }
111}
112
113impl std::convert::TryFrom<&str> for FuchsiaPkgAbsoluteComponentUrl {
114    type Error = ParseError;
115
116    fn try_from(value: &str) -> Result<Self, Self::Error> {
117        Self::parse(value)
118    }
119}
120
121impl std::fmt::Display for FuchsiaPkgAbsoluteComponentUrl {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{}#{}", self.package, self.resource.percent_encode())
124    }
125}
126
127impl serde::Serialize for FuchsiaPkgAbsoluteComponentUrl {
128    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
129        self.to_string().serialize(ser)
130    }
131}
132
133impl<'de> serde::Deserialize<'de> for FuchsiaPkgAbsoluteComponentUrl {
134    fn deserialize<D>(de: D) -> Result<Self, D::Error>
135    where
136        D: serde::Deserializer<'de>,
137    {
138        let url = String::deserialize(de)?;
139        Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use crate::ResourcePathError;
147    use crate::errors::PackagePathSegmentError;
148    use assert_matches::assert_matches;
149    use std::convert::TryFrom as _;
150
151    #[test]
152    fn parse_err() {
153        for (url, err) in [
154            ("example.org/name#resource", ParseError::MissingScheme),
155            ("//example.org/name#resource", ParseError::MissingScheme),
156            ("///name#resource", ParseError::MissingScheme),
157            ("/name#resource", ParseError::MissingScheme),
158            ("name#resource", ParseError::MissingScheme),
159            ("fuchsia-boot://example.org/name#resource", ParseError::InvalidScheme),
160            ("fuchsia-pkg:///name#resource", ParseError::MissingHost),
161            ("fuchsia-pkg://exaMple.org/name#resource", ParseError::InvalidHost),
162            ("fuchsia-pkg://example.org#resource", ParseError::MissingName),
163            (
164                "fuchsia-pkg://example.org//#resource",
165                ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
166            ),
167            (
168                "fuchsia-pkg://example.org/name/variant/extra#resource",
169                ParseError::ExtraPathSegments,
170            ),
171            ("fuchsia-pkg://example.org/name#", ParseError::MissingResource),
172            (
173                "fuchsia-pkg://example.org/name#/",
174                ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
175            ),
176            (
177                "fuchsia-pkg://example.org/name#resource/",
178                ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
179            ),
180        ] {
181            assert_matches!(
182                FuchsiaPkgAbsoluteComponentUrl::parse(url),
183                Err(e) if e == err,
184                "the url {:?}", url
185            );
186            assert_matches!(
187                url.parse::<FuchsiaPkgAbsoluteComponentUrl>(),
188                Err(e) if e == err,
189                "the url {:?}", url
190            );
191            assert_matches!(
192                FuchsiaPkgAbsoluteComponentUrl::try_from(url),
193                Err(e) if e == err,
194                "the url {:?}", url
195            );
196            assert_matches!(
197                serde_json::from_str::<FuchsiaPkgAbsoluteComponentUrl>(url),
198                Err(_),
199                "the url {:?}",
200                url
201            );
202        }
203    }
204
205    #[test]
206    fn parse_ok() {
207        for (url, variant, hash, resource) in [
208            ("fuchsia-pkg://example.org/name#resource", None, None, "resource"),
209            ("fuchsia-pkg://example.org/name/variant#resource", Some("variant"), None, "resource"),
210            (
211                "fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000#resource",
212                None,
213                Some("0000000000000000000000000000000000000000000000000000000000000000"),
214                "resource",
215            ),
216            ("fuchsia-pkg://example.org/name#%E2%98%BA", None, None, "☺"),
217        ] {
218            let json_url = format!("\"{url}\"");
219            let host = "example.org";
220            let name = "name";
221
222            // Creation
223            let name = name.parse::<crate::PackageName>().unwrap();
224            let variant = variant.map(|v| v.parse::<crate::PackageVariant>().unwrap());
225            let hash = hash.map(|h| h.parse::<Hash>().unwrap());
226            let resource = resource.parse::<crate::Resource>().unwrap();
227            let validate = |parsed: &FuchsiaPkgAbsoluteComponentUrl| {
228                assert_eq!(parsed.host(), host);
229                assert_eq!(parsed.name(), &name);
230                assert_eq!(parsed.variant(), variant.as_ref());
231                assert_eq!(parsed.hash(), hash);
232                assert_eq!(parsed.resource(), &resource);
233            };
234            validate(&FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap());
235            validate(&url.parse::<FuchsiaPkgAbsoluteComponentUrl>().unwrap());
236            validate(&FuchsiaPkgAbsoluteComponentUrl::try_from(url).unwrap());
237            validate(&serde_json::from_str::<FuchsiaPkgAbsoluteComponentUrl>(&json_url).unwrap());
238
239            // Stringification
240            assert_eq!(
241                FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap().to_string(),
242                url,
243                "the url {:?}",
244                url
245            );
246            assert_eq!(
247                serde_json::to_string(&FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap())
248                    .unwrap(),
249                json_url,
250                "the url {:?}",
251                url
252            );
253        }
254    }
255
256    #[test]
257    // Verify that resource path is validated at all, exhaustive testing of resource path
258    // validation is performed by the tests on the `Resource` type.
259    fn from_package_url_and_resource_err() {
260        for (resource, err) in [
261            ("", ParseError::InvalidResourcePath(ResourcePathError::PathIsEmpty)),
262            ("/", ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash)),
263        ] {
264            let package =
265                "fuchsia-pkg://example.org/name".parse::<FuchsiaPkgAbsolutePackageUrl>().unwrap();
266            assert_eq!(
267                FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
268                    package,
269                    resource.into()
270                ),
271                Err(err),
272                "the resource {:?}",
273                resource
274            );
275        }
276    }
277
278    #[test]
279    fn from_package_url_and_resource_ok() {
280        let package =
281            "fuchsia-pkg://example.org/name".parse::<FuchsiaPkgAbsolutePackageUrl>().unwrap();
282
283        let component = FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
284            package.clone(),
285            "resource".into(),
286        )
287        .unwrap();
288        assert_eq!(component.resource().as_ref(), "resource");
289
290        let component = FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
291            package.clone(),
292            "☺".into(),
293        )
294        .unwrap();
295        assert_eq!(component.resource().as_ref(), "☺");
296    }
297
298    #[test]
299    fn file_stem() {
300        for (url, expected) in [
301            ("fuchsia-pkg://example.org/name#meta/comp.cm", Some("comp")),
302            ("fuchsia-pkg://example.org/name#meta/my.comp.cm", Some("my.comp")),
303            ("fuchsia-pkg://example.org/name#resource", Some("resource")),
304        ] {
305            let parsed = FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap();
306            assert_eq!(parsed.file_stem(), expected, "the url {:?}", url);
307        }
308    }
309}