fuchsia_url/
relative_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;
7use crate::UrlParts;
8
9/// A relative URL locating a Fuchsia package. Used with a subpackage context.
10/// Has the form "<name>" where:
11///   * "name" is a valid package name
12/// https://fuchsia.dev/fuchsia-src/concepts/packages/package_url
13#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct RelativePackageUrl {
15    path: PackageName,
16}
17
18impl RelativePackageUrl {
19    pub(crate) fn from_parts(parts: UrlParts) -> Result<Self, ParseError> {
20        let UrlParts { scheme, host, path, hash, resource } = parts;
21        if scheme.is_some() {
22            return Err(ParseError::CannotContainScheme);
23        }
24        if host.is_some() {
25            return Err(ParseError::HostMustBeEmpty);
26        }
27        if hash.is_some() {
28            return Err(ParseError::CannotContainHash);
29        }
30        if resource.is_some() {
31            return Err(ParseError::CannotContainResource);
32        }
33        let (name, variant) = crate::parse_path_to_name_and_variant(&path)?;
34        if variant.is_some() {
35            return Err(ParseError::RelativePathCannotSpecifyVariant);
36        }
37        Ok(Self { path: name })
38    }
39
40    /// Parse a relative package URL.
41    pub fn parse(url: &str) -> Result<Self, ParseError> {
42        let parts = UrlParts::parse(url)?;
43        let relative_package_url = Self::from_parts(parts)?;
44        let () = crate::validate_inverse_relative_url(url)?;
45        Ok(relative_package_url)
46    }
47}
48
49impl std::str::FromStr for RelativePackageUrl {
50    type Err = ParseError;
51
52    fn from_str(url: &str) -> Result<Self, Self::Err> {
53        Self::parse(url)
54    }
55}
56
57impl From<PackageName> for RelativePackageUrl {
58    fn from(path: PackageName) -> Self {
59        Self { path }
60    }
61}
62
63impl From<RelativePackageUrl> for PackageName {
64    fn from(url: RelativePackageUrl) -> Self {
65        url.path
66    }
67}
68
69impl std::convert::TryFrom<&str> for RelativePackageUrl {
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 RelativePackageUrl {
78    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79        self.path.fmt(f)
80    }
81}
82
83impl AsRef<str> for RelativePackageUrl {
84    fn as_ref(&self) -> &str {
85        self.path.as_ref()
86    }
87}
88
89impl From<&RelativePackageUrl> for String {
90    fn from(url: &RelativePackageUrl) -> Self {
91        url.to_string()
92    }
93}
94
95impl serde::Serialize for RelativePackageUrl {
96    fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
97        self.to_string().serialize(ser)
98    }
99}
100
101impl<'de> serde::Deserialize<'de> for RelativePackageUrl {
102    fn deserialize<D>(de: D) -> Result<Self, D::Error>
103    where
104        D: serde::Deserializer<'de>,
105    {
106        let url = String::deserialize(de)?;
107        Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
108    }
109}
110
111#[cfg(test)]
112mod tests {
113    use super::*;
114    use assert_matches::assert_matches;
115    use std::convert::TryFrom as _;
116
117    #[test]
118    fn parse_err() {
119        for (url, err) in [
120            ("fuchsia-boot://example.org/name", ParseError::CannotContainScheme),
121            ("fuchsia-pkg://", ParseError::CannotContainScheme),
122            ("fuchsia-pkg://name", ParseError::CannotContainScheme),
123            ("fuchsia-pkg:///name", ParseError::CannotContainScheme),
124            ("//example.org/name", ParseError::HostMustBeEmpty),
125            (
126                "///name",
127                ParseError::InvalidRelativePath("///name".to_string(), Some("name".to_string())),
128            ),
129            ("example.org/name", ParseError::RelativePathCannotSpecifyVariant),
130            ("fuchsia-pkg://example.org/name", ParseError::CannotContainScheme),
131            ("name/variant", ParseError::RelativePathCannotSpecifyVariant),
132            ("name#resource", ParseError::CannotContainResource),
133            (
134                "/name",
135                ParseError::InvalidRelativePath("/name".to_string(), Some("name".to_string())),
136            ),
137            (".", ParseError::MissingName),
138            ("..", ParseError::MissingName),
139            (
140                "name?hash=0000000000000000000000000000000000000000000000000000000000000000",
141                ParseError::CannotContainHash,
142            ),
143        ] {
144            assert_matches!(
145                RelativePackageUrl::parse(url),
146                Err(e) if e == err,
147                "the url {:?}, expected: {:?}", url, err
148            );
149            assert_matches!(
150                url.parse::<RelativePackageUrl>(),
151                Err(e) if e == err,
152                "the url {:?}, expected: {:?}", url, err
153            );
154            assert_matches!(
155                RelativePackageUrl::try_from(url),
156                Err(e) if e == err,
157                "the url {:?}, expected: {:?}", url, err
158            );
159            assert_matches!(
160                serde_json::from_str::<RelativePackageUrl>(url),
161                Err(_),
162                "the url {:?}",
163                url
164            );
165        }
166    }
167
168    #[test]
169    fn parse_ok() {
170        for url in ["name", "other3-name"] {
171            let normalized_url = url.trim_start_matches('/');
172            let json_url = format!("\"{url}\"");
173            let normalized_json_url = format!("\"{normalized_url}\"");
174
175            // Creation
176            let name = normalized_url.parse::<crate::PackageName>().unwrap();
177            let validate = |parsed: &RelativePackageUrl| {
178                assert_eq!(parsed.path, name);
179            };
180            validate(&RelativePackageUrl::parse(url).unwrap());
181            validate(&url.parse::<RelativePackageUrl>().unwrap());
182            validate(&RelativePackageUrl::try_from(url).unwrap());
183            validate(&serde_json::from_str::<RelativePackageUrl>(&json_url).unwrap());
184
185            // Stringification
186            assert_eq!(
187                RelativePackageUrl::parse(url).unwrap().to_string(),
188                normalized_url,
189                "the url {:?}",
190                url
191            );
192            assert_eq!(
193                RelativePackageUrl::parse(url).unwrap().as_ref(),
194                normalized_url,
195                "the url {:?}",
196                url
197            );
198            assert_eq!(
199                serde_json::to_string(&RelativePackageUrl::parse(url).unwrap()).unwrap(),
200                normalized_json_url,
201                "the url {:?}",
202                url
203            );
204        }
205    }
206}