Skip to main content

fuchsia_url/
lib.rs

1// Copyright 2018 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 fuchsia_hash::{HASH_SIZE, Hash};
6
7pub mod boot;
8pub mod builtin_url;
9pub mod errors;
10mod fuchsia_pkg_absolute_component_url;
11mod fuchsia_pkg_absolute_package_url;
12mod fuchsia_pkg_component_url;
13mod fuchsia_pkg_package_url;
14mod fuchsia_pkg_pinned_absolute_package_url;
15mod fuchsia_pkg_unpinned_absolute_package_url;
16pub(crate) mod generic;
17mod host;
18mod parse;
19mod path;
20mod relative_component_url;
21mod relative_package_url;
22mod repository_url;
23mod resource;
24pub mod test;
25
26pub use crate::errors::ParseError;
27pub use crate::fuchsia_pkg_absolute_component_url::FuchsiaPkgAbsoluteComponentUrl;
28pub use crate::fuchsia_pkg_absolute_package_url::FuchsiaPkgAbsolutePackageUrl;
29pub use crate::fuchsia_pkg_component_url::FuchsiaPkgComponentUrl;
30pub use crate::fuchsia_pkg_package_url::FuchsiaPkgPackageUrl;
31pub use crate::fuchsia_pkg_pinned_absolute_package_url::FuchsiaPkgPinnedAbsolutePackageUrl;
32pub use crate::fuchsia_pkg_unpinned_absolute_package_url::FuchsiaPkgUnpinnedAbsolutePackageUrl;
33pub mod fuchsia_pkg {
34    pub use crate::{
35        FuchsiaPkgAbsoluteComponentUrl as AbsoluteComponentUrl,
36        FuchsiaPkgAbsolutePackageUrl as AbsolutePackageUrl, FuchsiaPkgComponentUrl as ComponentUrl,
37        FuchsiaPkgPackageUrl as PackageUrl,
38        FuchsiaPkgPinnedAbsolutePackageUrl as PinnedAbsolutePackageUrl,
39        FuchsiaPkgUnpinnedAbsolutePackageUrl as UnpinnedAbsolutePackageUrl,
40    };
41}
42pub use crate::generic::{NoneHash, NoneHost};
43pub use crate::parse::{MAX_PACKAGE_PATH_SEGMENT_BYTES, PackageName, PackageVariant};
44pub use crate::path::Path;
45pub(crate) use crate::path::parse_path_to_name_and_variant;
46pub use crate::relative_component_url::RelativeComponentUrl;
47pub use crate::relative_package_url::RelativePackageUrl;
48pub use crate::repository_url::RepositoryUrl;
49pub use crate::resource::{Resource, ResourcePathError};
50
51use crate::host::Host;
52use percent_encoding::{AsciiSet, CONTROLS};
53use std::sync::LazyLock;
54
55/// https://url.spec.whatwg.org/#fragment-percent-encode-set
56const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
57
58const RELATIVE_SCHEME: &str = "relative";
59
60/// A default base URL from which to parse relative component URL
61/// components.
62static RELATIVE_BASE: LazyLock<url::Url> =
63    LazyLock::new(|| url::Url::parse(&format!("{RELATIVE_SCHEME}:///")).unwrap());
64
65#[derive(Clone, Copy, PartialEq, Eq, Debug)]
66pub enum Scheme {
67    Builtin,
68    FuchsiaPkg,
69    FuchsiaBoot,
70}
71
72impl std::fmt::Display for Scheme {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            Self::Builtin => builtin_url::SCHEME,
76            Self::FuchsiaPkg => repository_url::SCHEME,
77            Self::FuchsiaBoot => boot::SCHEME_STR,
78        }
79        .fmt(f)
80    }
81}
82
83#[derive(Debug, PartialEq, Eq)]
84struct UrlParts {
85    scheme: Option<Scheme>,
86    host: Option<Host>,
87    path: Option<Path>,
88    hash: Option<Hash>,
89    resource: Option<Resource>,
90}
91
92impl UrlParts {
93    fn parse(input: &str) -> Result<Self, ParseError> {
94        match url::Url::parse(input) {
95            Ok(url) => Self::from_url(&url),
96            Err(url::ParseError::RelativeUrlWithoutBase) => {
97                Self::from_scheme_and_url(None, &RELATIVE_BASE.join(input)?)
98            }
99            Err(e) => Err(e)?,
100        }
101    }
102    fn from_url(url: &url::Url) -> Result<Self, ParseError> {
103        Self::from_scheme_and_url(
104            Some(match url.scheme() {
105                builtin_url::SCHEME => Scheme::Builtin,
106                repository_url::SCHEME => Scheme::FuchsiaPkg,
107                boot::SCHEME_STR => Scheme::FuchsiaBoot,
108                _ => return Err(ParseError::InvalidScheme),
109            }),
110            url,
111        )
112    }
113    fn from_scheme_and_url(scheme: Option<Scheme>, url: &url::Url) -> Result<Self, ParseError> {
114        if url.port().is_some() {
115            return Err(ParseError::CannotContainPort);
116        }
117
118        if !url.username().is_empty() {
119            return Err(ParseError::CannotContainUsername);
120        }
121
122        if url.password().is_some() {
123            return Err(ParseError::CannotContainPassword);
124        }
125
126        let host = url
127            .host_str()
128            .filter(|s| !s.is_empty())
129            .map(|s| Host::parse(s.to_string()))
130            .transpose()?;
131
132        // When parsing URLs, there are three kinds of scheme that affect the parsed host and path:
133        //   * File: file
134        //   * SpecialNotFile: http, https, ws, wss, ftp
135        //   * NotSpecial: <anything else>
136        // https://cs.opensource.google/fuchsia/fuchsia/+/main:third_party/rust_crates/vendor/url-2.3.1/src/parser.rs;l=152-168;drc=5d413711388939e4a532a0ab8bfb331e6044ee02
137        //
138        // | input         | host | path   | cannot-be-a-base | stringified   |
139        // |---------------+------+--------+------------------+---------------|
140        // | file:text     |      | /text  | false            | file:///text  |
141        // | file:/text    |      | /text  | false            | file:///text  |
142        // | file://text   | text | /      | false            | file://text/  |
143        // | file://text/  | text | /      | false            | file://text/  |
144        // | file://text// | text | /      | false            | file://text/  |
145        // | file:///text  |      | /text  | false            | file:///text  |
146        // | file:///text/ |      | /text/ | false            | file:///text/ |
147        // |               |      |        |                  |               |
148        // | http:text     | text | /      | false            | http://text/  |
149        // | http:/text    | text | /      | false            | http://text/  |
150        // | http://text   | text | /      | false            | http://text/  |
151        // | http://text/  | text | /      | false            | http://text/  |
152        // | http://text// | text | //     | false            | http://text// |
153        // | http:///text  | text | /      | false            | http://text/  |
154        // | http:///text/ | text | /      | false            | http://text/  |
155        // |               |      |        |                  |               |
156        // | else:text     |      | text   | true             | else:text     |
157        // | else:/text    |      | /text  | false            | else:/text    |
158        // | else://text   | text |        | false            | else://text   |
159        // | else://text/  | text | /      | false            | else://text/  |
160        // | else://text// | text | //     | false            | else://text// |
161        // | else:///text  |      | /text  | false            | else:///text  |
162        // | else:///text/ |      | /text/ | false            | else:///text/ |
163        //
164        // Fuchsia uses URLs to encode:
165        //   * scheme: which resolver should CM use
166        //   * host: which package store should the resolver use
167        //   * path: which package from the package store
168        //   * hash: exactly specify the package instead of using the store's path -> hash mapping
169        //   * resource: which file in the package
170        //
171        // Because the path is used to identify a package (some named thing in a collection) and
172        // we have control over how the things are allowed to be named, we simplify the situation
173        // by ignoring the leading slash (i.e. if the path is just a slash we treat the path as
174        // absent and if the path starts with a slash we trim the slash) and requiring that packages
175        // do not start with a slash.
176        let path = if url.path().is_empty() || url.path() == "/" {
177            None
178        } else {
179            Some(url.path().strip_prefix("/").unwrap_or_else(|| url.path()).parse()?)
180        };
181
182        let hash = parse_query_pairs(url.query_pairs())?;
183
184        let resource = if let Some(resource) = url.fragment() {
185            let resource = percent_encoding::percent_decode(resource.as_bytes())
186                .decode_utf8()
187                .map_err(ParseError::ResourcePathPercentDecode)?;
188            if resource.is_empty() {
189                None
190            } else {
191                Some(
192                    Resource::try_from(resource.into_owned())
193                        .map_err(ParseError::InvalidResourcePath)?,
194                )
195            }
196        } else {
197            None
198        };
199
200        Ok(Self { scheme, host, path, hash, resource })
201    }
202}
203
204/// After all other checks, ensure the input string does not change when joined
205/// with the `RELATIVE_BASE` URL, and then removing the base (inverse-join()).
206fn validate_inverse_relative_url(input: &str) -> Result<(), ParseError> {
207    let relative_url = RELATIVE_BASE.join(input)?;
208    let unbased = RELATIVE_BASE.make_relative(&relative_url);
209    if Some(input) == unbased.as_deref() {
210        Ok(())
211    } else {
212        Err(ParseError::InvalidRelativePath(input.to_string(), unbased))?
213    }
214}
215
216fn parse_query_pairs(pairs: url::form_urlencoded::Parse<'_>) -> Result<Option<Hash>, ParseError> {
217    let mut query_hash = None;
218    for (key, value) in pairs {
219        if key == "hash" {
220            if query_hash.is_some() {
221                return Err(ParseError::MultipleHashes);
222            }
223            query_hash = Some(value.parse().map_err(ParseError::InvalidHash)?);
224            // fuchsia-pkg URLs require lowercase hex characters, but fuchsia_hash::Hash::parse
225            // accepts uppercase A-F.
226            if !value.bytes().all(|b| (b >= b'0' && b <= b'9') || (b >= b'a' && b <= b'f')) {
227                return Err(ParseError::UpperCaseHash);
228            }
229        } else {
230            return Err(ParseError::ExtraQueryParameters);
231        }
232    }
233    Ok(query_hash)
234}
235
236/// A URL locating a Fuchsia component. Can be either absolute or relative.
237/// See [`AbsoluteComponentUrl`] and [`RelativeComponentUrl`] for more details.
238pub type ComponentUrl =
239    generic::ComponentUrl<Scheme, generic::OptionHost, Option<Path>, Option<Hash>>;
240
241/// A URL locating a Fuchsia component.
242/// Has the form "<scheme>://[host][/<path>][?hash=<hash>]#<resource>" where:
243///   * "scheme" is a [`Scheme`]
244///   * "host" is an optional [`Host`]
245///   * "path" is an optional [`Path`]
246///   * "hash" is an optional package [`Hash`]
247///   * "resource" is a [`Resource`]
248pub type AbsoluteComponentUrl =
249    generic::AbsoluteComponentUrl<Scheme, generic::OptionHost, Option<Path>, Option<Hash>>;
250
251/// A URL locating a Fuchsia package. Can be either absolute or relative.
252/// See [`AbsolutePackageUrl`] and [`RelativePackageUrl`] for more details.
253pub type PackageUrl = generic::PackageUrl<Scheme, generic::OptionHost, Option<Path>, Option<Hash>>;
254
255/// A URL locating a Fuchsia package.
256/// Has the form "<scheme>://[host][/<path>][?hash=<hash>]" where:
257///   * "scheme" is a [`Scheme`](crate::Scheme)
258///   * "host" is an optional [`Host`](crate::Host)
259///   * "path" is an optional [`Path`](crate::Path)
260///   * "hash" is an optional package [`Hash`](fuchsia_hash::Hash)
261pub type AbsolutePackageUrl =
262    generic::AbsolutePackageUrl<Scheme, generic::OptionHost, Option<Path>, Option<Hash>>;
263
264// Supertrait for sealing traits in this crate.
265trait Sealer {}
266
267#[cfg(test)]
268mod test_validate_inverse_relative_url {
269    use super::*;
270    use assert_matches::assert_matches;
271
272    macro_rules! test_err {
273        (
274            $(
275                $test_name:ident => {
276                    path = $path:expr,
277                    some_unbased = $some_unbased:expr,
278                }
279            )+
280        ) => {
281            $(
282                #[test]
283                fn $test_name() {
284                    let err = ParseError::InvalidRelativePath(
285                        $path.to_string(),
286                        $some_unbased.map(|s: &str| s.to_string()),
287                    );
288                    assert_matches!(
289                        validate_inverse_relative_url($path),
290                        Err(e) if e == err,
291                        "the url {:?}; expected = {:?}",
292                        $path, err
293                    );
294                }
295            )+
296        }
297    }
298
299    test_err! {
300        err_slash_prefix => {
301            path = "/name",
302            some_unbased = Some("name"),
303        }
304        err_three_slashes_prefix => {
305            path = "///name",
306            some_unbased = Some("name"),
307        }
308        err_slash_prefix_with_resource => {
309            path = "/name#resource",
310            some_unbased = Some("name#resource"),
311        }
312        err_three_slashes_prefix_and_resource => {
313            path = "///name#resource",
314            some_unbased = Some("name#resource"),
315        }
316        err_masks_host_must_be_empty_err => {
317            path = "//example.org/name",
318            some_unbased = None,
319        }
320        err_dot_masks_missing_name_err => {
321            path = ".",
322            some_unbased = Some(""),
323        }
324        err_dot_dot_masks_missing_name_err => {
325            path = "..",
326            some_unbased = Some(""),
327        }
328    }
329
330    #[test]
331    fn success() {
332        for path in ["name", "other3-name", "name#resource", "name#reso%09urce"] {
333            let () = validate_inverse_relative_url(path).unwrap();
334        }
335    }
336}
337
338#[cfg(test)]
339mod test_url_parts {
340    use super::*;
341    use assert_matches::assert_matches;
342
343    macro_rules! test_parse_err {
344        (
345            $(
346                $test_name:ident => {
347                    url = $url:expr,
348                    err = $err:pat,
349                }
350            )+
351        ) => {
352            $(
353                #[test]
354                fn $test_name() {
355                    assert_matches!(
356                        UrlParts::parse($url),
357                        Err($err)
358                    );
359                }
360            )+
361        }
362    }
363
364    test_parse_err! {
365        err_invalid_scheme => {
366            url = "bad-scheme://example.org",
367            err = ParseError::InvalidScheme,
368        }
369        err_port => {
370            url = "fuchsia-pkg://example.org:1",
371            err = ParseError::CannotContainPort,
372        }
373        err_username => {
374            url = "fuchsia-pkg://user@example.org",
375            err = ParseError::CannotContainUsername,
376        }
377        err_password => {
378            url = "fuchsia-pkg://:password@example.org",
379            err = ParseError::CannotContainPassword,
380        }
381        err_invalid_host => {
382            url = "fuchsia-pkg://exa$mple.org",
383            err = ParseError::InvalidHost,
384        }
385        // Path validation covered by test_validate_path, this just checks that the path is
386        // validated at all.
387        err_invalid_path => {
388            url = "fuchsia-pkg://example.org//",
389            err = ParseError::InvalidPathSegment(_),
390        }
391        err_empty_hash => {
392            url = "fuchsia-pkg://example.org/?hash=",
393            err = ParseError::InvalidHash(_),
394        }
395        err_invalid_hash => {
396            url = "fuchsia-pkg://example.org/?hash=INVALID_HASH",
397            err = ParseError::InvalidHash(_),
398        }
399        err_uppercase_hash => {
400            url = "fuchsia-pkg://example.org/?hash=A000000000000000000000000000000000000000000000000000000000000000",
401            err = ParseError::UpperCaseHash,
402        }
403        err_hash_too_long => {
404            url = "fuchsia-pkg://example.org/?hash=00000000000000000000000000000000000000000000000000000000000000001",
405            err = ParseError::InvalidHash(_),
406        }
407        err_hash_too_short => {
408            url = "fuchsia-pkg://example.org/?hash=000000000000000000000000000000000000000000000000000000000000000",
409            err = ParseError::InvalidHash(_),
410        }
411        err_multiple_hashes => {
412            url = "fuchsia-pkg://example.org/?hash=0000000000000000000000000000000000000000000000000000000000000000&\
413            hash=0000000000000000000000000000000000000000000000000000000000000000",
414            err = ParseError::MultipleHashes,
415        }
416        err_non_hash_query_parameter => {
417            url = "fuchsia-pkg://example.org/?invalid-key=invalid-value",
418            err = ParseError::ExtraQueryParameters,
419        }
420        err_resource_slash => {
421            url = "fuchsia-pkg://example.org/name#/",
422            err = ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
423        }
424        err_resource_leading_slash => {
425            url = "fuchsia-pkg://example.org/name#/resource",
426            err = ParseError::InvalidResourcePath(ResourcePathError::PathStartsWithSlash),
427        }
428        err_resource_trailing_slash => {
429            url = "fuchsia-pkg://example.org/name#resource/",
430            err = ParseError::InvalidResourcePath(ResourcePathError::PathEndsWithSlash),
431        }
432        err_resource_empty_segment => {
433            url = "fuchsia-pkg://example.org/name#resource//other",
434            err = ParseError::InvalidResourcePath(ResourcePathError::NameEmpty),
435        }
436        err_resource_bad_segment => {
437            url = "fuchsia-pkg://example.org/name#resource/./other",
438            err = ParseError::InvalidResourcePath(ResourcePathError::NameIsDot),
439        }
440        err_resource_percent_encoded_null => {
441            url = "fuchsia-pkg://example.org/name#resource%00",
442            err = ParseError::InvalidResourcePath(ResourcePathError::NameContainsNull),
443        }
444        err_resource_unencoded_null => {
445            url =  "fuchsia-pkg://example.org/name#reso\x00urce",
446            err = ParseError::InvalidResourcePath(ResourcePathError::NameContainsNull),
447        }
448    }
449
450    macro_rules! test_parse_ok {
451        (
452            $(
453                $test_name:ident => {
454                    url = $url:expr,
455                    scheme = $scheme:expr,
456                    host = $host:expr,
457                    path = $path:expr,
458                    hash = $hash:expr,
459                    resource = $resource:expr,
460                }
461            )+
462        ) => {
463            $(
464                #[test]
465                fn $test_name() {
466                    assert_eq!(
467                        UrlParts::parse($url).unwrap(),
468                        UrlParts {
469                            scheme: $scheme,
470                            host: $host,
471                            path: $path.map(|s| s.parse::<Path>().unwrap()),
472                            hash: $hash,
473                            resource: $resource,
474                        }
475                    )
476                }
477            )+
478        }
479    }
480
481    test_parse_ok! {
482        ok_fuchsia_pkg_scheme => {
483            url =  "fuchsia-pkg://",
484            scheme = Some(Scheme::FuchsiaPkg),
485            host = None,
486            path = Option::<&str>::None,
487            hash = None,
488            resource = None,
489        }
490        ok_fuchsia_boot_scheme => {
491            url =  "fuchsia-boot://",
492            scheme = Some(Scheme::FuchsiaBoot),
493            host = None,
494            path = Option::<&str>::None,
495            hash = None,
496            resource = None,
497        }
498        ok_host => {
499            url =  "fuchsia-pkg://example.org",
500            scheme = Some(Scheme::FuchsiaPkg),
501            host = Some(Host::parse("example.org".into()).unwrap()),
502            path = Option::<&str>::None,
503            hash = None,
504            resource = None,
505        }
506        ok_path_single_segment => {
507            url =  "fuchsia-pkg:///name",
508            scheme = Some(Scheme::FuchsiaPkg),
509            host = None,
510            path = Some("name"),
511            hash = None,
512            resource = None,
513        }
514        ok_path_multiple_segment => {
515            url =  "fuchsia-pkg:///name/variant/other",
516            scheme = Some(Scheme::FuchsiaPkg),
517            host = None,
518            path = Some("name/variant/other"),
519            hash = None,
520            resource = None,
521        }
522        ok_hash => {
523            url =  "fuchsia-pkg://?hash=0000000000000000000000000000000000000000000000000000000000000000",
524            scheme = Some(Scheme::FuchsiaPkg),
525            host = None,
526            path = Option::<&str>::None,
527            hash = Some(
528                "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap()
529            ),
530            resource = None,
531        }
532        ok_resource_single_segment => {
533            url =  "fuchsia-pkg://#resource",
534            scheme = Some(Scheme::FuchsiaPkg),
535            host = None,
536            path = Option::<&str>::None,
537            hash = None,
538            resource = Some("resource".parse().unwrap()),
539        }
540        ok_resource_multiple_segment => {
541            url =  "fuchsia-pkg://#resource/again/third",
542            scheme = Some(Scheme::FuchsiaPkg),
543            host = None,
544            path = Option::<&str>::None,
545            hash = None,
546            resource = Some("resource/again/third".parse().unwrap()),
547        }
548        ok_resource_encoded_control_character => {
549            url =  "fuchsia-pkg://#reso%09urce",
550            scheme = Some(Scheme::FuchsiaPkg),
551            host = None,
552            path = Option::<&str>::None,
553            hash = None,
554            resource = Some("reso\turce".parse().unwrap()),
555        }
556        ok_all_fields => {
557            url =  "fuchsia-pkg://example.org/name\
558            ?hash=0000000000000000000000000000000000000000000000000000000000000000\
559            #resource",
560            scheme = Some(Scheme::FuchsiaPkg),
561            host = Some(Host::parse("example.org".into()).unwrap()),
562            path = Some("name"),
563            hash = Some(
564                "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap()
565            ),
566            resource = Some("resource".parse().unwrap()),
567        }
568        ok_relative_path_single_segment => {
569            url =  "name",
570            scheme = None,
571            host = None,
572            path = Some("name"),
573            hash = None,
574            resource = None,
575        }
576        ok_relative_path_single_segment_leading_slash => {
577            url =  "/name",
578            scheme = None,
579            host = None,
580            path = Some("name"),
581            hash = None,
582            resource = None,
583        }
584        ok_relative_path_multiple_segment => {
585            url =  "name/variant/other",
586            scheme = None,
587            host = None,
588            path = Some("name/variant/other"),
589            hash = None,
590            resource = None,
591        }
592        ok_relative_path_multiple_segment_leading_slash => {
593            url =  "/name/variant/other",
594            scheme = None,
595            host = None,
596            path = Some("name/variant/other"),
597            hash = None,
598            resource = None,
599        }
600        ok_relative_hash => {
601            url =  "?hash=0000000000000000000000000000000000000000000000000000000000000000",
602            scheme = None,
603            host = None,
604            path = Option::<&str>::None,
605            hash = Some(
606                "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap()
607            ),
608            resource = None,
609        }
610        ok_relative_resource_single_segment => {
611            url =  "#resource",
612            scheme = None,
613            host = None,
614            path = Option::<&str>::None,
615            hash = None,
616            resource = Some("resource".parse().unwrap()),
617        }
618        ok_relative_resource_multiple_segment => {
619            url =  "#resource/again/third",
620            scheme = None,
621            host = None,
622            path = Option::<&str>::None,
623            hash = None,
624            resource = Some("resource/again/third".parse().unwrap()),
625        }
626        ok_relative_all_fields => {
627            url =  "name\
628            ?hash=0000000000000000000000000000000000000000000000000000000000000000\
629            #resource",
630            scheme = None,
631            host = None,
632            path = Some("name"),
633            hash = Some(
634                "0000000000000000000000000000000000000000000000000000000000000000".parse().unwrap()
635            ),
636            resource = Some("resource".parse().unwrap()),
637        }
638    }
639}
640
641#[cfg(test)]
642mod test_url_type_aliases {
643    use super::*;
644
645    #[test]
646    fn component_url_round_trip() {
647        let abs_empty = "fuchsia-boot://#my-resource";
648        assert_eq!(ComponentUrl::parse(abs_empty).unwrap().to_string(), abs_empty);
649
650        let abs_present = "fuchsia-boot://my-host/my/path\
651            ?hash=0000000000000000000000000000000000000000000000000000000000000000#my-resource";
652        assert_eq!(ComponentUrl::parse(abs_present).unwrap().to_string(), abs_present);
653
654        let rel = "my-path#my-resource";
655        assert_eq!(ComponentUrl::parse(rel).unwrap().to_string(), rel);
656    }
657
658    #[test]
659    fn package_url_round_trip() {
660        let abs_empty = "fuchsia-boot://";
661        assert_eq!(PackageUrl::parse(abs_empty).unwrap().to_string(), abs_empty);
662
663        let abs_present = "fuchsia-boot://my-host/my/path\
664            ?hash=0000000000000000000000000000000000000000000000000000000000000000";
665        assert_eq!(PackageUrl::parse(abs_present).unwrap().to_string(), abs_present);
666
667        let rel = "my-path";
668        assert_eq!(PackageUrl::parse(rel).unwrap().to_string(), rel);
669    }
670}