1use crate::io::AssetSourceId;
2use alloc::{
3 borrow::ToOwned,
4 string::{String, ToString},
5};
6use atomicow::CowArc;
7use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
8use core::{
9 fmt::{Debug, Display},
10 hash::Hash,
11 ops::Deref,
12};
13use serde::{de::Visitor, Deserialize, Serialize};
14use std::path::{Path, PathBuf};
15use thiserror::Error;
16
17#[derive(Eq, PartialEq, Hash, Clone, Default, Reflect)]
55#[reflect(opaque)]
56#[reflect(Debug, PartialEq, Hash, Clone, Serialize, Deserialize)]
57pub struct AssetPath<'a> {
58 source: AssetSourceId<'a>,
59 path: CowArc<'a, Path>,
60 label: Option<CowArc<'a, str>>,
61}
62
63impl<'a> Debug for AssetPath<'a> {
64 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65 Display::fmt(self, f)
66 }
67}
68
69impl<'a> Display for AssetPath<'a> {
70 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71 if let AssetSourceId::Name(name) = self.source() {
72 write!(f, "{name}://")?;
73 }
74 write!(f, "{}", self.path.display())?;
75 if let Some(label) = &self.label {
76 write!(f, "#{label}")?;
77 }
78 Ok(())
79 }
80}
81
82#[derive(Error, Debug, PartialEq, Eq)]
84pub enum ParseAssetPathError {
85 #[error("Asset source must not contain a `#` character")]
87 InvalidSourceSyntax,
88 #[error("Asset label must not contain a `://` substring")]
90 InvalidLabelSyntax,
91 #[error("Asset source must be at least one character. Either specify the source before the '://' or remove the `://`")]
93 MissingSource,
94 #[error("Asset label must be at least one character. Either specify the label after the '#' or remove the '#'")]
96 MissingLabel,
97}
98
99impl<'a> AssetPath<'a> {
100 pub fn parse(asset_path: &'a str) -> AssetPath<'a> {
112 Self::try_parse(asset_path).unwrap()
113 }
114
115 pub fn try_parse(asset_path: &'a str) -> Result<AssetPath<'a>, ParseAssetPathError> {
126 let (source, path, label) = Self::parse_internal(asset_path)?;
127 Ok(Self {
128 source: match source {
129 Some(source) => AssetSourceId::Name(CowArc::Borrowed(source)),
130 None => AssetSourceId::Default,
131 },
132 path: CowArc::Borrowed(path),
133 label: label.map(CowArc::Borrowed),
134 })
135 }
136
137 fn parse_internal(
139 asset_path: &str,
140 ) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> {
141 let chars = asset_path.char_indices();
142 let mut source_range = None;
143 let mut path_range = 0..asset_path.len();
144 let mut label_range = None;
145
146 let mut source_delimiter_chars_matched = 0;
155 let mut last_found_source_index = 0;
156 for (index, char) in chars {
157 match char {
158 ':' => {
159 source_delimiter_chars_matched = 1;
160 }
161 '/' => {
162 match source_delimiter_chars_matched {
163 1 => {
164 source_delimiter_chars_matched = 2;
165 }
166 2 => {
167 if source_range.is_none() {
169 if label_range.is_some() {
171 return Err(ParseAssetPathError::InvalidSourceSyntax);
172 }
173 source_range = Some(0..index - 2);
174 path_range.start = index + 1;
175 }
176 last_found_source_index = index - 2;
177 source_delimiter_chars_matched = 0;
178 }
179 _ => {}
180 }
181 }
182 '#' => {
183 path_range.end = index;
184 label_range = Some(index + 1..asset_path.len());
185 source_delimiter_chars_matched = 0;
186 }
187 _ => {
188 source_delimiter_chars_matched = 0;
189 }
190 }
191 }
192 if let Some(range) = label_range.clone() {
194 if range.start <= last_found_source_index {
196 return Err(ParseAssetPathError::InvalidLabelSyntax);
197 }
198 }
199 let source = match source_range {
202 Some(source_range) => {
203 if source_range.is_empty() {
204 return Err(ParseAssetPathError::MissingSource);
205 }
206 Some(&asset_path[source_range])
207 }
208 None => None,
209 };
210 let label = match label_range {
213 Some(label_range) => {
214 if label_range.is_empty() {
215 return Err(ParseAssetPathError::MissingLabel);
216 }
217 Some(&asset_path[label_range])
218 }
219 None => None,
220 };
221
222 let path = Path::new(&asset_path[path_range]);
223 Ok((source, path, label))
224 }
225
226 #[inline]
228 pub fn from_path_buf(path_buf: PathBuf) -> AssetPath<'a> {
229 AssetPath {
230 path: CowArc::Owned(path_buf.into()),
231 source: AssetSourceId::Default,
232 label: None,
233 }
234 }
235
236 #[inline]
238 pub fn from_path(path: &'a Path) -> AssetPath<'a> {
239 AssetPath {
240 path: CowArc::Borrowed(path),
241 source: AssetSourceId::Default,
242 label: None,
243 }
244 }
245
246 #[inline]
249 pub fn source(&self) -> &AssetSourceId<'_> {
250 &self.source
251 }
252
253 #[inline]
255 pub fn label(&self) -> Option<&str> {
256 self.label.as_deref()
257 }
258
259 #[inline]
261 pub fn label_cow(&self) -> Option<CowArc<'a, str>> {
262 self.label.clone()
263 }
264
265 #[inline]
267 pub fn path(&self) -> &Path {
268 self.path.deref()
269 }
270
271 #[inline]
273 pub fn without_label(&self) -> AssetPath<'_> {
274 Self {
275 source: self.source.clone(),
276 path: self.path.clone(),
277 label: None,
278 }
279 }
280
281 #[inline]
283 pub fn remove_label(&mut self) {
284 self.label = None;
285 }
286
287 #[inline]
289 pub fn take_label(&mut self) -> Option<CowArc<'a, str>> {
290 self.label.take()
291 }
292
293 #[inline]
296 pub fn with_label(self, label: impl Into<CowArc<'a, str>>) -> AssetPath<'a> {
297 AssetPath {
298 source: self.source,
299 path: self.path,
300 label: Some(label.into()),
301 }
302 }
303
304 #[inline]
307 pub fn with_source(self, source: impl Into<AssetSourceId<'a>>) -> AssetPath<'a> {
308 AssetPath {
309 source: source.into(),
310 path: self.path,
311 label: self.label,
312 }
313 }
314
315 pub fn parent(&self) -> Option<AssetPath<'a>> {
317 let path = match &self.path {
318 CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?),
319 CowArc::Static(path) => CowArc::Static(path.parent()?),
320 CowArc::Owned(path) => CowArc::Owned(path.parent()?.into()),
321 };
322 Some(AssetPath {
323 source: self.source.clone(),
324 label: None,
325 path,
326 })
327 }
328
329 pub fn into_owned(self) -> AssetPath<'static> {
335 AssetPath {
336 source: self.source.into_owned(),
337 path: self.path.into_owned(),
338 label: self.label.map(CowArc::into_owned),
339 }
340 }
341
342 #[inline]
348 pub fn clone_owned(&self) -> AssetPath<'static> {
349 self.clone().into_owned()
350 }
351
352 pub fn resolve(&self, path: &AssetPath<'_>) -> AssetPath<'static> {
374 let is_label_only = matches!(path.source(), AssetSourceId::Default)
375 && path.path().as_os_str().is_empty()
376 && path.label().is_some();
377
378 if is_label_only {
379 self.clone_owned()
380 .with_label(path.label().unwrap().to_owned())
381 } else {
382 let explicit_source = match path.source() {
383 AssetSourceId::Default => None,
384 AssetSourceId::Name(name) => Some(name.as_ref()),
385 };
386
387 self.resolve_from_parts(false, explicit_source, path.path(), path.label())
388 }
389 }
390
391 pub fn resolve_embed(&self, path: &AssetPath<'_>) -> AssetPath<'static> {
411 let is_label_only = matches!(path.source(), AssetSourceId::Default)
412 && path.path().as_os_str().is_empty()
413 && path.label().is_some();
414
415 if is_label_only {
416 self.clone_owned()
417 .with_label(path.label().unwrap().to_owned())
418 } else {
419 let explicit_source = match path.source() {
420 AssetSourceId::Default => None,
421 AssetSourceId::Name(name) => Some(name.as_ref()),
422 };
423
424 self.resolve_from_parts(true, explicit_source, path.path(), path.label())
425 }
426 }
427
428 pub fn resolve_str(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
434 self.resolve_internal(path, false)
435 }
436
437 pub fn resolve_embed_str(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
444 self.resolve_internal(path, true)
445 }
446
447 fn resolve_from_parts(
448 &self,
449 replace: bool,
450 source: Option<&str>,
451 rpath: &Path,
452 rlabel: Option<&str>,
453 ) -> AssetPath<'static> {
454 let mut base_path = PathBuf::from(self.path());
455 if replace && !self.path.to_str().unwrap().ends_with('/') {
456 base_path.pop();
458 }
459
460 let mut is_absolute = false;
462 let rpath = match rpath.strip_prefix("/") {
463 Ok(p) => {
464 is_absolute = true;
465 p
466 }
467 _ => rpath,
468 };
469
470 let mut result_path = if !is_absolute && source.is_none() {
471 base_path
472 } else {
473 PathBuf::new()
474 };
475 result_path.push(rpath);
476 result_path = normalize_path(result_path.as_path());
477
478 AssetPath {
479 source: match source {
480 Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
481 None => self.source.clone_owned(),
482 },
483 path: CowArc::Owned(result_path.into()),
484 label: rlabel.map(|l| CowArc::Owned(l.into())),
485 }
486 }
487
488 fn resolve_internal(
489 &self,
490 path: &str,
491 replace: bool,
492 ) -> Result<AssetPath<'static>, ParseAssetPathError> {
493 if let Some(label) = path.strip_prefix('#') {
494 Ok(self.clone_owned().with_label(label.to_owned()))
496 } else {
497 let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
498 Ok(self.resolve_from_parts(replace, source, rpath, rlabel))
499 }
500 }
501
502 pub fn get_full_extension(&self) -> Option<&str> {
507 let file_name = self.path().file_name()?.to_str()?;
508 let index = file_name.find('.')?;
509 let mut extension = &file_name[index + 1..];
510
511 let query = extension.find('?');
513 if let Some(offset) = query {
514 extension = &extension[..offset];
515 }
516
517 Some(extension)
518 }
519
520 pub fn get_extension(&self) -> Option<&str> {
526 let full_extension = self.get_full_extension()?;
527 Some(match full_extension.rfind(".") {
528 None => full_extension,
529 Some(index) => &full_extension[(index + 1)..],
530 })
531 }
532
533 pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
534 full_extension.char_indices().filter_map(|(i, c)| {
535 if c == '.' {
536 Some(&full_extension[i + 1..])
537 } else {
538 None
539 }
540 })
541 }
542
543 pub fn is_unapproved(&self) -> bool {
570 use std::path::Component;
571
572 let mut component_count: usize = 0;
573
574 for component in self.path.components() {
575 match component {
576 Component::Prefix(_) | Component::RootDir => return true,
577 Component::CurDir => {}
578 Component::ParentDir if component_count == 0 => return true,
579 Component::ParentDir => component_count -= 1,
580 Component::Normal(_) => component_count += 1,
581 }
582 }
583
584 false
585 }
586}
587
588impl From<&'static str> for AssetPath<'static> {
592 #[inline]
593 fn from(asset_path: &'static str) -> Self {
594 let (source, path, label) = Self::parse_internal(asset_path).unwrap();
595 AssetPath {
596 source: source.into(),
597 path: CowArc::Static(path),
598 label: label.map(CowArc::Static),
599 }
600 }
601}
602
603impl<'a> From<&'a String> for AssetPath<'a> {
604 #[inline]
605 fn from(asset_path: &'a String) -> Self {
606 AssetPath::parse(asset_path.as_str())
607 }
608}
609
610impl From<String> for AssetPath<'static> {
611 #[inline]
612 fn from(asset_path: String) -> Self {
613 AssetPath::parse(asset_path.as_str()).into_owned()
614 }
615}
616
617impl From<&'static Path> for AssetPath<'static> {
618 #[inline]
619 fn from(path: &'static Path) -> Self {
620 Self {
621 source: AssetSourceId::Default,
622 path: CowArc::Static(path),
623 label: None,
624 }
625 }
626}
627
628impl From<PathBuf> for AssetPath<'static> {
629 #[inline]
630 fn from(path: PathBuf) -> Self {
631 Self {
632 source: AssetSourceId::Default,
633 path: path.into(),
634 label: None,
635 }
636 }
637}
638
639impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
640 fn from(value: &'a AssetPath<'b>) -> Self {
641 value.clone()
642 }
643}
644
645impl<'a> From<AssetPath<'a>> for PathBuf {
646 fn from(value: AssetPath<'a>) -> Self {
647 value.path().to_path_buf()
648 }
649}
650
651impl<'a> Serialize for AssetPath<'a> {
652 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
653 where
654 S: serde::Serializer,
655 {
656 self.to_string().serialize(serializer)
657 }
658}
659
660impl<'de> Deserialize<'de> for AssetPath<'static> {
661 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
662 where
663 D: serde::Deserializer<'de>,
664 {
665 deserializer.deserialize_string(AssetPathVisitor)
666 }
667}
668
669struct AssetPathVisitor;
670
671impl<'de> Visitor<'de> for AssetPathVisitor {
672 type Value = AssetPath<'static>;
673
674 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
675 formatter.write_str("string AssetPath")
676 }
677
678 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
679 where
680 E: serde::de::Error,
681 {
682 match AssetPath::try_parse(v) {
683 Ok(path) => Ok(path.into_owned()),
684 Err(err) => Err(E::custom(err)),
685 }
686 }
687}
688
689pub(crate) fn normalize_path(path: &Path) -> PathBuf {
692 let mut result_path = PathBuf::new();
693 for elt in path.iter() {
694 if elt == "." {
695 } else if elt == ".." {
697 if result_path.file_name().is_some() {
700 assert!(result_path.pop());
703 } else {
704 result_path.push(elt);
706 }
707 } else {
708 result_path.push(elt);
709 }
710 }
711 result_path
712}
713
714#[cfg(test)]
715mod tests {
716 use crate::AssetPath;
717 use std::path::Path;
718
719 #[test]
720 fn parse_asset_path() {
721 let result = AssetPath::parse_internal("a/b.test");
722 assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
723
724 let result = AssetPath::parse_internal("http://a/b.test");
725 assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
726
727 let result = AssetPath::parse_internal("http://a/b.test#Foo");
728 assert_eq!(
729 result,
730 Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
731 );
732
733 let result = AssetPath::parse_internal("localhost:80/b.test");
734 assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
735
736 let result = AssetPath::parse_internal("http://localhost:80/b.test");
737 assert_eq!(
738 result,
739 Ok((Some("http"), Path::new("localhost:80/b.test"), None))
740 );
741
742 let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
743 assert_eq!(
744 result,
745 Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
746 );
747
748 let result = AssetPath::parse_internal("#insource://a/b.test");
749 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
750
751 let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
752 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
753
754 let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
755 assert!(
756 result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
757 || result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
758 );
759
760 let result = AssetPath::parse_internal("http://");
761 assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
762
763 let result = AssetPath::parse_internal("://x");
764 assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
765
766 let result = AssetPath::parse_internal("a/b.test#");
767 assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
768 }
769
770 #[test]
771 fn test_serialize() {
772 assert!(ron::de::from_str::<AssetPath>("\"a/b.test\"").is_ok());
773 assert!(ron::de::from_str::<AssetPath>("\"a/b.test#\"").is_err());
774 }
775
776 #[test]
777 fn test_parent() {
778 let result = AssetPath::from("a/b.test");
780 assert_eq!(result.parent(), Some(AssetPath::from("a")));
781 assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
782 assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
783
784 let result = AssetPath::from("http://a");
786 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
787 assert_eq!(result.parent().unwrap().parent(), None);
788
789 let result = AssetPath::from("http://a#Foo");
791 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
792 }
793
794 #[test]
795 fn test_with_source() {
796 let result = AssetPath::from("http://a#Foo");
797 assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
798 }
799
800 #[test]
801 fn test_without_label() {
802 let result = AssetPath::from("http://a#Foo");
803 assert_eq!(result.without_label(), AssetPath::from("http://a"));
804 }
805
806 #[test]
807 fn test_resolve_full() {
808 let base = AssetPath::from("alice/bob#carol");
810 assert_eq!(
811 base.resolve_str("/joe/next").unwrap(),
812 AssetPath::from("joe/next")
813 );
814 assert_eq!(
815 base.resolve(&AssetPath::parse("/joe/next")),
816 AssetPath::from("joe/next")
817 );
818 assert_eq!(
819 base.resolve_embed_str("/joe/next").unwrap(),
820 AssetPath::from("joe/next")
821 );
822 assert_eq!(
823 base.resolve_embed(&AssetPath::parse("/joe/next")),
824 AssetPath::from("joe/next")
825 );
826 assert_eq!(
827 base.resolve_str("/joe/next#dave").unwrap(),
828 AssetPath::from("joe/next#dave")
829 );
830 assert_eq!(
831 base.resolve(&AssetPath::parse("/joe/next#dave")),
832 AssetPath::from("joe/next#dave")
833 );
834 assert_eq!(
835 base.resolve_embed_str("/joe/next#dave").unwrap(),
836 AssetPath::from("joe/next#dave")
837 );
838 assert_eq!(
839 base.resolve_embed(&AssetPath::parse("/joe/next#dave")),
840 AssetPath::from("joe/next#dave")
841 );
842 }
843
844 #[test]
845 fn test_resolve_implicit_relative() {
846 let base = AssetPath::from("alice/bob#carol");
848 assert_eq!(
849 base.resolve_str("joe/next").unwrap(),
850 AssetPath::from("alice/bob/joe/next")
851 );
852 assert_eq!(
853 base.resolve(&AssetPath::parse("joe/next")),
854 AssetPath::from("alice/bob/joe/next")
855 );
856 assert_eq!(
857 base.resolve_embed_str("joe/next").unwrap(),
858 AssetPath::from("alice/joe/next")
859 );
860 assert_eq!(
861 base.resolve_embed(&AssetPath::parse("joe/next")),
862 AssetPath::from("alice/joe/next")
863 );
864 assert_eq!(
865 base.resolve_str("joe/next#dave").unwrap(),
866 AssetPath::from("alice/bob/joe/next#dave")
867 );
868 assert_eq!(
869 base.resolve(&AssetPath::parse("joe/next#dave")),
870 AssetPath::from("alice/bob/joe/next#dave")
871 );
872 assert_eq!(
873 base.resolve_embed_str("joe/next#dave").unwrap(),
874 AssetPath::from("alice/joe/next#dave")
875 );
876 assert_eq!(
877 base.resolve_embed(&AssetPath::parse("joe/next#dave")),
878 AssetPath::from("alice/joe/next#dave")
879 );
880 }
881
882 #[test]
883 fn test_resolve_explicit_relative() {
884 let base = AssetPath::from("alice/bob#carol");
886 assert_eq!(
887 base.resolve_str("./martin#dave").unwrap(),
888 AssetPath::from("alice/bob/martin#dave")
889 );
890 assert_eq!(
891 base.resolve(&AssetPath::parse("./martin#dave")),
892 AssetPath::from("alice/bob/martin#dave")
893 );
894 assert_eq!(
895 base.resolve_embed_str("./martin#dave").unwrap(),
896 AssetPath::from("alice/martin#dave")
897 );
898 assert_eq!(
899 base.resolve_embed(&AssetPath::parse("./martin#dave")),
900 AssetPath::from("alice/martin#dave")
901 );
902 assert_eq!(
903 base.resolve_str("../martin#dave").unwrap(),
904 AssetPath::from("alice/martin#dave")
905 );
906 assert_eq!(
907 base.resolve(&AssetPath::parse("../martin#dave")),
908 AssetPath::from("alice/martin#dave")
909 );
910 assert_eq!(
911 base.resolve_embed_str("../martin#dave").unwrap(),
912 AssetPath::from("martin#dave")
913 );
914 assert_eq!(
915 base.resolve_embed(&AssetPath::parse("../martin#dave")),
916 AssetPath::from("martin#dave")
917 );
918 }
919
920 #[test]
921 fn test_resolve_trailing_slash() {
922 let base = AssetPath::from("alice/bob/");
924 assert_eq!(
925 base.resolve_str("./martin#dave").unwrap(),
926 AssetPath::from("alice/bob/martin#dave")
927 );
928 assert_eq!(
929 base.resolve(&AssetPath::parse("./martin#dave")),
930 AssetPath::from("alice/bob/martin#dave")
931 );
932 assert_eq!(
933 base.resolve_embed_str("./martin#dave").unwrap(),
934 AssetPath::from("alice/bob/martin#dave")
935 );
936 assert_eq!(
937 base.resolve_embed(&AssetPath::parse("./martin#dave")),
938 AssetPath::from("alice/bob/martin#dave")
939 );
940 assert_eq!(
941 base.resolve_str("../martin#dave").unwrap(),
942 AssetPath::from("alice/martin#dave")
943 );
944 assert_eq!(
945 base.resolve(&AssetPath::parse("../martin#dave")),
946 AssetPath::from("alice/martin#dave")
947 );
948 assert_eq!(
949 base.resolve_embed_str("../martin#dave").unwrap(),
950 AssetPath::from("alice/martin#dave")
951 );
952 assert_eq!(
953 base.resolve_embed(&AssetPath::parse("../martin#dave")),
954 AssetPath::from("alice/martin#dave")
955 );
956 }
957
958 #[test]
959 fn test_resolve_canonicalize() {
960 let base = AssetPath::from("alice/bob#carol");
962 assert_eq!(
963 base.resolve_str("./martin/stephan/..#dave").unwrap(),
964 AssetPath::from("alice/bob/martin#dave")
965 );
966 assert_eq!(
967 base.resolve(&AssetPath::parse("./martin/stephan/..#dave")),
968 AssetPath::from("alice/bob/martin#dave")
969 );
970 assert_eq!(
971 base.resolve_embed_str("./martin/stephan/..#dave").unwrap(),
972 AssetPath::from("alice/martin#dave")
973 );
974 assert_eq!(
975 base.resolve_embed(&AssetPath::parse("./martin/stephan/..#dave")),
976 AssetPath::from("alice/martin#dave")
977 );
978 assert_eq!(
979 base.resolve_str("../martin/.#dave").unwrap(),
980 AssetPath::from("alice/martin#dave")
981 );
982 assert_eq!(
983 base.resolve(&AssetPath::parse("../martin/.#dave")),
984 AssetPath::from("alice/martin#dave")
985 );
986 assert_eq!(
987 base.resolve_embed_str("../martin/.#dave").unwrap(),
988 AssetPath::from("martin#dave")
989 );
990 assert_eq!(
991 base.resolve_embed(&AssetPath::parse("../martin/.#dave")),
992 AssetPath::from("martin#dave")
993 );
994 assert_eq!(
995 base.resolve_str("/martin/stephan/..#dave").unwrap(),
996 AssetPath::from("martin#dave")
997 );
998 assert_eq!(
999 base.resolve(&AssetPath::parse("/martin/stephan/..#dave")),
1000 AssetPath::from("martin#dave")
1001 );
1002 assert_eq!(
1003 base.resolve_embed_str("/martin/stephan/..#dave").unwrap(),
1004 AssetPath::from("martin#dave")
1005 );
1006 assert_eq!(
1007 base.resolve_embed(&AssetPath::parse("/martin/stephan/..#dave")),
1008 AssetPath::from("martin#dave")
1009 );
1010 }
1011
1012 #[test]
1013 fn test_resolve_canonicalize_base() {
1014 let base = AssetPath::from("alice/../bob#carol");
1016 assert_eq!(
1017 base.resolve_str("./martin/stephan/..#dave").unwrap(),
1018 AssetPath::from("bob/martin#dave")
1019 );
1020 assert_eq!(
1021 base.resolve(&AssetPath::parse("./martin/stephan/..#dave")),
1022 AssetPath::from("bob/martin#dave")
1023 );
1024 assert_eq!(
1025 base.resolve_embed_str("./martin/stephan/..#dave").unwrap(),
1026 AssetPath::from("martin#dave")
1027 );
1028 assert_eq!(
1029 base.resolve_embed(&AssetPath::parse("./martin/stephan/..#dave")),
1030 AssetPath::from("martin#dave")
1031 );
1032 assert_eq!(
1033 base.resolve_str("../martin/.#dave").unwrap(),
1034 AssetPath::from("martin#dave")
1035 );
1036 assert_eq!(
1037 base.resolve(&AssetPath::parse("../martin/.#dave")),
1038 AssetPath::from("martin#dave")
1039 );
1040 assert_eq!(
1041 base.resolve_embed_str("../martin/.#dave").unwrap(),
1042 AssetPath::from("../martin#dave")
1043 );
1044 assert_eq!(
1045 base.resolve_embed(&AssetPath::parse("../martin/.#dave")),
1046 AssetPath::from("../martin#dave")
1047 );
1048 assert_eq!(
1049 base.resolve_str("/martin/stephan/..#dave").unwrap(),
1050 AssetPath::from("martin#dave")
1051 );
1052 assert_eq!(
1053 base.resolve(&AssetPath::parse("/martin/stephan/..#dave")),
1054 AssetPath::from("martin#dave")
1055 );
1056 assert_eq!(
1057 base.resolve_embed_str("/martin/stephan/..#dave").unwrap(),
1058 AssetPath::from("martin#dave")
1059 );
1060 assert_eq!(
1061 base.resolve_embed(&AssetPath::parse("/martin/stephan/..#dave")),
1062 AssetPath::from("martin#dave")
1063 );
1064 }
1065
1066 #[test]
1067 fn test_resolve_canonicalize_with_source() {
1068 let base = AssetPath::from("source://alice/bob#carol");
1070 assert_eq!(
1071 base.resolve_str("./martin/stephan/..#dave").unwrap(),
1072 AssetPath::from("source://alice/bob/martin#dave")
1073 );
1074 assert_eq!(
1075 base.resolve(&AssetPath::parse("./martin/stephan/..#dave")),
1076 AssetPath::from("source://alice/bob/martin#dave")
1077 );
1078 assert_eq!(
1079 base.resolve_embed_str("./martin/stephan/..#dave").unwrap(),
1080 AssetPath::from("source://alice/martin#dave")
1081 );
1082 assert_eq!(
1083 base.resolve_embed(&AssetPath::parse("./martin/stephan/..#dave")),
1084 AssetPath::from("source://alice/martin#dave")
1085 );
1086 assert_eq!(
1087 base.resolve_str("../martin/.#dave").unwrap(),
1088 AssetPath::from("source://alice/martin#dave")
1089 );
1090 assert_eq!(
1091 base.resolve(&AssetPath::parse("../martin/.#dave")),
1092 AssetPath::from("source://alice/martin#dave")
1093 );
1094 assert_eq!(
1095 base.resolve_embed_str("../martin/.#dave").unwrap(),
1096 AssetPath::from("source://martin#dave")
1097 );
1098 assert_eq!(
1099 base.resolve_embed(&AssetPath::parse("../martin/.#dave")),
1100 AssetPath::from("source://martin#dave")
1101 );
1102 assert_eq!(
1103 base.resolve_str("/martin/stephan/..#dave").unwrap(),
1104 AssetPath::from("source://martin#dave")
1105 );
1106 assert_eq!(
1107 base.resolve(&AssetPath::parse("/martin/stephan/..#dave")),
1108 AssetPath::from("source://martin#dave")
1109 );
1110 assert_eq!(
1111 base.resolve_embed_str("/martin/stephan/..#dave").unwrap(),
1112 AssetPath::from("source://martin#dave")
1113 );
1114 assert_eq!(
1115 base.resolve_embed(&AssetPath::parse("/martin/stephan/..#dave")),
1116 AssetPath::from("source://martin#dave")
1117 );
1118 }
1119
1120 #[test]
1121 fn test_resolve_absolute() {
1122 let base = AssetPath::from("alice/bob#carol");
1124 assert_eq!(
1125 base.resolve_str("/martin/stephan").unwrap(),
1126 AssetPath::from("martin/stephan")
1127 );
1128 assert_eq!(
1129 base.resolve(&AssetPath::parse("/martin/stephan")),
1130 AssetPath::from("martin/stephan")
1131 );
1132 assert_eq!(
1133 base.resolve_embed_str("/martin/stephan").unwrap(),
1134 AssetPath::from("martin/stephan")
1135 );
1136 assert_eq!(
1137 base.resolve_embed(&AssetPath::parse("/martin/stephan")),
1138 AssetPath::from("martin/stephan")
1139 );
1140 assert_eq!(
1141 base.resolve_str("/martin/stephan#dave").unwrap(),
1142 AssetPath::from("martin/stephan/#dave")
1143 );
1144 assert_eq!(
1145 base.resolve(&AssetPath::parse("/martin/stephan#dave")),
1146 AssetPath::from("martin/stephan/#dave")
1147 );
1148 assert_eq!(
1149 base.resolve_embed_str("/martin/stephan#dave").unwrap(),
1150 AssetPath::from("martin/stephan/#dave")
1151 );
1152 assert_eq!(
1153 base.resolve_embed(&AssetPath::parse("/martin/stephan#dave")),
1154 AssetPath::from("martin/stephan/#dave")
1155 );
1156 }
1157
1158 #[test]
1159 fn test_resolve_asset_source() {
1160 let base = AssetPath::from("alice/bob#carol");
1162 assert_eq!(
1163 base.resolve_str("source://martin/stephan").unwrap(),
1164 AssetPath::from("source://martin/stephan")
1165 );
1166 assert_eq!(
1167 base.resolve(&AssetPath::parse("source://martin/stephan")),
1168 AssetPath::from("source://martin/stephan")
1169 );
1170 assert_eq!(
1171 base.resolve_embed_str("source://martin/stephan").unwrap(),
1172 AssetPath::from("source://martin/stephan")
1173 );
1174 assert_eq!(
1175 base.resolve_embed(&AssetPath::parse("source://martin/stephan")),
1176 AssetPath::from("source://martin/stephan")
1177 );
1178 assert_eq!(
1179 base.resolve_str("source://martin/stephan#dave").unwrap(),
1180 AssetPath::from("source://martin/stephan/#dave")
1181 );
1182 assert_eq!(
1183 base.resolve(&AssetPath::parse("source://martin/stephan#dave")),
1184 AssetPath::from("source://martin/stephan/#dave")
1185 );
1186 assert_eq!(
1187 base.resolve_embed_str("source://martin/stephan#dave")
1188 .unwrap(),
1189 AssetPath::from("source://martin/stephan/#dave")
1190 );
1191 assert_eq!(
1192 base.resolve_embed(&AssetPath::parse("source://martin/stephan#dave")),
1193 AssetPath::from("source://martin/stephan/#dave")
1194 );
1195 }
1196
1197 #[test]
1198 fn test_resolve_label() {
1199 let base = AssetPath::from("alice/bob#carol");
1201 assert_eq!(
1202 base.resolve_str("#dave").unwrap(),
1203 AssetPath::from("alice/bob#dave")
1204 );
1205 assert_eq!(
1206 base.resolve(&AssetPath::parse("#dave")),
1207 AssetPath::from("alice/bob#dave")
1208 );
1209 assert_eq!(
1210 base.resolve_embed_str("#dave").unwrap(),
1211 AssetPath::from("alice/bob#dave")
1212 );
1213 assert_eq!(
1214 base.resolve_embed(&AssetPath::parse("#dave")),
1215 AssetPath::from("alice/bob#dave")
1216 );
1217 }
1218
1219 #[test]
1220 fn test_resolve_insufficient_elements() {
1221 let base = AssetPath::from("alice/bob#carol");
1223 assert_eq!(
1224 base.resolve_str("../../joe/next").unwrap(),
1225 AssetPath::from("joe/next")
1226 );
1227 assert_eq!(
1228 base.resolve(&AssetPath::parse("../../joe/next")),
1229 AssetPath::from("joe/next")
1230 );
1231 assert_eq!(
1232 base.resolve_embed_str("../../joe/next").unwrap(),
1233 AssetPath::from("../joe/next")
1234 );
1235 assert_eq!(
1236 base.resolve_embed(&AssetPath::parse("../../joe/next")),
1237 AssetPath::from("../joe/next")
1238 );
1239 }
1240
1241 #[test]
1242 fn resolve_embed_relative_to_external_path() {
1243 let base = AssetPath::from("../../a/b.gltf");
1244 assert_eq!(
1245 base.resolve_embed_str("c.bin").unwrap(),
1246 AssetPath::from("../../a/c.bin")
1247 );
1248 assert_eq!(
1249 base.resolve_embed(&AssetPath::parse("c.bin")),
1250 AssetPath::from("../../a/c.bin")
1251 );
1252 }
1253
1254 #[test]
1255 fn resolve_relative_to_external_path() {
1256 let base = AssetPath::from("../../a/b.gltf");
1257 assert_eq!(
1258 base.resolve_str("c.bin").unwrap(),
1259 AssetPath::from("../../a/b.gltf/c.bin")
1260 );
1261 assert_eq!(
1262 base.resolve(&AssetPath::parse("c.bin")),
1263 AssetPath::from("../../a/b.gltf/c.bin")
1264 );
1265 }
1266
1267 #[test]
1268 fn test_get_full_extension() {
1269 let result = AssetPath::from("http://a.tar.gz#Foo");
1270 assert_eq!(result.get_full_extension(), Some("tar.gz"));
1271
1272 let result = AssetPath::from("http://a#Foo");
1273 assert_eq!(result.get_full_extension(), None);
1274
1275 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1276 assert_eq!(result.get_full_extension(), Some("tar.bz2"));
1277
1278 let result = AssetPath::from("asset.Custom");
1279 assert_eq!(result.get_full_extension(), Some("Custom"));
1280 }
1281
1282 #[test]
1283 fn test_get_extension() {
1284 let result = AssetPath::from("http://a.tar.gz#Foo");
1285 assert_eq!(result.get_extension(), Some("gz"));
1286
1287 let result = AssetPath::from("http://a#Foo");
1288 assert_eq!(result.get_extension(), None);
1289
1290 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1291 assert_eq!(result.get_extension(), Some("bz2"));
1292
1293 let result = AssetPath::from("asset.Custom");
1294 assert_eq!(result.get_extension(), Some("Custom"));
1295 }
1296}