Skip to main content

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