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