Skip to main content

fuchsia_url/
repository_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::{Host, Scheme, UrlParts};
7
8pub const SCHEME: &str = "fuchsia-pkg";
9
10/// A URL locating a Fuchsia package repository.
11/// Has the form "fuchsia-pkg://<repository>", where "repository" is a valid hostname.
12/// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url?hl=en#repository
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct RepositoryUrl {
15    host: Host,
16}
17
18impl RepositoryUrl {
19    /// Returns an error if the provided hostname does not comply to the package URL spec:
20    /// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url#repository
21    /// Contains only lowercase ascii letters, digits, a hyphen or the dot delimiter.
22    pub fn parse_host(host: String) -> Result<Self, ParseError> {
23        Ok(Self { host: Host::parse(host)? })
24    }
25
26    /// Parse a "fuchsia-pkg://" URL that locates a package repository.
27    pub fn parse(url: &str) -> Result<Self, ParseError> {
28        let UrlParts { scheme, host, path, hash, resource } = UrlParts::parse(url)?;
29        let scheme = scheme.ok_or(ParseError::MissingScheme)?;
30        let host = host.ok_or(ParseError::MissingHost)?;
31        if path.is_some() {
32            return Err(ParseError::ExtraPathSegments);
33        }
34        if hash.is_some() {
35            return Err(ParseError::CannotContainHash);
36        }
37        if resource.is_some() {
38            return Err(ParseError::CannotContainResource);
39        }
40        Self::new(scheme, host)
41    }
42
43    pub(crate) fn new(scheme: Scheme, host: Host) -> Result<Self, ParseError> {
44        if scheme != Scheme::FuchsiaPkg {
45            return Err(ParseError::InvalidScheme);
46        }
47
48        Ok(Self { host })
49    }
50
51    /// The hostname of the URL.
52    pub fn host(&self) -> &str {
53        self.host.as_ref()
54    }
55
56    /// Consumes the URL and returns the hostname.
57    pub fn into_host(self) -> String {
58        self.host.into()
59    }
60}
61
62impl std::str::FromStr for RepositoryUrl {
63    type Err = ParseError;
64
65    fn from_str(url: &str) -> Result<Self, Self::Err> {
66        Self::parse(url)
67    }
68}
69
70impl std::convert::TryFrom<&str> for RepositoryUrl {
71    type Error = ParseError;
72
73    fn try_from(value: &str) -> Result<Self, Self::Error> {
74        Self::parse(value)
75    }
76}
77
78impl std::fmt::Display for RepositoryUrl {
79    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80        write!(f, "{}://{}", SCHEME, self.host.as_ref())
81    }
82}
83
84impl serde::Serialize for RepositoryUrl {
85    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
86        self.to_string().serialize(ser)
87    }
88}
89
90impl<'de> serde::Deserialize<'de> for RepositoryUrl {
91    fn deserialize<D>(de: D) -> Result<Self, D::Error>
92    where
93        D: serde::Deserializer<'de>,
94    {
95        let url = String::deserialize(de)?;
96        Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103    use crate::errors::PackagePathSegmentError;
104    use assert_matches::assert_matches;
105    use std::convert::TryFrom as _;
106
107    #[test]
108    fn parse_err() {
109        for (url, err) in [
110            ("example.org", ParseError::MissingScheme),
111            ("fuchsia-boot://example.org", ParseError::InvalidScheme),
112            ("fuchsia-pkg://", ParseError::MissingHost),
113            ("fuchsia-pkg://exaMple.org", ParseError::InvalidHost),
114            ("fuchsia-pkg://example.org/path", ParseError::ExtraPathSegments),
115            (
116                "fuchsia-pkg://example.org//",
117                ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
118            ),
119            (
120                "fuchsia-pkg://example.org?hash=0000000000000000000000000000000000000000000000000000000000000000",
121                ParseError::CannotContainHash,
122            ),
123            ("fuchsia-pkg://example.org#resource", ParseError::CannotContainResource),
124            ("fuchsia-pkg://example.org/#resource", ParseError::CannotContainResource),
125        ] {
126            assert_matches!(
127                RepositoryUrl::parse(url),
128                Err(e) if e == err,
129                "the url {:?}", url
130            );
131            assert_matches!(
132                url.parse::<RepositoryUrl>(),
133                Err(e) if e == err,
134                "the url {:?}", url
135            );
136            assert_matches!(
137                RepositoryUrl::try_from(url),
138                Err(e) if e == err,
139                "the url {:?}", url
140            );
141            assert_matches!(
142                serde_json::from_str::<RepositoryUrl>(url),
143                Err(_),
144                "the url {:?}",
145                url
146            );
147        }
148    }
149
150    #[test]
151    fn parse_ok() {
152        for (url, host, display) in [
153            ("fuchsia-pkg://example.org", "example.org", "fuchsia-pkg://example.org"),
154            ("fuchsia-pkg://example.org/", "example.org", "fuchsia-pkg://example.org"),
155            ("fuchsia-pkg://example", "example", "fuchsia-pkg://example"),
156        ] {
157            // Creation
158            assert_eq!(RepositoryUrl::parse(url).unwrap().host(), host, "the url {:?}", url);
159            assert_eq!(url.parse::<RepositoryUrl>().unwrap().host(), host, "the url {:?}", url);
160            assert_eq!(RepositoryUrl::try_from(url).unwrap().host(), host, "the url {:?}", url);
161            assert_eq!(
162                serde_json::from_str::<RepositoryUrl>(&format!("\"{url}\"")).unwrap().host(),
163                host,
164                "the url {:?}",
165                url
166            );
167
168            // Stringification
169            assert_eq!(
170                RepositoryUrl::parse(url).unwrap().to_string(),
171                display,
172                "the url {:?}",
173                url
174            );
175            assert_eq!(
176                serde_json::to_string(&RepositoryUrl::parse(url).unwrap()).unwrap(),
177                format!("\"{display}\""),
178                "the url {:?}",
179                url
180            );
181        }
182    }
183}