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) => path.parent()?.to_path_buf().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 let mut simplified = PathBuf::new();
572 for component in self.path.components() {
573 match component {
574 Component::Prefix(_) | Component::RootDir => return true,
575 Component::CurDir => {}
576 Component::ParentDir => {
577 if !simplified.pop() {
578 return true;
579 }
580 }
581 Component::Normal(os_str) => simplified.push(os_str),
582 }
583 }
584
585 false
586 }
587}
588
589impl From<&'static str> for AssetPath<'static> {
593 #[inline]
594 fn from(asset_path: &'static str) -> Self {
595 let (source, path, label) = Self::parse_internal(asset_path).unwrap();
596 AssetPath {
597 source: source.into(),
598 path: CowArc::Static(path),
599 label: label.map(CowArc::Static),
600 }
601 }
602}
603
604impl<'a> From<&'a String> for AssetPath<'a> {
605 #[inline]
606 fn from(asset_path: &'a String) -> Self {
607 AssetPath::parse(asset_path.as_str())
608 }
609}
610
611impl From<String> for AssetPath<'static> {
612 #[inline]
613 fn from(asset_path: String) -> Self {
614 AssetPath::parse(asset_path.as_str()).into_owned()
615 }
616}
617
618impl From<&'static Path> for AssetPath<'static> {
619 #[inline]
620 fn from(path: &'static Path) -> Self {
621 Self {
622 source: AssetSourceId::Default,
623 path: CowArc::Static(path),
624 label: None,
625 }
626 }
627}
628
629impl From<PathBuf> for AssetPath<'static> {
630 #[inline]
631 fn from(path: PathBuf) -> Self {
632 Self {
633 source: AssetSourceId::Default,
634 path: path.into(),
635 label: None,
636 }
637 }
638}
639
640impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
641 fn from(value: &'a AssetPath<'b>) -> Self {
642 value.clone()
643 }
644}
645
646impl<'a> From<AssetPath<'a>> for PathBuf {
647 fn from(value: AssetPath<'a>) -> Self {
648 value.path().to_path_buf()
649 }
650}
651
652impl<'a> Serialize for AssetPath<'a> {
653 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
654 where
655 S: serde::Serializer,
656 {
657 self.to_string().serialize(serializer)
658 }
659}
660
661impl<'de> Deserialize<'de> for AssetPath<'static> {
662 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
663 where
664 D: serde::Deserializer<'de>,
665 {
666 deserializer.deserialize_string(AssetPathVisitor)
667 }
668}
669
670struct AssetPathVisitor;
671
672impl<'de> Visitor<'de> for AssetPathVisitor {
673 type Value = AssetPath<'static>;
674
675 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
676 formatter.write_str("string AssetPath")
677 }
678
679 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
680 where
681 E: serde::de::Error,
682 {
683 match AssetPath::try_parse(v) {
684 Ok(path) => Ok(path.into_owned()),
685 Err(err) => Err(E::custom(err)),
686 }
687 }
688}
689
690pub(crate) fn normalize_path(path: &Path) -> PathBuf {
693 let mut result_path = PathBuf::new();
694 for elt in path.iter() {
695 if elt == "." {
696 } else if elt == ".." {
698 if result_path.file_name().is_some() {
701 assert!(result_path.pop());
704 } else {
705 result_path.push(elt);
707 }
708 } else {
709 result_path.push(elt);
710 }
711 }
712 result_path
713}
714
715#[cfg(test)]
716mod tests {
717 use crate::AssetPath;
718 use std::path::Path;
719
720 #[test]
721 fn parse_asset_path() {
722 let result = AssetPath::parse_internal("a/b.test");
723 assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
724
725 let result = AssetPath::parse_internal("http://a/b.test");
726 assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
727
728 let result = AssetPath::parse_internal("http://a/b.test#Foo");
729 assert_eq!(
730 result,
731 Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
732 );
733
734 let result = AssetPath::parse_internal("localhost:80/b.test");
735 assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
736
737 let result = AssetPath::parse_internal("http://localhost:80/b.test");
738 assert_eq!(
739 result,
740 Ok((Some("http"), Path::new("localhost:80/b.test"), None))
741 );
742
743 let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
744 assert_eq!(
745 result,
746 Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
747 );
748
749 let result = AssetPath::parse_internal("#insource://a/b.test");
750 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
751
752 let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
753 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
754
755 let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
756 assert!(
757 result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
758 || result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
759 );
760
761 let result = AssetPath::parse_internal("http://");
762 assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
763
764 let result = AssetPath::parse_internal("://x");
765 assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
766
767 let result = AssetPath::parse_internal("a/b.test#");
768 assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
769 }
770
771 #[test]
772 fn test_serialize() {
773 assert!(ron::de::from_str::<AssetPath>("\"a/b.test\"").is_ok());
774 assert!(ron::de::from_str::<AssetPath>("\"a/b.test#\"").is_err());
775 }
776
777 #[test]
778 fn test_parent() {
779 let result = AssetPath::from("a/b.test");
781 assert_eq!(result.parent(), Some(AssetPath::from("a")));
782 assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
783 assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
784
785 let result = AssetPath::from("http://a");
787 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
788 assert_eq!(result.parent().unwrap().parent(), None);
789
790 let result = AssetPath::from("http://a#Foo");
792 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
793 }
794
795 #[test]
796 fn test_with_source() {
797 let result = AssetPath::from("http://a#Foo");
798 assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
799 }
800
801 #[test]
802 fn test_without_label() {
803 let result = AssetPath::from("http://a#Foo");
804 assert_eq!(result.without_label(), AssetPath::from("http://a"));
805 }
806
807 #[test]
808 fn test_resolve_full() {
809 let base = AssetPath::from("alice/bob#carol");
811 assert_eq!(
812 base.resolve_str("/joe/next").unwrap(),
813 AssetPath::from("joe/next")
814 );
815 assert_eq!(
816 base.resolve(&AssetPath::parse("/joe/next")),
817 AssetPath::from("joe/next")
818 );
819 assert_eq!(
820 base.resolve_embed_str("/joe/next").unwrap(),
821 AssetPath::from("joe/next")
822 );
823 assert_eq!(
824 base.resolve_embed(&AssetPath::parse("/joe/next")),
825 AssetPath::from("joe/next")
826 );
827 assert_eq!(
828 base.resolve_str("/joe/next#dave").unwrap(),
829 AssetPath::from("joe/next#dave")
830 );
831 assert_eq!(
832 base.resolve(&AssetPath::parse("/joe/next#dave")),
833 AssetPath::from("joe/next#dave")
834 );
835 assert_eq!(
836 base.resolve_embed_str("/joe/next#dave").unwrap(),
837 AssetPath::from("joe/next#dave")
838 );
839 assert_eq!(
840 base.resolve_embed(&AssetPath::parse("/joe/next#dave")),
841 AssetPath::from("joe/next#dave")
842 );
843 }
844
845 #[test]
846 fn test_resolve_implicit_relative() {
847 let base = AssetPath::from("alice/bob#carol");
849 assert_eq!(
850 base.resolve_str("joe/next").unwrap(),
851 AssetPath::from("alice/bob/joe/next")
852 );
853 assert_eq!(
854 base.resolve(&AssetPath::parse("joe/next")),
855 AssetPath::from("alice/bob/joe/next")
856 );
857 assert_eq!(
858 base.resolve_embed_str("joe/next").unwrap(),
859 AssetPath::from("alice/joe/next")
860 );
861 assert_eq!(
862 base.resolve_embed(&AssetPath::parse("joe/next")),
863 AssetPath::from("alice/joe/next")
864 );
865 assert_eq!(
866 base.resolve_str("joe/next#dave").unwrap(),
867 AssetPath::from("alice/bob/joe/next#dave")
868 );
869 assert_eq!(
870 base.resolve(&AssetPath::parse("joe/next#dave")),
871 AssetPath::from("alice/bob/joe/next#dave")
872 );
873 assert_eq!(
874 base.resolve_embed_str("joe/next#dave").unwrap(),
875 AssetPath::from("alice/joe/next#dave")
876 );
877 assert_eq!(
878 base.resolve_embed(&AssetPath::parse("joe/next#dave")),
879 AssetPath::from("alice/joe/next#dave")
880 );
881 }
882
883 #[test]
884 fn test_resolve_explicit_relative() {
885 let base = AssetPath::from("alice/bob#carol");
887 assert_eq!(
888 base.resolve_str("./martin#dave").unwrap(),
889 AssetPath::from("alice/bob/martin#dave")
890 );
891 assert_eq!(
892 base.resolve(&AssetPath::parse("./martin#dave")),
893 AssetPath::from("alice/bob/martin#dave")
894 );
895 assert_eq!(
896 base.resolve_embed_str("./martin#dave").unwrap(),
897 AssetPath::from("alice/martin#dave")
898 );
899 assert_eq!(
900 base.resolve_embed(&AssetPath::parse("./martin#dave")),
901 AssetPath::from("alice/martin#dave")
902 );
903 assert_eq!(
904 base.resolve_str("../martin#dave").unwrap(),
905 AssetPath::from("alice/martin#dave")
906 );
907 assert_eq!(
908 base.resolve(&AssetPath::parse("../martin#dave")),
909 AssetPath::from("alice/martin#dave")
910 );
911 assert_eq!(
912 base.resolve_embed_str("../martin#dave").unwrap(),
913 AssetPath::from("martin#dave")
914 );
915 assert_eq!(
916 base.resolve_embed(&AssetPath::parse("../martin#dave")),
917 AssetPath::from("martin#dave")
918 );
919 }
920
921 #[test]
922 fn test_resolve_trailing_slash() {
923 let base = AssetPath::from("alice/bob/");
925 assert_eq!(
926 base.resolve_str("./martin#dave").unwrap(),
927 AssetPath::from("alice/bob/martin#dave")
928 );
929 assert_eq!(
930 base.resolve(&AssetPath::parse("./martin#dave")),
931 AssetPath::from("alice/bob/martin#dave")
932 );
933 assert_eq!(
934 base.resolve_embed_str("./martin#dave").unwrap(),
935 AssetPath::from("alice/bob/martin#dave")
936 );
937 assert_eq!(
938 base.resolve_embed(&AssetPath::parse("./martin#dave")),
939 AssetPath::from("alice/bob/martin#dave")
940 );
941 assert_eq!(
942 base.resolve_str("../martin#dave").unwrap(),
943 AssetPath::from("alice/martin#dave")
944 );
945 assert_eq!(
946 base.resolve(&AssetPath::parse("../martin#dave")),
947 AssetPath::from("alice/martin#dave")
948 );
949 assert_eq!(
950 base.resolve_embed_str("../martin#dave").unwrap(),
951 AssetPath::from("alice/martin#dave")
952 );
953 assert_eq!(
954 base.resolve_embed(&AssetPath::parse("../martin#dave")),
955 AssetPath::from("alice/martin#dave")
956 );
957 }
958
959 #[test]
960 fn test_resolve_canonicalize() {
961 let base = AssetPath::from("alice/bob#carol");
963 assert_eq!(
964 base.resolve_str("./martin/stephan/..#dave").unwrap(),
965 AssetPath::from("alice/bob/martin#dave")
966 );
967 assert_eq!(
968 base.resolve(&AssetPath::parse("./martin/stephan/..#dave")),
969 AssetPath::from("alice/bob/martin#dave")
970 );
971 assert_eq!(
972 base.resolve_embed_str("./martin/stephan/..#dave").unwrap(),
973 AssetPath::from("alice/martin#dave")
974 );
975 assert_eq!(
976 base.resolve_embed(&AssetPath::parse("./martin/stephan/..#dave")),
977 AssetPath::from("alice/martin#dave")
978 );
979 assert_eq!(
980 base.resolve_str("../martin/.#dave").unwrap(),
981 AssetPath::from("alice/martin#dave")
982 );
983 assert_eq!(
984 base.resolve(&AssetPath::parse("../martin/.#dave")),
985 AssetPath::from("alice/martin#dave")
986 );
987 assert_eq!(
988 base.resolve_embed_str("../martin/.#dave").unwrap(),
989 AssetPath::from("martin#dave")
990 );
991 assert_eq!(
992 base.resolve_embed(&AssetPath::parse("../martin/.#dave")),
993 AssetPath::from("martin#dave")
994 );
995 assert_eq!(
996 base.resolve_str("/martin/stephan/..#dave").unwrap(),
997 AssetPath::from("martin#dave")
998 );
999 assert_eq!(
1000 base.resolve(&AssetPath::parse("/martin/stephan/..#dave")),
1001 AssetPath::from("martin#dave")
1002 );
1003 assert_eq!(
1004 base.resolve_embed_str("/martin/stephan/..#dave").unwrap(),
1005 AssetPath::from("martin#dave")
1006 );
1007 assert_eq!(
1008 base.resolve_embed(&AssetPath::parse("/martin/stephan/..#dave")),
1009 AssetPath::from("martin#dave")
1010 );
1011 }
1012
1013 #[test]
1014 fn test_resolve_canonicalize_base() {
1015 let base = AssetPath::from("alice/../bob#carol");
1017 assert_eq!(
1018 base.resolve_str("./martin/stephan/..#dave").unwrap(),
1019 AssetPath::from("bob/martin#dave")
1020 );
1021 assert_eq!(
1022 base.resolve(&AssetPath::parse("./martin/stephan/..#dave")),
1023 AssetPath::from("bob/martin#dave")
1024 );
1025 assert_eq!(
1026 base.resolve_embed_str("./martin/stephan/..#dave").unwrap(),
1027 AssetPath::from("martin#dave")
1028 );
1029 assert_eq!(
1030 base.resolve_embed(&AssetPath::parse("./martin/stephan/..#dave")),
1031 AssetPath::from("martin#dave")
1032 );
1033 assert_eq!(
1034 base.resolve_str("../martin/.#dave").unwrap(),
1035 AssetPath::from("martin#dave")
1036 );
1037 assert_eq!(
1038 base.resolve(&AssetPath::parse("../martin/.#dave")),
1039 AssetPath::from("martin#dave")
1040 );
1041 assert_eq!(
1042 base.resolve_embed_str("../martin/.#dave").unwrap(),
1043 AssetPath::from("../martin#dave")
1044 );
1045 assert_eq!(
1046 base.resolve_embed(&AssetPath::parse("../martin/.#dave")),
1047 AssetPath::from("../martin#dave")
1048 );
1049 assert_eq!(
1050 base.resolve_str("/martin/stephan/..#dave").unwrap(),
1051 AssetPath::from("martin#dave")
1052 );
1053 assert_eq!(
1054 base.resolve(&AssetPath::parse("/martin/stephan/..#dave")),
1055 AssetPath::from("martin#dave")
1056 );
1057 assert_eq!(
1058 base.resolve_embed_str("/martin/stephan/..#dave").unwrap(),
1059 AssetPath::from("martin#dave")
1060 );
1061 assert_eq!(
1062 base.resolve_embed(&AssetPath::parse("/martin/stephan/..#dave")),
1063 AssetPath::from("martin#dave")
1064 );
1065 }
1066
1067 #[test]
1068 fn test_resolve_canonicalize_with_source() {
1069 let base = AssetPath::from("source://alice/bob#carol");
1071 assert_eq!(
1072 base.resolve_str("./martin/stephan/..#dave").unwrap(),
1073 AssetPath::from("source://alice/bob/martin#dave")
1074 );
1075 assert_eq!(
1076 base.resolve(&AssetPath::parse("./martin/stephan/..#dave")),
1077 AssetPath::from("source://alice/bob/martin#dave")
1078 );
1079 assert_eq!(
1080 base.resolve_embed_str("./martin/stephan/..#dave").unwrap(),
1081 AssetPath::from("source://alice/martin#dave")
1082 );
1083 assert_eq!(
1084 base.resolve_embed(&AssetPath::parse("./martin/stephan/..#dave")),
1085 AssetPath::from("source://alice/martin#dave")
1086 );
1087 assert_eq!(
1088 base.resolve_str("../martin/.#dave").unwrap(),
1089 AssetPath::from("source://alice/martin#dave")
1090 );
1091 assert_eq!(
1092 base.resolve(&AssetPath::parse("../martin/.#dave")),
1093 AssetPath::from("source://alice/martin#dave")
1094 );
1095 assert_eq!(
1096 base.resolve_embed_str("../martin/.#dave").unwrap(),
1097 AssetPath::from("source://martin#dave")
1098 );
1099 assert_eq!(
1100 base.resolve_embed(&AssetPath::parse("../martin/.#dave")),
1101 AssetPath::from("source://martin#dave")
1102 );
1103 assert_eq!(
1104 base.resolve_str("/martin/stephan/..#dave").unwrap(),
1105 AssetPath::from("source://martin#dave")
1106 );
1107 assert_eq!(
1108 base.resolve(&AssetPath::parse("/martin/stephan/..#dave")),
1109 AssetPath::from("source://martin#dave")
1110 );
1111 assert_eq!(
1112 base.resolve_embed_str("/martin/stephan/..#dave").unwrap(),
1113 AssetPath::from("source://martin#dave")
1114 );
1115 assert_eq!(
1116 base.resolve_embed(&AssetPath::parse("/martin/stephan/..#dave")),
1117 AssetPath::from("source://martin#dave")
1118 );
1119 }
1120
1121 #[test]
1122 fn test_resolve_absolute() {
1123 let base = AssetPath::from("alice/bob#carol");
1125 assert_eq!(
1126 base.resolve_str("/martin/stephan").unwrap(),
1127 AssetPath::from("martin/stephan")
1128 );
1129 assert_eq!(
1130 base.resolve(&AssetPath::parse("/martin/stephan")),
1131 AssetPath::from("martin/stephan")
1132 );
1133 assert_eq!(
1134 base.resolve_embed_str("/martin/stephan").unwrap(),
1135 AssetPath::from("martin/stephan")
1136 );
1137 assert_eq!(
1138 base.resolve_embed(&AssetPath::parse("/martin/stephan")),
1139 AssetPath::from("martin/stephan")
1140 );
1141 assert_eq!(
1142 base.resolve_str("/martin/stephan#dave").unwrap(),
1143 AssetPath::from("martin/stephan/#dave")
1144 );
1145 assert_eq!(
1146 base.resolve(&AssetPath::parse("/martin/stephan#dave")),
1147 AssetPath::from("martin/stephan/#dave")
1148 );
1149 assert_eq!(
1150 base.resolve_embed_str("/martin/stephan#dave").unwrap(),
1151 AssetPath::from("martin/stephan/#dave")
1152 );
1153 assert_eq!(
1154 base.resolve_embed(&AssetPath::parse("/martin/stephan#dave")),
1155 AssetPath::from("martin/stephan/#dave")
1156 );
1157 }
1158
1159 #[test]
1160 fn test_resolve_asset_source() {
1161 let base = AssetPath::from("alice/bob#carol");
1163 assert_eq!(
1164 base.resolve_str("source://martin/stephan").unwrap(),
1165 AssetPath::from("source://martin/stephan")
1166 );
1167 assert_eq!(
1168 base.resolve(&AssetPath::parse("source://martin/stephan")),
1169 AssetPath::from("source://martin/stephan")
1170 );
1171 assert_eq!(
1172 base.resolve_embed_str("source://martin/stephan").unwrap(),
1173 AssetPath::from("source://martin/stephan")
1174 );
1175 assert_eq!(
1176 base.resolve_embed(&AssetPath::parse("source://martin/stephan")),
1177 AssetPath::from("source://martin/stephan")
1178 );
1179 assert_eq!(
1180 base.resolve_str("source://martin/stephan#dave").unwrap(),
1181 AssetPath::from("source://martin/stephan/#dave")
1182 );
1183 assert_eq!(
1184 base.resolve(&AssetPath::parse("source://martin/stephan#dave")),
1185 AssetPath::from("source://martin/stephan/#dave")
1186 );
1187 assert_eq!(
1188 base.resolve_embed_str("source://martin/stephan#dave")
1189 .unwrap(),
1190 AssetPath::from("source://martin/stephan/#dave")
1191 );
1192 assert_eq!(
1193 base.resolve_embed(&AssetPath::parse("source://martin/stephan#dave")),
1194 AssetPath::from("source://martin/stephan/#dave")
1195 );
1196 }
1197
1198 #[test]
1199 fn test_resolve_label() {
1200 let base = AssetPath::from("alice/bob#carol");
1202 assert_eq!(
1203 base.resolve_str("#dave").unwrap(),
1204 AssetPath::from("alice/bob#dave")
1205 );
1206 assert_eq!(
1207 base.resolve(&AssetPath::parse("#dave")),
1208 AssetPath::from("alice/bob#dave")
1209 );
1210 assert_eq!(
1211 base.resolve_embed_str("#dave").unwrap(),
1212 AssetPath::from("alice/bob#dave")
1213 );
1214 assert_eq!(
1215 base.resolve_embed(&AssetPath::parse("#dave")),
1216 AssetPath::from("alice/bob#dave")
1217 );
1218 }
1219
1220 #[test]
1221 fn test_resolve_insufficient_elements() {
1222 let base = AssetPath::from("alice/bob#carol");
1224 assert_eq!(
1225 base.resolve_str("../../joe/next").unwrap(),
1226 AssetPath::from("joe/next")
1227 );
1228 assert_eq!(
1229 base.resolve(&AssetPath::parse("../../joe/next")),
1230 AssetPath::from("joe/next")
1231 );
1232 assert_eq!(
1233 base.resolve_embed_str("../../joe/next").unwrap(),
1234 AssetPath::from("../joe/next")
1235 );
1236 assert_eq!(
1237 base.resolve_embed(&AssetPath::parse("../../joe/next")),
1238 AssetPath::from("../joe/next")
1239 );
1240 }
1241
1242 #[test]
1243 fn resolve_embed_relative_to_external_path() {
1244 let base = AssetPath::from("../../a/b.gltf");
1245 assert_eq!(
1246 base.resolve_embed_str("c.bin").unwrap(),
1247 AssetPath::from("../../a/c.bin")
1248 );
1249 assert_eq!(
1250 base.resolve_embed(&AssetPath::parse("c.bin")),
1251 AssetPath::from("../../a/c.bin")
1252 );
1253 }
1254
1255 #[test]
1256 fn resolve_relative_to_external_path() {
1257 let base = AssetPath::from("../../a/b.gltf");
1258 assert_eq!(
1259 base.resolve_str("c.bin").unwrap(),
1260 AssetPath::from("../../a/b.gltf/c.bin")
1261 );
1262 assert_eq!(
1263 base.resolve(&AssetPath::parse("c.bin")),
1264 AssetPath::from("../../a/b.gltf/c.bin")
1265 );
1266 }
1267
1268 #[test]
1269 fn test_get_full_extension() {
1270 let result = AssetPath::from("http://a.tar.gz#Foo");
1271 assert_eq!(result.get_full_extension(), Some("tar.gz"));
1272
1273 let result = AssetPath::from("http://a#Foo");
1274 assert_eq!(result.get_full_extension(), None);
1275
1276 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1277 assert_eq!(result.get_full_extension(), Some("tar.bz2"));
1278
1279 let result = AssetPath::from("asset.Custom");
1280 assert_eq!(result.get_full_extension(), Some("Custom"));
1281 }
1282
1283 #[test]
1284 fn test_get_extension() {
1285 let result = AssetPath::from("http://a.tar.gz#Foo");
1286 assert_eq!(result.get_extension(), Some("gz"));
1287
1288 let result = AssetPath::from("http://a#Foo");
1289 assert_eq!(result.get_extension(), None);
1290
1291 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1292 assert_eq!(result.get_extension(), Some("bz2"));
1293
1294 let result = AssetPath::from("asset.Custom");
1295 assert_eq!(result.get_extension(), Some("Custom"));
1296 }
1297}