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 package URL of this URL (this URL without the resource path).
71    pub fn package_url(&self) -> &FuchsiaPkgAbsolutePackageUrl {
72        &self.package
73    }
74
75    /// Split this component URL into a package URL and a resource.
76    pub fn into_package_and_resource(self) -> (FuchsiaPkgAbsolutePackageUrl, Resource) {
77        let Self { package, resource } = self;
78        (package, resource)
79    }
80}
81
82// FuchsiaPkgAbsoluteComponentUrl does not maintain any invariants on its `package` field in
83// addition to those already maintained by FuchsiaPkgAbsolutePackageUrl so this is safe.
84impl std::ops::Deref for FuchsiaPkgAbsoluteComponentUrl {
85    type Target = FuchsiaPkgAbsolutePackageUrl;
86
87    fn deref(&self) -> &Self::Target {
88        &self.package
89    }
90}
91
92// FuchsiaPkgAbsoluteComponentUrl does not maintain any invariants on its `package` field in
93// addition to those already maintained by FuchsiaPkgAbsolutePackageUrl so this is safe.
94impl std::ops::DerefMut for FuchsiaPkgAbsoluteComponentUrl {
95    fn deref_mut(&mut self) -> &mut Self::Target {
96        &mut self.package
97    }
98}
99
100impl std::str::FromStr for FuchsiaPkgAbsoluteComponentUrl {
101    type Err = ParseError;
102
103    fn from_str(url: &str) -> Result<Self, Self::Err> {
104        Self::parse(url)
105    }
106}
107
108impl std::convert::TryFrom<&str> for FuchsiaPkgAbsoluteComponentUrl {
109    type Error = ParseError;
110
111    fn try_from(value: &str) -> Result<Self, Self::Error> {
112        Self::parse(value)
113    }
114}
115
116impl std::fmt::Display for FuchsiaPkgAbsoluteComponentUrl {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(f, "{}#{}", self.package, self.resource.percent_encode())
119    }
120}
121
122impl serde::Serialize for FuchsiaPkgAbsoluteComponentUrl {
123    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
124        self.to_string().serialize(ser)
125    }
126}
127
128impl<'de> serde::Deserialize<'de> for FuchsiaPkgAbsoluteComponentUrl {
129    fn deserialize<D>(de: D) -> Result<Self, D::Error>
130    where
131        D: serde::Deserializer<'de>,
132    {
133        let url = String::deserialize(de)?;
134        Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141    use crate::ResourcePathError;
142    use crate::errors::PackagePathSegmentError;
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                FuchsiaPkgAbsoluteComponentUrl::parse(url),
178                Err(e) if e == err,
179                "the url {:?}", url
180            );
181            assert_matches!(
182                url.parse::<FuchsiaPkgAbsoluteComponentUrl>(),
183                Err(e) if e == err,
184                "the url {:?}", url
185            );
186            assert_matches!(
187                FuchsiaPkgAbsoluteComponentUrl::try_from(url),
188                Err(e) if e == err,
189                "the url {:?}", url
190            );
191            assert_matches!(
192                serde_json::from_str::<FuchsiaPkgAbsoluteComponentUrl>(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            ("fuchsia-pkg://example.org/name/variant#resource", Some("variant"), None, "resource"),
205            (
206                "fuchsia-pkg://example.org/name?hash=0000000000000000000000000000000000000000000000000000000000000000#resource",
207                None,
208                Some("0000000000000000000000000000000000000000000000000000000000000000"),
209                "resource",
210            ),
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 resource = resource.parse::<crate::Resource>().unwrap();
222            let validate = |parsed: &FuchsiaPkgAbsoluteComponentUrl| {
223                assert_eq!(parsed.host(), host);
224                assert_eq!(parsed.name(), &name);
225                assert_eq!(parsed.variant(), variant.as_ref());
226                assert_eq!(parsed.hash(), hash);
227                assert_eq!(parsed.resource(), &resource);
228            };
229            validate(&FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap());
230            validate(&url.parse::<FuchsiaPkgAbsoluteComponentUrl>().unwrap());
231            validate(&FuchsiaPkgAbsoluteComponentUrl::try_from(url).unwrap());
232            validate(&serde_json::from_str::<FuchsiaPkgAbsoluteComponentUrl>(&json_url).unwrap());
233
234            // Stringification
235            assert_eq!(
236                FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap().to_string(),
237                url,
238                "the url {:?}",
239                url
240            );
241            assert_eq!(
242                serde_json::to_string(&FuchsiaPkgAbsoluteComponentUrl::parse(url).unwrap())
243                    .unwrap(),
244                json_url,
245                "the url {:?}",
246                url
247            );
248        }
249    }
250
251    #[test]
252    // Verify that resource path is validated at all, exhaustive testing of resource path
253    // validation is performed by the tests on the `Resource` type.
254    fn from_package_url_and_resource_err() {
255        for (resource, err) in [
256            ("", ParseError::InvalidResourcePath(ResourcePathError::PathIsEmpty)),
257            ("/", ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash)),
258        ] {
259            let package =
260                "fuchsia-pkg://example.org/name".parse::<FuchsiaPkgAbsolutePackageUrl>().unwrap();
261            assert_eq!(
262                FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
263                    package,
264                    resource.into()
265                ),
266                Err(err),
267                "the resource {:?}",
268                resource
269            );
270        }
271    }
272
273    #[test]
274    fn from_package_url_and_resource_ok() {
275        let package =
276            "fuchsia-pkg://example.org/name".parse::<FuchsiaPkgAbsolutePackageUrl>().unwrap();
277
278        let component = FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
279            package.clone(),
280            "resource".into(),
281        )
282        .unwrap();
283        assert_eq!(component.resource().as_ref(), "resource");
284
285        let component = FuchsiaPkgAbsoluteComponentUrl::from_package_url_and_resource(
286            package.clone(),
287            "☺".into(),
288        )
289        .unwrap();
290        assert_eq!(component.resource().as_ref(), "☺");
291    }
292}