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: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
390 self.resolve_internal(path, false)
391 }
392
393 pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
416 self.resolve_internal(path, true)
417 }
418
419 fn resolve_internal(
420 &self,
421 path: &str,
422 replace: bool,
423 ) -> Result<AssetPath<'static>, ParseAssetPathError> {
424 if let Some(label) = path.strip_prefix('#') {
425 Ok(self.clone_owned().with_label(label.to_owned()))
427 } else {
428 let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
429 let mut base_path = PathBuf::from(self.path());
430 if replace && !self.path.to_str().unwrap().ends_with('/') {
431 base_path.pop();
433 }
434
435 let mut is_absolute = false;
437 let rpath = match rpath.strip_prefix("/") {
438 Ok(p) => {
439 is_absolute = true;
440 p
441 }
442 _ => rpath,
443 };
444
445 let mut result_path = if !is_absolute && source.is_none() {
446 base_path
447 } else {
448 PathBuf::new()
449 };
450 result_path.push(rpath);
451 result_path = normalize_path(result_path.as_path());
452
453 Ok(AssetPath {
454 source: match source {
455 Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
456 None => self.source.clone_owned(),
457 },
458 path: CowArc::Owned(result_path.into()),
459 label: rlabel.map(|l| CowArc::Owned(l.into())),
460 })
461 }
462 }
463
464 pub fn get_full_extension(&self) -> Option<String> {
469 let file_name = self.path().file_name()?.to_str()?;
470 let index = file_name.find('.')?;
471 let mut extension = file_name[index + 1..].to_owned();
472
473 let query = extension.find('?');
475 if let Some(offset) = query {
476 extension.truncate(offset);
477 }
478
479 Some(extension)
480 }
481
482 pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
483 full_extension.char_indices().filter_map(|(i, c)| {
484 if c == '.' {
485 Some(&full_extension[i + 1..])
486 } else {
487 None
488 }
489 })
490 }
491
492 pub fn is_unapproved(&self) -> bool {
519 use std::path::Component;
520 let mut simplified = PathBuf::new();
521 for component in self.path.components() {
522 match component {
523 Component::Prefix(_) | Component::RootDir => return true,
524 Component::CurDir => {}
525 Component::ParentDir => {
526 if !simplified.pop() {
527 return true;
528 }
529 }
530 Component::Normal(os_str) => simplified.push(os_str),
531 }
532 }
533
534 false
535 }
536}
537
538impl From<&'static str> for AssetPath<'static> {
542 #[inline]
543 fn from(asset_path: &'static str) -> Self {
544 let (source, path, label) = Self::parse_internal(asset_path).unwrap();
545 AssetPath {
546 source: source.into(),
547 path: CowArc::Static(path),
548 label: label.map(CowArc::Static),
549 }
550 }
551}
552
553impl<'a> From<&'a String> for AssetPath<'a> {
554 #[inline]
555 fn from(asset_path: &'a String) -> Self {
556 AssetPath::parse(asset_path.as_str())
557 }
558}
559
560impl From<String> for AssetPath<'static> {
561 #[inline]
562 fn from(asset_path: String) -> Self {
563 AssetPath::parse(asset_path.as_str()).into_owned()
564 }
565}
566
567impl From<&'static Path> for AssetPath<'static> {
568 #[inline]
569 fn from(path: &'static Path) -> Self {
570 Self {
571 source: AssetSourceId::Default,
572 path: CowArc::Static(path),
573 label: None,
574 }
575 }
576}
577
578impl From<PathBuf> for AssetPath<'static> {
579 #[inline]
580 fn from(path: PathBuf) -> Self {
581 Self {
582 source: AssetSourceId::Default,
583 path: path.into(),
584 label: None,
585 }
586 }
587}
588
589impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
590 fn from(value: &'a AssetPath<'b>) -> Self {
591 value.clone()
592 }
593}
594
595impl<'a> From<AssetPath<'a>> for PathBuf {
596 fn from(value: AssetPath<'a>) -> Self {
597 value.path().to_path_buf()
598 }
599}
600
601impl<'a> Serialize for AssetPath<'a> {
602 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
603 where
604 S: serde::Serializer,
605 {
606 self.to_string().serialize(serializer)
607 }
608}
609
610impl<'de> Deserialize<'de> for AssetPath<'static> {
611 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
612 where
613 D: serde::Deserializer<'de>,
614 {
615 deserializer.deserialize_string(AssetPathVisitor)
616 }
617}
618
619struct AssetPathVisitor;
620
621impl<'de> Visitor<'de> for AssetPathVisitor {
622 type Value = AssetPath<'static>;
623
624 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
625 formatter.write_str("string AssetPath")
626 }
627
628 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
629 where
630 E: serde::de::Error,
631 {
632 Ok(AssetPath::parse(v).into_owned())
633 }
634
635 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
636 where
637 E: serde::de::Error,
638 {
639 Ok(AssetPath::from(v))
640 }
641}
642
643pub(crate) fn normalize_path(path: &Path) -> PathBuf {
646 let mut result_path = PathBuf::new();
647 for elt in path.iter() {
648 if elt == "." {
649 } else if elt == ".." {
651 if !result_path.pop() {
652 result_path.push(elt);
654 }
655 } else {
656 result_path.push(elt);
657 }
658 }
659 result_path
660}
661
662#[cfg(test)]
663mod tests {
664 use crate::AssetPath;
665 use alloc::string::ToString;
666 use std::path::Path;
667
668 #[test]
669 fn parse_asset_path() {
670 let result = AssetPath::parse_internal("a/b.test");
671 assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
672
673 let result = AssetPath::parse_internal("http://a/b.test");
674 assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
675
676 let result = AssetPath::parse_internal("http://a/b.test#Foo");
677 assert_eq!(
678 result,
679 Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
680 );
681
682 let result = AssetPath::parse_internal("localhost:80/b.test");
683 assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
684
685 let result = AssetPath::parse_internal("http://localhost:80/b.test");
686 assert_eq!(
687 result,
688 Ok((Some("http"), Path::new("localhost:80/b.test"), None))
689 );
690
691 let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
692 assert_eq!(
693 result,
694 Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
695 );
696
697 let result = AssetPath::parse_internal("#insource://a/b.test");
698 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
699
700 let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
701 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
702
703 let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
704 assert!(
705 result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
706 || result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
707 );
708
709 let result = AssetPath::parse_internal("http://");
710 assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
711
712 let result = AssetPath::parse_internal("://x");
713 assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
714
715 let result = AssetPath::parse_internal("a/b.test#");
716 assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
717 }
718
719 #[test]
720 fn test_parent() {
721 let result = AssetPath::from("a/b.test");
723 assert_eq!(result.parent(), Some(AssetPath::from("a")));
724 assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
725 assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
726
727 let result = AssetPath::from("http://a");
729 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
730 assert_eq!(result.parent().unwrap().parent(), None);
731
732 let result = AssetPath::from("http://a#Foo");
734 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
735 }
736
737 #[test]
738 fn test_with_source() {
739 let result = AssetPath::from("http://a#Foo");
740 assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
741 }
742
743 #[test]
744 fn test_without_label() {
745 let result = AssetPath::from("http://a#Foo");
746 assert_eq!(result.without_label(), AssetPath::from("http://a"));
747 }
748
749 #[test]
750 fn test_resolve_full() {
751 let base = AssetPath::from("alice/bob#carol");
753 assert_eq!(
754 base.resolve("/joe/next").unwrap(),
755 AssetPath::from("joe/next")
756 );
757 assert_eq!(
758 base.resolve_embed("/joe/next").unwrap(),
759 AssetPath::from("joe/next")
760 );
761 assert_eq!(
762 base.resolve("/joe/next#dave").unwrap(),
763 AssetPath::from("joe/next#dave")
764 );
765 assert_eq!(
766 base.resolve_embed("/joe/next#dave").unwrap(),
767 AssetPath::from("joe/next#dave")
768 );
769 }
770
771 #[test]
772 fn test_resolve_implicit_relative() {
773 let base = AssetPath::from("alice/bob#carol");
775 assert_eq!(
776 base.resolve("joe/next").unwrap(),
777 AssetPath::from("alice/bob/joe/next")
778 );
779 assert_eq!(
780 base.resolve_embed("joe/next").unwrap(),
781 AssetPath::from("alice/joe/next")
782 );
783 assert_eq!(
784 base.resolve("joe/next#dave").unwrap(),
785 AssetPath::from("alice/bob/joe/next#dave")
786 );
787 assert_eq!(
788 base.resolve_embed("joe/next#dave").unwrap(),
789 AssetPath::from("alice/joe/next#dave")
790 );
791 }
792
793 #[test]
794 fn test_resolve_explicit_relative() {
795 let base = AssetPath::from("alice/bob#carol");
797 assert_eq!(
798 base.resolve("./martin#dave").unwrap(),
799 AssetPath::from("alice/bob/martin#dave")
800 );
801 assert_eq!(
802 base.resolve_embed("./martin#dave").unwrap(),
803 AssetPath::from("alice/martin#dave")
804 );
805 assert_eq!(
806 base.resolve("../martin#dave").unwrap(),
807 AssetPath::from("alice/martin#dave")
808 );
809 assert_eq!(
810 base.resolve_embed("../martin#dave").unwrap(),
811 AssetPath::from("martin#dave")
812 );
813 }
814
815 #[test]
816 fn test_resolve_trailing_slash() {
817 let base = AssetPath::from("alice/bob/");
819 assert_eq!(
820 base.resolve("./martin#dave").unwrap(),
821 AssetPath::from("alice/bob/martin#dave")
822 );
823 assert_eq!(
824 base.resolve_embed("./martin#dave").unwrap(),
825 AssetPath::from("alice/bob/martin#dave")
826 );
827 assert_eq!(
828 base.resolve("../martin#dave").unwrap(),
829 AssetPath::from("alice/martin#dave")
830 );
831 assert_eq!(
832 base.resolve_embed("../martin#dave").unwrap(),
833 AssetPath::from("alice/martin#dave")
834 );
835 }
836
837 #[test]
838 fn test_resolve_canonicalize() {
839 let base = AssetPath::from("alice/bob#carol");
841 assert_eq!(
842 base.resolve("./martin/stephan/..#dave").unwrap(),
843 AssetPath::from("alice/bob/martin#dave")
844 );
845 assert_eq!(
846 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
847 AssetPath::from("alice/martin#dave")
848 );
849 assert_eq!(
850 base.resolve("../martin/.#dave").unwrap(),
851 AssetPath::from("alice/martin#dave")
852 );
853 assert_eq!(
854 base.resolve_embed("../martin/.#dave").unwrap(),
855 AssetPath::from("martin#dave")
856 );
857 assert_eq!(
858 base.resolve("/martin/stephan/..#dave").unwrap(),
859 AssetPath::from("martin#dave")
860 );
861 assert_eq!(
862 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
863 AssetPath::from("martin#dave")
864 );
865 }
866
867 #[test]
868 fn test_resolve_canonicalize_base() {
869 let base = AssetPath::from("alice/../bob#carol");
871 assert_eq!(
872 base.resolve("./martin/stephan/..#dave").unwrap(),
873 AssetPath::from("bob/martin#dave")
874 );
875 assert_eq!(
876 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
877 AssetPath::from("martin#dave")
878 );
879 assert_eq!(
880 base.resolve("../martin/.#dave").unwrap(),
881 AssetPath::from("martin#dave")
882 );
883 assert_eq!(
884 base.resolve_embed("../martin/.#dave").unwrap(),
885 AssetPath::from("../martin#dave")
886 );
887 assert_eq!(
888 base.resolve("/martin/stephan/..#dave").unwrap(),
889 AssetPath::from("martin#dave")
890 );
891 assert_eq!(
892 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
893 AssetPath::from("martin#dave")
894 );
895 }
896
897 #[test]
898 fn test_resolve_canonicalize_with_source() {
899 let base = AssetPath::from("source://alice/bob#carol");
901 assert_eq!(
902 base.resolve("./martin/stephan/..#dave").unwrap(),
903 AssetPath::from("source://alice/bob/martin#dave")
904 );
905 assert_eq!(
906 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
907 AssetPath::from("source://alice/martin#dave")
908 );
909 assert_eq!(
910 base.resolve("../martin/.#dave").unwrap(),
911 AssetPath::from("source://alice/martin#dave")
912 );
913 assert_eq!(
914 base.resolve_embed("../martin/.#dave").unwrap(),
915 AssetPath::from("source://martin#dave")
916 );
917 assert_eq!(
918 base.resolve("/martin/stephan/..#dave").unwrap(),
919 AssetPath::from("source://martin#dave")
920 );
921 assert_eq!(
922 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
923 AssetPath::from("source://martin#dave")
924 );
925 }
926
927 #[test]
928 fn test_resolve_absolute() {
929 let base = AssetPath::from("alice/bob#carol");
931 assert_eq!(
932 base.resolve("/martin/stephan").unwrap(),
933 AssetPath::from("martin/stephan")
934 );
935 assert_eq!(
936 base.resolve_embed("/martin/stephan").unwrap(),
937 AssetPath::from("martin/stephan")
938 );
939 assert_eq!(
940 base.resolve("/martin/stephan#dave").unwrap(),
941 AssetPath::from("martin/stephan/#dave")
942 );
943 assert_eq!(
944 base.resolve_embed("/martin/stephan#dave").unwrap(),
945 AssetPath::from("martin/stephan/#dave")
946 );
947 }
948
949 #[test]
950 fn test_resolve_asset_source() {
951 let base = AssetPath::from("alice/bob#carol");
953 assert_eq!(
954 base.resolve("source://martin/stephan").unwrap(),
955 AssetPath::from("source://martin/stephan")
956 );
957 assert_eq!(
958 base.resolve_embed("source://martin/stephan").unwrap(),
959 AssetPath::from("source://martin/stephan")
960 );
961 assert_eq!(
962 base.resolve("source://martin/stephan#dave").unwrap(),
963 AssetPath::from("source://martin/stephan/#dave")
964 );
965 assert_eq!(
966 base.resolve_embed("source://martin/stephan#dave").unwrap(),
967 AssetPath::from("source://martin/stephan/#dave")
968 );
969 }
970
971 #[test]
972 fn test_resolve_label() {
973 let base = AssetPath::from("alice/bob#carol");
975 assert_eq!(
976 base.resolve("#dave").unwrap(),
977 AssetPath::from("alice/bob#dave")
978 );
979 assert_eq!(
980 base.resolve_embed("#dave").unwrap(),
981 AssetPath::from("alice/bob#dave")
982 );
983 }
984
985 #[test]
986 fn test_resolve_insufficient_elements() {
987 let base = AssetPath::from("alice/bob#carol");
989 assert_eq!(
990 base.resolve("../../joe/next").unwrap(),
991 AssetPath::from("joe/next")
992 );
993 assert_eq!(
994 base.resolve_embed("../../joe/next").unwrap(),
995 AssetPath::from("../joe/next")
996 );
997 }
998
999 #[test]
1000 fn test_get_extension() {
1001 let result = AssetPath::from("http://a.tar.gz#Foo");
1002 assert_eq!(result.get_full_extension(), Some("tar.gz".to_string()));
1003
1004 let result = AssetPath::from("http://a#Foo");
1005 assert_eq!(result.get_full_extension(), None);
1006
1007 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1008 assert_eq!(result.get_full_extension(), Some("tar.bz2".to_string()));
1009
1010 let result = AssetPath::from("asset.Custom");
1011 assert_eq!(result.get_full_extension(), Some("Custom".to_string()));
1012 }
1013}