Skip to main content

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