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