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(path: &'a Path) -> AssetPath<'a> {
229 AssetPath {
230 path: CowArc::Borrowed(path),
231 source: AssetSourceId::Default,
232 label: None,
233 }
234 }
235
236 #[inline]
239 pub fn source(&self) -> &AssetSourceId {
240 &self.source
241 }
242
243 #[inline]
245 pub fn label(&self) -> Option<&str> {
246 self.label.as_deref()
247 }
248
249 #[inline]
251 pub fn label_cow(&self) -> Option<CowArc<'a, str>> {
252 self.label.clone()
253 }
254
255 #[inline]
257 pub fn path(&self) -> &Path {
258 self.path.deref()
259 }
260
261 #[inline]
263 pub fn without_label(&self) -> AssetPath<'_> {
264 Self {
265 source: self.source.clone(),
266 path: self.path.clone(),
267 label: None,
268 }
269 }
270
271 #[inline]
273 pub fn remove_label(&mut self) {
274 self.label = None;
275 }
276
277 #[inline]
279 pub fn take_label(&mut self) -> Option<CowArc<'a, str>> {
280 self.label.take()
281 }
282
283 #[inline]
286 pub fn with_label(self, label: impl Into<CowArc<'a, str>>) -> AssetPath<'a> {
287 AssetPath {
288 source: self.source,
289 path: self.path,
290 label: Some(label.into()),
291 }
292 }
293
294 #[inline]
297 pub fn with_source(self, source: impl Into<AssetSourceId<'a>>) -> AssetPath<'a> {
298 AssetPath {
299 source: source.into(),
300 path: self.path,
301 label: self.label,
302 }
303 }
304
305 pub fn parent(&self) -> Option<AssetPath<'a>> {
307 let path = match &self.path {
308 CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?),
309 CowArc::Static(path) => CowArc::Static(path.parent()?),
310 CowArc::Owned(path) => path.parent()?.to_path_buf().into(),
311 };
312 Some(AssetPath {
313 source: self.source.clone(),
314 label: None,
315 path,
316 })
317 }
318
319 pub fn into_owned(self) -> AssetPath<'static> {
325 AssetPath {
326 source: self.source.into_owned(),
327 path: self.path.into_owned(),
328 label: self.label.map(CowArc::into_owned),
329 }
330 }
331
332 #[inline]
338 pub fn clone_owned(&self) -> AssetPath<'static> {
339 self.clone().into_owned()
340 }
341
342 pub fn resolve(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
380 self.resolve_internal(path, false)
381 }
382
383 pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
406 self.resolve_internal(path, true)
407 }
408
409 fn resolve_internal(
410 &self,
411 path: &str,
412 replace: bool,
413 ) -> Result<AssetPath<'static>, ParseAssetPathError> {
414 if let Some(label) = path.strip_prefix('#') {
415 Ok(self.clone_owned().with_label(label.to_owned()))
417 } else {
418 let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
419 let mut base_path = PathBuf::from(self.path());
420 if replace && !self.path.to_str().unwrap().ends_with('/') {
421 base_path.pop();
423 }
424
425 let mut is_absolute = false;
427 let rpath = match rpath.strip_prefix("/") {
428 Ok(p) => {
429 is_absolute = true;
430 p
431 }
432 _ => rpath,
433 };
434
435 let mut result_path = if !is_absolute && source.is_none() {
436 base_path
437 } else {
438 PathBuf::new()
439 };
440 result_path.push(rpath);
441 result_path = normalize_path(result_path.as_path());
442
443 Ok(AssetPath {
444 source: match source {
445 Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
446 None => self.source.clone_owned(),
447 },
448 path: CowArc::Owned(result_path.into()),
449 label: rlabel.map(|l| CowArc::Owned(l.into())),
450 })
451 }
452 }
453
454 pub fn get_full_extension(&self) -> Option<String> {
459 let file_name = self.path().file_name()?.to_str()?;
460 let index = file_name.find('.')?;
461 let mut extension = file_name[index + 1..].to_owned();
462
463 let query = extension.find('?');
465 if let Some(offset) = query {
466 extension.truncate(offset);
467 }
468
469 Some(extension)
470 }
471
472 pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
473 full_extension.chars().enumerate().filter_map(|(i, c)| {
474 if c == '.' {
475 Some(&full_extension[i + 1..])
476 } else {
477 None
478 }
479 })
480 }
481
482 pub fn is_unapproved(&self) -> bool {
509 use std::path::Component;
510 let mut simplified = PathBuf::new();
511 for component in self.path.components() {
512 match component {
513 Component::Prefix(_) | Component::RootDir => return true,
514 Component::CurDir => {}
515 Component::ParentDir => {
516 if !simplified.pop() {
517 return true;
518 }
519 }
520 Component::Normal(os_str) => simplified.push(os_str),
521 }
522 }
523
524 false
525 }
526}
527
528impl AssetPath<'static> {
529 #[inline]
531 pub fn as_static(self) -> Self {
532 let Self {
533 source,
534 path,
535 label,
536 } = self;
537
538 let source = source.as_static();
539 let path = path.as_static();
540 let label = label.map(CowArc::as_static);
541
542 Self {
543 source,
544 path,
545 label,
546 }
547 }
548
549 #[inline]
551 pub fn from_static(value: impl Into<Self>) -> Self {
552 value.into().as_static()
553 }
554}
555
556impl<'a> From<&'a str> for AssetPath<'a> {
557 #[inline]
558 fn from(asset_path: &'a str) -> Self {
559 let (source, path, label) = Self::parse_internal(asset_path).unwrap();
560
561 AssetPath {
562 source: source.into(),
563 path: CowArc::Borrowed(path),
564 label: label.map(CowArc::Borrowed),
565 }
566 }
567}
568
569impl<'a> From<&'a String> for AssetPath<'a> {
570 #[inline]
571 fn from(asset_path: &'a String) -> Self {
572 AssetPath::parse(asset_path.as_str())
573 }
574}
575
576impl From<String> for AssetPath<'static> {
577 #[inline]
578 fn from(asset_path: String) -> Self {
579 AssetPath::parse(asset_path.as_str()).into_owned()
580 }
581}
582
583impl<'a> From<&'a Path> for AssetPath<'a> {
584 #[inline]
585 fn from(path: &'a Path) -> Self {
586 Self {
587 source: AssetSourceId::Default,
588 path: CowArc::Borrowed(path),
589 label: None,
590 }
591 }
592}
593
594impl From<PathBuf> for AssetPath<'static> {
595 #[inline]
596 fn from(path: PathBuf) -> Self {
597 Self {
598 source: AssetSourceId::Default,
599 path: path.into(),
600 label: None,
601 }
602 }
603}
604
605impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
606 fn from(value: &'a AssetPath<'b>) -> Self {
607 value.clone()
608 }
609}
610
611impl<'a> From<AssetPath<'a>> for PathBuf {
612 fn from(value: AssetPath<'a>) -> Self {
613 value.path().to_path_buf()
614 }
615}
616
617impl<'a> Serialize for AssetPath<'a> {
618 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
619 where
620 S: serde::Serializer,
621 {
622 self.to_string().serialize(serializer)
623 }
624}
625
626impl<'de> Deserialize<'de> for AssetPath<'static> {
627 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
628 where
629 D: serde::Deserializer<'de>,
630 {
631 deserializer.deserialize_string(AssetPathVisitor)
632 }
633}
634
635struct AssetPathVisitor;
636
637impl<'de> Visitor<'de> for AssetPathVisitor {
638 type Value = AssetPath<'static>;
639
640 fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
641 formatter.write_str("string AssetPath")
642 }
643
644 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
645 where
646 E: serde::de::Error,
647 {
648 Ok(AssetPath::parse(v).into_owned())
649 }
650
651 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
652 where
653 E: serde::de::Error,
654 {
655 Ok(AssetPath::from(v))
656 }
657}
658
659pub(crate) fn normalize_path(path: &Path) -> PathBuf {
662 let mut result_path = PathBuf::new();
663 for elt in path.iter() {
664 if elt == "." {
665 } else if elt == ".." {
667 if !result_path.pop() {
668 result_path.push(elt);
670 }
671 } else {
672 result_path.push(elt);
673 }
674 }
675 result_path
676}
677
678#[cfg(test)]
679mod tests {
680 use crate::AssetPath;
681 use alloc::string::ToString;
682 use std::path::Path;
683
684 #[test]
685 fn parse_asset_path() {
686 let result = AssetPath::parse_internal("a/b.test");
687 assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
688
689 let result = AssetPath::parse_internal("http://a/b.test");
690 assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
691
692 let result = AssetPath::parse_internal("http://a/b.test#Foo");
693 assert_eq!(
694 result,
695 Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
696 );
697
698 let result = AssetPath::parse_internal("localhost:80/b.test");
699 assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
700
701 let result = AssetPath::parse_internal("http://localhost:80/b.test");
702 assert_eq!(
703 result,
704 Ok((Some("http"), Path::new("localhost:80/b.test"), None))
705 );
706
707 let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
708 assert_eq!(
709 result,
710 Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
711 );
712
713 let result = AssetPath::parse_internal("#insource://a/b.test");
714 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
715
716 let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
717 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
718
719 let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
720 assert!(
721 result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
722 || result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
723 );
724
725 let result = AssetPath::parse_internal("http://");
726 assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
727
728 let result = AssetPath::parse_internal("://x");
729 assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
730
731 let result = AssetPath::parse_internal("a/b.test#");
732 assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
733 }
734
735 #[test]
736 fn test_parent() {
737 let result = AssetPath::from("a/b.test");
739 assert_eq!(result.parent(), Some(AssetPath::from("a")));
740 assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
741 assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
742
743 let result = AssetPath::from("http://a");
745 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
746 assert_eq!(result.parent().unwrap().parent(), None);
747
748 let result = AssetPath::from("http://a#Foo");
750 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
751 }
752
753 #[test]
754 fn test_with_source() {
755 let result = AssetPath::from("http://a#Foo");
756 assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
757 }
758
759 #[test]
760 fn test_without_label() {
761 let result = AssetPath::from("http://a#Foo");
762 assert_eq!(result.without_label(), AssetPath::from("http://a"));
763 }
764
765 #[test]
766 fn test_resolve_full() {
767 let base = AssetPath::from("alice/bob#carol");
769 assert_eq!(
770 base.resolve("/joe/next").unwrap(),
771 AssetPath::from("joe/next")
772 );
773 assert_eq!(
774 base.resolve_embed("/joe/next").unwrap(),
775 AssetPath::from("joe/next")
776 );
777 assert_eq!(
778 base.resolve("/joe/next#dave").unwrap(),
779 AssetPath::from("joe/next#dave")
780 );
781 assert_eq!(
782 base.resolve_embed("/joe/next#dave").unwrap(),
783 AssetPath::from("joe/next#dave")
784 );
785 }
786
787 #[test]
788 fn test_resolve_implicit_relative() {
789 let base = AssetPath::from("alice/bob#carol");
791 assert_eq!(
792 base.resolve("joe/next").unwrap(),
793 AssetPath::from("alice/bob/joe/next")
794 );
795 assert_eq!(
796 base.resolve_embed("joe/next").unwrap(),
797 AssetPath::from("alice/joe/next")
798 );
799 assert_eq!(
800 base.resolve("joe/next#dave").unwrap(),
801 AssetPath::from("alice/bob/joe/next#dave")
802 );
803 assert_eq!(
804 base.resolve_embed("joe/next#dave").unwrap(),
805 AssetPath::from("alice/joe/next#dave")
806 );
807 }
808
809 #[test]
810 fn test_resolve_explicit_relative() {
811 let base = AssetPath::from("alice/bob#carol");
813 assert_eq!(
814 base.resolve("./martin#dave").unwrap(),
815 AssetPath::from("alice/bob/martin#dave")
816 );
817 assert_eq!(
818 base.resolve_embed("./martin#dave").unwrap(),
819 AssetPath::from("alice/martin#dave")
820 );
821 assert_eq!(
822 base.resolve("../martin#dave").unwrap(),
823 AssetPath::from("alice/martin#dave")
824 );
825 assert_eq!(
826 base.resolve_embed("../martin#dave").unwrap(),
827 AssetPath::from("martin#dave")
828 );
829 }
830
831 #[test]
832 fn test_resolve_trailing_slash() {
833 let base = AssetPath::from("alice/bob/");
835 assert_eq!(
836 base.resolve("./martin#dave").unwrap(),
837 AssetPath::from("alice/bob/martin#dave")
838 );
839 assert_eq!(
840 base.resolve_embed("./martin#dave").unwrap(),
841 AssetPath::from("alice/bob/martin#dave")
842 );
843 assert_eq!(
844 base.resolve("../martin#dave").unwrap(),
845 AssetPath::from("alice/martin#dave")
846 );
847 assert_eq!(
848 base.resolve_embed("../martin#dave").unwrap(),
849 AssetPath::from("alice/martin#dave")
850 );
851 }
852
853 #[test]
854 fn test_resolve_canonicalize() {
855 let base = AssetPath::from("alice/bob#carol");
857 assert_eq!(
858 base.resolve("./martin/stephan/..#dave").unwrap(),
859 AssetPath::from("alice/bob/martin#dave")
860 );
861 assert_eq!(
862 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
863 AssetPath::from("alice/martin#dave")
864 );
865 assert_eq!(
866 base.resolve("../martin/.#dave").unwrap(),
867 AssetPath::from("alice/martin#dave")
868 );
869 assert_eq!(
870 base.resolve_embed("../martin/.#dave").unwrap(),
871 AssetPath::from("martin#dave")
872 );
873 assert_eq!(
874 base.resolve("/martin/stephan/..#dave").unwrap(),
875 AssetPath::from("martin#dave")
876 );
877 assert_eq!(
878 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
879 AssetPath::from("martin#dave")
880 );
881 }
882
883 #[test]
884 fn test_resolve_canonicalize_base() {
885 let base = AssetPath::from("alice/../bob#carol");
887 assert_eq!(
888 base.resolve("./martin/stephan/..#dave").unwrap(),
889 AssetPath::from("bob/martin#dave")
890 );
891 assert_eq!(
892 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
893 AssetPath::from("martin#dave")
894 );
895 assert_eq!(
896 base.resolve("../martin/.#dave").unwrap(),
897 AssetPath::from("martin#dave")
898 );
899 assert_eq!(
900 base.resolve_embed("../martin/.#dave").unwrap(),
901 AssetPath::from("../martin#dave")
902 );
903 assert_eq!(
904 base.resolve("/martin/stephan/..#dave").unwrap(),
905 AssetPath::from("martin#dave")
906 );
907 assert_eq!(
908 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
909 AssetPath::from("martin#dave")
910 );
911 }
912
913 #[test]
914 fn test_resolve_canonicalize_with_source() {
915 let base = AssetPath::from("source://alice/bob#carol");
917 assert_eq!(
918 base.resolve("./martin/stephan/..#dave").unwrap(),
919 AssetPath::from("source://alice/bob/martin#dave")
920 );
921 assert_eq!(
922 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
923 AssetPath::from("source://alice/martin#dave")
924 );
925 assert_eq!(
926 base.resolve("../martin/.#dave").unwrap(),
927 AssetPath::from("source://alice/martin#dave")
928 );
929 assert_eq!(
930 base.resolve_embed("../martin/.#dave").unwrap(),
931 AssetPath::from("source://martin#dave")
932 );
933 assert_eq!(
934 base.resolve("/martin/stephan/..#dave").unwrap(),
935 AssetPath::from("source://martin#dave")
936 );
937 assert_eq!(
938 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
939 AssetPath::from("source://martin#dave")
940 );
941 }
942
943 #[test]
944 fn test_resolve_absolute() {
945 let base = AssetPath::from("alice/bob#carol");
947 assert_eq!(
948 base.resolve("/martin/stephan").unwrap(),
949 AssetPath::from("martin/stephan")
950 );
951 assert_eq!(
952 base.resolve_embed("/martin/stephan").unwrap(),
953 AssetPath::from("martin/stephan")
954 );
955 assert_eq!(
956 base.resolve("/martin/stephan#dave").unwrap(),
957 AssetPath::from("martin/stephan/#dave")
958 );
959 assert_eq!(
960 base.resolve_embed("/martin/stephan#dave").unwrap(),
961 AssetPath::from("martin/stephan/#dave")
962 );
963 }
964
965 #[test]
966 fn test_resolve_asset_source() {
967 let base = AssetPath::from("alice/bob#carol");
969 assert_eq!(
970 base.resolve("source://martin/stephan").unwrap(),
971 AssetPath::from("source://martin/stephan")
972 );
973 assert_eq!(
974 base.resolve_embed("source://martin/stephan").unwrap(),
975 AssetPath::from("source://martin/stephan")
976 );
977 assert_eq!(
978 base.resolve("source://martin/stephan#dave").unwrap(),
979 AssetPath::from("source://martin/stephan/#dave")
980 );
981 assert_eq!(
982 base.resolve_embed("source://martin/stephan#dave").unwrap(),
983 AssetPath::from("source://martin/stephan/#dave")
984 );
985 }
986
987 #[test]
988 fn test_resolve_label() {
989 let base = AssetPath::from("alice/bob#carol");
991 assert_eq!(
992 base.resolve("#dave").unwrap(),
993 AssetPath::from("alice/bob#dave")
994 );
995 assert_eq!(
996 base.resolve_embed("#dave").unwrap(),
997 AssetPath::from("alice/bob#dave")
998 );
999 }
1000
1001 #[test]
1002 fn test_resolve_insufficient_elements() {
1003 let base = AssetPath::from("alice/bob#carol");
1005 assert_eq!(
1006 base.resolve("../../joe/next").unwrap(),
1007 AssetPath::from("joe/next")
1008 );
1009 assert_eq!(
1010 base.resolve_embed("../../joe/next").unwrap(),
1011 AssetPath::from("../joe/next")
1012 );
1013 }
1014
1015 #[test]
1016 fn test_get_extension() {
1017 let result = AssetPath::from("http://a.tar.gz#Foo");
1018 assert_eq!(result.get_full_extension(), Some("tar.gz".to_string()));
1019
1020 let result = AssetPath::from("http://a#Foo");
1021 assert_eq!(result.get_full_extension(), None);
1022
1023 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1024 assert_eq!(result.get_full_extension(), Some("tar.bz2".to_string()));
1025
1026 let result = AssetPath::from("asset.Custom");
1027 assert_eq!(result.get_full_extension(), Some("Custom".to_string()));
1028 }
1029}