Skip to main content

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