Skip to main content

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