fuchsia_url/
boot_url.rs

1// Copyright 2019 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
5pub use crate::errors::ParseError;
6pub use crate::parse::{validate_package_path_segment, validate_resource_path};
7use crate::{validate_path, AbsoluteComponentUrl, Scheme, UrlParts};
8
9pub const SCHEME: &str = "fuchsia-boot";
10
11/// Decoded representation of a fuchsia-boot URL.
12///
13/// fuchsia-boot:///path/to#path/to/resource
14#[derive(Clone, Debug, PartialEq, Eq)]
15pub struct BootUrl {
16    path: String,
17    resource: Option<String>,
18}
19
20impl BootUrl {
21    pub fn parse(input: &str) -> Result<Self, ParseError> {
22        Self::try_from_parts(UrlParts::parse(input)?)
23    }
24
25    fn try_from_parts(
26        UrlParts { scheme, host, path, hash, resource }: UrlParts,
27    ) -> Result<Self, ParseError> {
28        if scheme.ok_or(ParseError::MissingScheme)? != Scheme::FuchsiaBoot {
29            return Err(ParseError::InvalidScheme);
30        }
31
32        if host.is_some() {
33            return Err(ParseError::HostMustBeEmpty);
34        }
35
36        if hash.is_some() {
37            return Err(ParseError::CannotContainHash);
38        }
39
40        Ok(Self { path, resource })
41    }
42
43    pub fn path(&self) -> &str {
44        &self.path
45    }
46
47    pub fn resource(&self) -> Option<&str> {
48        self.resource.as_ref().map(|s| s.as_str())
49    }
50
51    pub fn root_url(&self) -> BootUrl {
52        BootUrl { path: self.path.clone(), resource: None }
53    }
54
55    pub fn new_path(path: String) -> Result<Self, ParseError> {
56        let () = validate_path(&path)?;
57        Ok(Self { path, resource: None })
58    }
59
60    pub fn new_resource(path: String, resource: String) -> Result<BootUrl, ParseError> {
61        let () = validate_path(&path)?;
62        let () = validate_resource_path(&resource).map_err(ParseError::InvalidResourcePath)?;
63        Ok(Self { path, resource: Some(resource) })
64    }
65
66    pub fn new_resource_without_variant(
67        path: String,
68        resource: String,
69    ) -> Result<BootUrl, ParseError> {
70        let () = validate_path(&path)?;
71        let (name, _) = crate::parse_path_to_name_and_variant(&path)?;
72
73        let () = validate_resource_path(&resource).map_err(ParseError::InvalidResourcePath)?;
74        Ok(Self { path: format!("/{}", name), resource: Some(resource) })
75    }
76}
77
78impl TryFrom<&AbsoluteComponentUrl> for BootUrl {
79    type Error = ParseError;
80    fn try_from(component_url: &AbsoluteComponentUrl) -> Result<Self, ParseError> {
81        let path = format!("/{}", component_url.package_url().name());
82        let resource = component_url.resource().to_string();
83        Self::new_resource(path, resource)
84    }
85}
86
87impl std::fmt::Display for BootUrl {
88    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89        write!(f, "{}://{}", SCHEME, self.path)?;
90        if let Some(ref resource) = self.resource {
91            write!(f, "#{}", percent_encoding::utf8_percent_encode(resource, crate::FRAGMENT))?;
92        }
93
94        Ok(())
95    }
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    use crate::errors::{PackagePathSegmentError, ResourcePathError};
102    use crate::AbsoluteComponentUrl;
103
104    macro_rules! test_parse_ok {
105        (
106            $(
107                $test_name:ident => {
108                    url = $pkg_url:expr,
109                    path = $pkg_path:expr,
110                    resource = $pkg_resource:expr,
111                }
112            )+
113        ) => {
114            $(
115                #[test]
116                fn $test_name() {
117                    let pkg_url = $pkg_url.to_string();
118                    assert_eq!(
119                        BootUrl::parse(&pkg_url),
120                        Ok(BootUrl {
121                            path: $pkg_path,
122                            resource: $pkg_resource,
123                        })
124                    );
125                }
126            )+
127        }
128    }
129
130    macro_rules! test_parse_err {
131        (
132            $(
133                $test_name:ident => {
134                    urls = $urls:expr,
135                    err = $err:expr,
136                }
137            )+
138        ) => {
139            $(
140                #[test]
141                fn $test_name() {
142                    for url in &$urls {
143                        assert_eq!(
144                            BootUrl::parse(url),
145                            Err($err),
146                        );
147                    }
148                }
149            )+
150        }
151    }
152
153    macro_rules! test_format {
154        (
155            $(
156                $test_name:ident => {
157                    parsed = $parsed:expr,
158                    formatted = $formatted:expr,
159                }
160            )+
161        ) => {
162            $(
163                #[test]
164                fn $test_name() {
165                    assert_eq!(
166                        format!("{}", $parsed),
167                        $formatted
168                    );
169                }
170            )+
171        }
172    }
173
174    test_parse_ok! {
175        test_parse_absolute_path => {
176            url = "fuchsia-boot:///package",
177            path = "/package".to_string(),
178            resource = None,
179        }
180        test_parse_multiple_path_segments => {
181            url = "fuchsia-boot:///package/foo",
182            path = "/package/foo".to_string(),
183            resource = None,
184        }
185        test_parse_more_path_segments => {
186            url = "fuchsia-boot:///package/foo/bar/baz",
187            path = "/package/foo/bar/baz".to_string(),
188            resource = None,
189        }
190        test_parse_root => {
191            url = "fuchsia-boot:///",
192            path = "/".to_string(),
193            resource = None,
194        }
195        test_parse_empty_root => {
196            url = "fuchsia-boot://",
197            path = "/".to_string(),
198            resource = None,
199        }
200        test_parse_resource => {
201            url = "fuchsia-boot:///package#resource",
202            path = "/package".to_string(),
203            resource = Some("resource".to_string()),
204        }
205        test_parse_resource_with_path_segments => {
206            url = "fuchsia-boot:///package/foo#resource",
207            path = "/package/foo".to_string(),
208            resource = Some("resource".to_string()),
209        }
210        test_parse_empty_resource => {
211            url = "fuchsia-boot:///package#",
212            path = "/package".to_string(),
213            resource = None,
214        }
215        test_parse_root_empty_resource => {
216            url = "fuchsia-boot:///#",
217            path = "/".to_string(),
218            resource = None,
219        }
220        test_parse_root_resource => {
221            url = "fuchsia-boot:///#resource",
222            path = "/".to_string(),
223            resource = Some("resource".to_string()),
224        }
225        test_parse_empty_root_empty_resource => {
226            url = "fuchsia-boot://#",
227            path = "/".to_string(),
228            resource = None,
229        }
230        test_parse_empty_root_present_resource => {
231            url = "fuchsia-boot://#meta/root.cm",
232            path = "/".to_string(),
233            resource = Some("meta/root.cm".to_string()),
234        }
235        test_parse_large_path_segments => {
236            url = format!(
237                "fuchsia-boot:///{}/{}/{}",
238                "a".repeat(255),
239                "b".repeat(255),
240                "c".repeat(255),
241            ),
242            path = format!("/{}/{}/{}", "a".repeat(255), "b".repeat(255), "c".repeat(255)),
243            resource = None,
244        }
245    }
246
247    test_parse_err! {
248        test_parse_missing_scheme => {
249            urls = [
250                "package",
251            ],
252            err = ParseError::MissingScheme,
253        }
254        test_parse_invalid_scheme => {
255            urls = [
256                "fuchsia-pkg://",
257            ],
258            err = ParseError::InvalidScheme,
259        }
260        test_parse_invalid_path => {
261            urls = [
262                "fuchsia-boot:////",
263            ],
264            err = ParseError::InvalidPathSegment(PackagePathSegmentError::Empty),
265        }
266        test_parse_invalid_path_another => {
267            urls = [
268                "fuchsia-boot:///package:1234",
269            ],
270            err = ParseError::InvalidPathSegment(
271                PackagePathSegmentError::InvalidCharacter { character: ':'}),
272        }
273        test_parse_invalid_path_segment => {
274            urls = [
275                "fuchsia-boot:///path/foo$bar/baz",
276            ],
277            err = ParseError::InvalidPathSegment(
278                PackagePathSegmentError::InvalidCharacter { character: '$' }
279            ),
280        }
281        test_parse_path_cannot_be_longer_than_255_chars => {
282            urls = [
283                &format!("fuchsia-boot:///fuchsia.com/{}", "a".repeat(256)),
284            ],
285            err = ParseError::InvalidPathSegment(PackagePathSegmentError::TooLong(256)),
286        }
287        test_parse_path_cannot_have_invalid_characters => {
288            urls = [
289                "fuchsia-boot:///$",
290            ],
291            err = ParseError::InvalidPathSegment(
292                PackagePathSegmentError::InvalidCharacter { character: '$' }
293            ),
294        }
295        test_parse_path_cannot_have_invalid_characters_another => {
296            urls = [
297                "fuchsia-boot:///foo$bar",
298            ],
299            err = ParseError::InvalidPathSegment(
300                PackagePathSegmentError::InvalidCharacter { character: '$' }
301            ),
302        }
303        test_parse_host_must_be_empty => {
304            urls = [
305                "fuchsia-boot://hello",
306            ],
307            err = ParseError::HostMustBeEmpty,
308        }
309        test_parse_resource_cannot_be_slash => {
310            urls = [
311                "fuchsia-boot:///package#/",
312            ],
313            err = ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
314        }
315        test_parse_resource_cannot_start_with_slash => {
316            urls = [
317                "fuchsia-boot:///package#/foo",
318                "fuchsia-boot:///package#/foo/bar",
319            ],
320            err = ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
321        }
322        test_parse_resource_cannot_end_with_slash => {
323            urls = [
324                "fuchsia-boot:///package#foo/",
325                "fuchsia-boot:///package#foo/bar/",
326            ],
327            err = ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
328        }
329        test_parse_resource_cannot_contain_dot_dot => {
330            urls = [
331                "fuchsia-boot:///package#foo/../bar",
332            ],
333            err = ParseError::InvalidResourcePath(ResourcePathError::NameIsDotDot),
334        }
335        test_parse_resource_cannot_contain_empty_segments => {
336            urls = [
337                "fuchsia-boot:///package#foo//bar",
338            ],
339            err = ParseError::InvalidResourcePath(ResourcePathError::NameEmpty),
340        }
341        test_parse_resource_cannot_contain_percent_encoded_nul_chars => {
342            urls = [
343                "fuchsia-boot:///package#foo%00bar",
344            ],
345            err = ParseError::InvalidResourcePath(ResourcePathError::NameContainsNull),
346        }
347        test_parse_rejects_query_params => {
348            urls = [
349                "fuchsia-boot:///package?foo=bar",
350            ],
351            err = ParseError::ExtraQueryParameters,
352        }
353    }
354
355    test_format! {
356        test_format_path_url => {
357            parsed = BootUrl::new_path("/path/to".to_string()).unwrap(),
358            formatted = "fuchsia-boot:///path/to",
359        }
360        test_format_resource_url => {
361            parsed = BootUrl::new_resource("/path/to".to_string(), "path/to/resource".to_string()).unwrap(),
362            formatted = "fuchsia-boot:///path/to#path/to/resource",
363        }
364    }
365
366    #[test]
367    fn test_new_path() {
368        let url = BootUrl::new_path("/path/to".to_string()).unwrap();
369        assert_eq!("/path/to", url.path());
370        assert_eq!(None, url.resource());
371        assert_eq!(url, url.root_url());
372        assert_eq!("fuchsia-boot:///path/to", format!("{}", url.root_url()));
373    }
374
375    #[test]
376    fn test_new_resource() {
377        let url = BootUrl::new_resource("/path/to".to_string(), "foo/bar".to_string()).unwrap();
378        assert_eq!("/path/to", url.path());
379        assert_eq!(Some("foo/bar"), url.resource());
380        let mut url_no_resource = url.clone();
381        url_no_resource.resource = None;
382        assert_eq!(url_no_resource, url.root_url());
383        assert_eq!("fuchsia-boot:///path/to", format!("{}", url.root_url()));
384    }
385
386    #[test]
387    fn test_new_resource_without_variant() {
388        let url =
389            BootUrl::new_resource_without_variant("/name/0".to_string(), "foo/bar".to_string())
390                .unwrap();
391        assert_eq!("/name", url.path());
392        assert_eq!(Some("foo/bar"), url.resource());
393
394        let url = BootUrl::new_resource_without_variant("/name".to_string(), "foo/bar".to_string())
395            .unwrap();
396        assert_eq!("/name", url.path());
397        assert_eq!(Some("foo/bar"), url.resource());
398    }
399
400    #[test]
401    fn test_from_component_url() {
402        let component_url = AbsoluteComponentUrl::new(
403            "fuchsia-pkg://fuchsia.com".parse().unwrap(),
404            "package_name".parse().unwrap(),
405            None,
406            None,
407            "path/to/resource.txt".into(),
408        )
409        .unwrap();
410        let boot_url = BootUrl::try_from(&component_url).unwrap();
411        assert_eq!(
412            boot_url,
413            BootUrl { path: "/package_name".into(), resource: Some("path/to/resource.txt".into()) }
414        );
415    }
416}