1use crate::errors::ParseError;
6use crate::{RelativePackageUrl, UrlParts};
7
8#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14pub struct RelativeComponentUrl {
15 package: RelativePackageUrl,
16 resource: String,
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: resource })
26 }
27
28 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 pub fn package_url(&self) -> &RelativePackageUrl {
37 &self.package
38 }
39
40 pub fn resource(&self) -> &str {
42 &self.resource
43 }
44
45 pub(crate) fn into_package_and_resource(self) -> (RelativePackageUrl, String) {
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!(
70 f,
71 "{}#{}",
72 self.package,
73 percent_encoding::utf8_percent_encode(&self.resource, crate::FRAGMENT)
74 )
75 }
76}
77
78impl serde::Serialize for RelativeComponentUrl {
79 fn serialize<S: serde::Serializer>(&self, ser: S) -> Result<S::Ok, S::Error> {
80 self.to_string().serialize(ser)
81 }
82}
83
84impl<'de> serde::Deserialize<'de> for RelativeComponentUrl {
85 fn deserialize<D>(de: D) -> Result<Self, D::Error>
86 where
87 D: serde::Deserializer<'de>,
88 {
89 let url = String::deserialize(de)?;
90 Ok(Self::parse(&url).map_err(|err| serde::de::Error::custom(err))?)
91 }
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::errors::{PackagePathSegmentError, ResourcePathError};
98 use assert_matches::assert_matches;
99 use std::convert::TryFrom as _;
100
101 #[test]
102 fn parse_err() {
103 for (url, err) in [
104 ("fuchsia-pkg://example.org/name#resource", ParseError::CannotContainScheme),
105 ("fuchsia-pkg:///name#resource", ParseError::CannotContainScheme),
106 ("fuchsia-pkg://name#resource", ParseError::CannotContainScheme),
107 ("//example.org/name#resource", ParseError::HostMustBeEmpty),
108 (
109 "///name#resource",
110 ParseError::InvalidRelativePath(
111 "///name#resource".to_string(),
112 Some("name#resource".to_string()),
113 ),
114 ),
115 (
116 "nAme#resource",
117 ParseError::InvalidPathSegment(PackagePathSegmentError::InvalidCharacter {
118 character: 'A',
119 }),
120 ),
121 ("name", ParseError::MissingResource),
122 ("name#", ParseError::MissingResource),
123 ("#resource", ParseError::MissingName),
124 (".#resource", ParseError::MissingName),
125 ("..#resource", ParseError::MissingName),
126 (
127 "name#resource/",
128 ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
129 ),
130 (
131 "/name#resource",
132 ParseError::InvalidRelativePath(
133 "/name#resource".to_string(),
134 Some("name#resource".to_string()),
135 ),
136 ),
137 ("name#..", ParseError::InvalidResourcePath(ResourcePathError::NameIsDotDot)),
138 (
139 "name#resource%00",
140 ParseError::InvalidResourcePath(ResourcePathError::NameContainsNull),
141 ),
142 ("extra/segment#resource", ParseError::RelativePathCannotSpecifyVariant),
143 ("too/many/segments#resource", ParseError::ExtraPathSegments),
144 ] {
145 assert_matches!(
146 RelativeComponentUrl::parse(url),
147 Err(e) if e == err,
148 "the url {:?}; expected = {:?}",
149 url, err
150 );
151 assert_matches!(
152 url.parse::<RelativeComponentUrl>(),
153 Err(e) if e == err,
154 "the url {:?}; expected = {:?}",
155 url, err
156 );
157 assert_matches!(
158 RelativeComponentUrl::try_from(url),
159 Err(e) if e == err,
160 "the url {:?}; expected = {:?}",
161 url, err
162 );
163 assert_matches!(
164 serde_json::from_str::<RelativeComponentUrl>(url),
165 Err(_),
166 "the url {:?}",
167 url
168 );
169 }
170 }
171
172 #[test]
173 fn parse_ok() {
174 for (url, package, resource) in
175 [("name#resource", "name", "resource"), ("name#reso%09urce", "name", "reso\turce")]
176 {
177 let normalized_url = url.trim_start_matches('/');
178 let json_url = format!("\"{url}\"");
179 let normalized_json_url = format!("\"{normalized_url}\"");
180
181 let validate = |parsed: &RelativeComponentUrl| {
183 assert_eq!(parsed.package_url().as_ref(), package);
184 assert_eq!(parsed.resource(), resource);
185 };
186 validate(&RelativeComponentUrl::parse(url).unwrap());
187 validate(&url.parse::<RelativeComponentUrl>().unwrap());
188 validate(&RelativeComponentUrl::try_from(url).unwrap());
189 validate(&serde_json::from_str::<RelativeComponentUrl>(&json_url).unwrap());
190
191 assert_eq!(RelativeComponentUrl::parse(url).unwrap().to_string(), normalized_url);
193 assert_eq!(
194 serde_json::to_string(&RelativeComponentUrl::parse(url).unwrap()).unwrap(),
195 normalized_json_url,
196 );
197 }
198 }
199}