1use crate::io::AssetSourceId;
2use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
3use bevy_utils::CowArc;
4use serde::{de::Visitor, Deserialize, Serialize};
5use std::{
6 fmt::{Debug, Display},
7 hash::Hash,
8 ops::Deref,
9 path::{Path, PathBuf},
10};
11use thiserror::Error;
12
13#[derive(Eq, PartialEq, Hash, Clone, Default, Reflect)]
51#[reflect_value(Debug, PartialEq, Hash, Serialize, Deserialize)]
52pub struct AssetPath<'a> {
53 source: AssetSourceId<'a>,
54 path: CowArc<'a, Path>,
55 label: Option<CowArc<'a, str>>,
56}
57
58impl<'a> Debug for AssetPath<'a> {
59 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 Display::fmt(self, f)
61 }
62}
63
64impl<'a> Display for AssetPath<'a> {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 if let AssetSourceId::Name(name) = self.source() {
67 write!(f, "{name}://")?;
68 }
69 write!(f, "{}", self.path.display())?;
70 if let Some(label) = &self.label {
71 write!(f, "#{label}")?;
72 }
73 Ok(())
74 }
75}
76
77#[derive(Error, Debug, PartialEq, Eq)]
79pub enum ParseAssetPathError {
80 #[error("Asset source must not contain a `#` character")]
82 InvalidSourceSyntax,
83 #[error("Asset label must not contain a `://` substring")]
85 InvalidLabelSyntax,
86 #[error("Asset source must be at least one character. Either specify the source before the '://' or remove the `://`")]
88 MissingSource,
89 #[error("Asset label must be at least one character. Either specify the label after the '#' or remove the '#'")]
91 MissingLabel,
92}
93
94impl<'a> AssetPath<'a> {
95 pub fn parse(asset_path: &'a str) -> AssetPath<'a> {
107 Self::try_parse(asset_path).unwrap()
108 }
109
110 pub fn try_parse(asset_path: &'a str) -> Result<AssetPath<'a>, ParseAssetPathError> {
121 let (source, path, label) = Self::parse_internal(asset_path)?;
122 Ok(Self {
123 source: match source {
124 Some(source) => AssetSourceId::Name(CowArc::Borrowed(source)),
125 None => AssetSourceId::Default,
126 },
127 path: CowArc::Borrowed(path),
128 label: label.map(CowArc::Borrowed),
129 })
130 }
131
132 fn parse_internal(
134 asset_path: &str,
135 ) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> {
136 let chars = asset_path.char_indices();
137 let mut source_range = None;
138 let mut path_range = 0..asset_path.len();
139 let mut label_range = None;
140
141 let mut source_delimiter_chars_matched = 0;
150 let mut last_found_source_index = 0;
151 for (index, char) in chars {
152 match char {
153 ':' => {
154 source_delimiter_chars_matched = 1;
155 }
156 '/' => {
157 match source_delimiter_chars_matched {
158 1 => {
159 source_delimiter_chars_matched = 2;
160 }
161 2 => {
162 if source_range.is_none() {
164 if label_range.is_some() {
166 return Err(ParseAssetPathError::InvalidSourceSyntax);
167 }
168 source_range = Some(0..index - 2);
169 path_range.start = index + 1;
170 }
171 last_found_source_index = index - 2;
172 source_delimiter_chars_matched = 0;
173 }
174 _ => {}
175 }
176 }
177 '#' => {
178 path_range.end = index;
179 label_range = Some(index + 1..asset_path.len());
180 source_delimiter_chars_matched = 0;
181 }
182 _ => {
183 source_delimiter_chars_matched = 0;
184 }
185 }
186 }
187 if let Some(range) = label_range.clone() {
189 if range.start <= last_found_source_index {
191 return Err(ParseAssetPathError::InvalidLabelSyntax);
192 }
193 }
194 let source = match source_range {
197 Some(source_range) => {
198 if source_range.is_empty() {
199 return Err(ParseAssetPathError::MissingSource);
200 }
201 Some(&asset_path[source_range])
202 }
203 None => None,
204 };
205 let label = match label_range {
208 Some(label_range) => {
209 if label_range.is_empty() {
210 return Err(ParseAssetPathError::MissingLabel);
211 }
212 Some(&asset_path[label_range])
213 }
214 None => None,
215 };
216
217 let path = Path::new(&asset_path[path_range]);
218 Ok((source, path, label))
219 }
220
221 #[inline]
223 pub fn from_path(path: &'a Path) -> AssetPath<'a> {
224 AssetPath {
225 path: CowArc::Borrowed(path),
226 source: AssetSourceId::Default,
227 label: None,
228 }
229 }
230
231 #[inline]
234 pub fn source(&self) -> &AssetSourceId {
235 &self.source
236 }
237
238 #[inline]
240 pub fn label(&self) -> Option<&str> {
241 self.label.as_deref()
242 }
243
244 #[inline]
246 pub fn label_cow(&self) -> Option<CowArc<'a, str>> {
247 self.label.clone()
248 }
249
250 #[inline]
252 pub fn path(&self) -> &Path {
253 self.path.deref()
254 }
255
256 #[inline]
258 pub fn without_label(&self) -> AssetPath<'_> {
259 Self {
260 source: self.source.clone(),
261 path: self.path.clone(),
262 label: None,
263 }
264 }
265
266 #[inline]
268 pub fn remove_label(&mut self) {
269 self.label = None;
270 }
271
272 #[inline]
274 pub fn take_label(&mut self) -> Option<CowArc<'a, str>> {
275 self.label.take()
276 }
277
278 #[inline]
281 pub fn with_label(self, label: impl Into<CowArc<'a, str>>) -> AssetPath<'a> {
282 AssetPath {
283 source: self.source,
284 path: self.path,
285 label: Some(label.into()),
286 }
287 }
288
289 #[inline]
292 pub fn with_source(self, source: impl Into<AssetSourceId<'a>>) -> AssetPath<'a> {
293 AssetPath {
294 source: source.into(),
295 path: self.path,
296 label: self.label,
297 }
298 }
299
300 pub fn parent(&self) -> Option<AssetPath<'a>> {
302 let path = match &self.path {
303 CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?),
304 CowArc::Static(path) => CowArc::Static(path.parent()?),
305 CowArc::Owned(path) => path.parent()?.to_path_buf().into(),
306 };
307 Some(AssetPath {
308 source: self.source.clone(),
309 label: None,
310 path,
311 })
312 }
313
314 pub fn into_owned(self) -> AssetPath<'static> {
320 AssetPath {
321 source: self.source.into_owned(),
322 path: self.path.into_owned(),
323 label: self.label.map(|l| l.into_owned()),
324 }
325 }
326
327 #[inline]
333 pub fn clone_owned(&self) -> AssetPath<'static> {
334 self.clone().into_owned()
335 }
336
337 pub fn resolve(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
375 self.resolve_internal(path, false)
376 }
377
378 pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
401 self.resolve_internal(path, true)
402 }
403
404 fn resolve_internal(
405 &self,
406 path: &str,
407 replace: bool,
408 ) -> Result<AssetPath<'static>, ParseAssetPathError> {
409 if let Some(label) = path.strip_prefix('#') {
410 Ok(self.clone_owned().with_label(label.to_owned()))
412 } else {
413 let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
414 let mut base_path = PathBuf::from(self.path());
415 if replace && !self.path.to_str().unwrap().ends_with('/') {
416 base_path.pop();
418 }
419
420 let mut is_absolute = false;
422 let rpath = match rpath.strip_prefix("/") {
423 Ok(p) => {
424 is_absolute = true;
425 p
426 }
427 _ => rpath,
428 };
429
430 let mut result_path = if !is_absolute && source.is_none() {
431 base_path
432 } else {
433 PathBuf::new()
434 };
435 result_path.push(rpath);
436 result_path = normalize_path(result_path.as_path());
437
438 Ok(AssetPath {
439 source: match source {
440 Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
441 None => self.source.clone_owned(),
442 },
443 path: CowArc::Owned(result_path.into()),
444 label: rlabel.map(|l| CowArc::Owned(l.into())),
445 })
446 }
447 }
448
449 pub fn get_full_extension(&self) -> Option<String> {
454 let file_name = self.path().file_name()?.to_str()?;
455 let index = file_name.find('.')?;
456 let mut extension = file_name[index + 1..].to_lowercase();
457
458 let query = extension.find('?');
460 if let Some(offset) = query {
461 extension.truncate(offset);
462 }
463
464 Some(extension)
465 }
466
467 pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
468 full_extension.chars().enumerate().filter_map(|(i, c)| {
469 if c == '.' {
470 Some(&full_extension[i + 1..])
471 } else {
472 None
473 }
474 })
475 }
476}
477
478impl From<&'static str> for AssetPath<'static> {
479 #[inline]
480 fn from(asset_path: &'static str) -> Self {
481 let (source, path, label) = Self::parse_internal(asset_path).unwrap();
482 AssetPath {
483 source: source.into(),
484 path: CowArc::Static(path),
485 label: label.map(CowArc::Static),
486 }
487 }
488}
489
490impl<'a> From<&'a String> for AssetPath<'a> {
491 #[inline]
492 fn from(asset_path: &'a String) -> Self {
493 AssetPath::parse(asset_path.as_str())
494 }
495}
496
497impl From<String> for AssetPath<'static> {
498 #[inline]
499 fn from(asset_path: String) -> Self {
500 AssetPath::parse(asset_path.as_str()).into_owned()
501 }
502}
503
504impl From<&'static Path> for AssetPath<'static> {
505 #[inline]
506 fn from(path: &'static Path) -> Self {
507 Self {
508 source: AssetSourceId::Default,
509 path: CowArc::Static(path),
510 label: None,
511 }
512 }
513}
514
515impl From<PathBuf> for AssetPath<'static> {
516 #[inline]
517 fn from(path: PathBuf) -> Self {
518 Self {
519 source: AssetSourceId::Default,
520 path: path.into(),
521 label: None,
522 }
523 }
524}
525
526impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
527 fn from(value: &'a AssetPath<'b>) -> Self {
528 value.clone()
529 }
530}
531
532impl<'a> From<AssetPath<'a>> for PathBuf {
533 fn from(value: AssetPath<'a>) -> Self {
534 value.path().to_path_buf()
535 }
536}
537
538impl<'a> Serialize for AssetPath<'a> {
539 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
540 where
541 S: serde::Serializer,
542 {
543 self.to_string().serialize(serializer)
544 }
545}
546
547impl<'de> Deserialize<'de> for AssetPath<'static> {
548 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
549 where
550 D: serde::Deserializer<'de>,
551 {
552 deserializer.deserialize_string(AssetPathVisitor)
553 }
554}
555
556struct AssetPathVisitor;
557
558impl<'de> Visitor<'de> for AssetPathVisitor {
559 type Value = AssetPath<'static>;
560
561 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
562 formatter.write_str("string AssetPath")
563 }
564
565 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
566 where
567 E: serde::de::Error,
568 {
569 Ok(AssetPath::parse(v).into_owned())
570 }
571
572 fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
573 where
574 E: serde::de::Error,
575 {
576 Ok(AssetPath::from(v))
577 }
578}
579
580pub(crate) fn normalize_path(path: &Path) -> PathBuf {
583 let mut result_path = PathBuf::new();
584 for elt in path.iter() {
585 if elt == "." {
586 } else if elt == ".." {
588 if !result_path.pop() {
589 result_path.push(elt);
591 }
592 } else {
593 result_path.push(elt);
594 }
595 }
596 result_path
597}
598
599#[cfg(test)]
600mod tests {
601 use crate::AssetPath;
602 use std::path::Path;
603
604 #[test]
605 fn parse_asset_path() {
606 let result = AssetPath::parse_internal("a/b.test");
607 assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
608
609 let result = AssetPath::parse_internal("http://a/b.test");
610 assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
611
612 let result = AssetPath::parse_internal("http://a/b.test#Foo");
613 assert_eq!(
614 result,
615 Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
616 );
617
618 let result = AssetPath::parse_internal("localhost:80/b.test");
619 assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
620
621 let result = AssetPath::parse_internal("http://localhost:80/b.test");
622 assert_eq!(
623 result,
624 Ok((Some("http"), Path::new("localhost:80/b.test"), None))
625 );
626
627 let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
628 assert_eq!(
629 result,
630 Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
631 );
632
633 let result = AssetPath::parse_internal("#insource://a/b.test");
634 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
635
636 let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
637 assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
638
639 let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
640 assert!(
641 result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
642 || result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
643 );
644
645 let result = AssetPath::parse_internal("http://");
646 assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
647
648 let result = AssetPath::parse_internal("://x");
649 assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
650
651 let result = AssetPath::parse_internal("a/b.test#");
652 assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
653 }
654
655 #[test]
656 fn test_parent() {
657 let result = AssetPath::from("a/b.test");
659 assert_eq!(result.parent(), Some(AssetPath::from("a")));
660 assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
661 assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
662
663 let result = AssetPath::from("http://a");
665 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
666 assert_eq!(result.parent().unwrap().parent(), None);
667
668 let result = AssetPath::from("http://a#Foo");
670 assert_eq!(result.parent(), Some(AssetPath::from("http://")));
671 }
672
673 #[test]
674 fn test_with_source() {
675 let result = AssetPath::from("http://a#Foo");
676 assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
677 }
678
679 #[test]
680 fn test_without_label() {
681 let result = AssetPath::from("http://a#Foo");
682 assert_eq!(result.without_label(), AssetPath::from("http://a"));
683 }
684
685 #[test]
686 fn test_resolve_full() {
687 let base = AssetPath::from("alice/bob#carol");
689 assert_eq!(
690 base.resolve("/joe/next").unwrap(),
691 AssetPath::from("joe/next")
692 );
693 assert_eq!(
694 base.resolve_embed("/joe/next").unwrap(),
695 AssetPath::from("joe/next")
696 );
697 assert_eq!(
698 base.resolve("/joe/next#dave").unwrap(),
699 AssetPath::from("joe/next#dave")
700 );
701 assert_eq!(
702 base.resolve_embed("/joe/next#dave").unwrap(),
703 AssetPath::from("joe/next#dave")
704 );
705 }
706
707 #[test]
708 fn test_resolve_implicit_relative() {
709 let base = AssetPath::from("alice/bob#carol");
711 assert_eq!(
712 base.resolve("joe/next").unwrap(),
713 AssetPath::from("alice/bob/joe/next")
714 );
715 assert_eq!(
716 base.resolve_embed("joe/next").unwrap(),
717 AssetPath::from("alice/joe/next")
718 );
719 assert_eq!(
720 base.resolve("joe/next#dave").unwrap(),
721 AssetPath::from("alice/bob/joe/next#dave")
722 );
723 assert_eq!(
724 base.resolve_embed("joe/next#dave").unwrap(),
725 AssetPath::from("alice/joe/next#dave")
726 );
727 }
728
729 #[test]
730 fn test_resolve_explicit_relative() {
731 let base = AssetPath::from("alice/bob#carol");
733 assert_eq!(
734 base.resolve("./martin#dave").unwrap(),
735 AssetPath::from("alice/bob/martin#dave")
736 );
737 assert_eq!(
738 base.resolve_embed("./martin#dave").unwrap(),
739 AssetPath::from("alice/martin#dave")
740 );
741 assert_eq!(
742 base.resolve("../martin#dave").unwrap(),
743 AssetPath::from("alice/martin#dave")
744 );
745 assert_eq!(
746 base.resolve_embed("../martin#dave").unwrap(),
747 AssetPath::from("martin#dave")
748 );
749 }
750
751 #[test]
752 fn test_resolve_trailing_slash() {
753 let base = AssetPath::from("alice/bob/");
755 assert_eq!(
756 base.resolve("./martin#dave").unwrap(),
757 AssetPath::from("alice/bob/martin#dave")
758 );
759 assert_eq!(
760 base.resolve_embed("./martin#dave").unwrap(),
761 AssetPath::from("alice/bob/martin#dave")
762 );
763 assert_eq!(
764 base.resolve("../martin#dave").unwrap(),
765 AssetPath::from("alice/martin#dave")
766 );
767 assert_eq!(
768 base.resolve_embed("../martin#dave").unwrap(),
769 AssetPath::from("alice/martin#dave")
770 );
771 }
772
773 #[test]
774 fn test_resolve_canonicalize() {
775 let base = AssetPath::from("alice/bob#carol");
777 assert_eq!(
778 base.resolve("./martin/stephan/..#dave").unwrap(),
779 AssetPath::from("alice/bob/martin#dave")
780 );
781 assert_eq!(
782 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
783 AssetPath::from("alice/martin#dave")
784 );
785 assert_eq!(
786 base.resolve("../martin/.#dave").unwrap(),
787 AssetPath::from("alice/martin#dave")
788 );
789 assert_eq!(
790 base.resolve_embed("../martin/.#dave").unwrap(),
791 AssetPath::from("martin#dave")
792 );
793 assert_eq!(
794 base.resolve("/martin/stephan/..#dave").unwrap(),
795 AssetPath::from("martin#dave")
796 );
797 assert_eq!(
798 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
799 AssetPath::from("martin#dave")
800 );
801 }
802
803 #[test]
804 fn test_resolve_canonicalize_base() {
805 let base = AssetPath::from("alice/../bob#carol");
807 assert_eq!(
808 base.resolve("./martin/stephan/..#dave").unwrap(),
809 AssetPath::from("bob/martin#dave")
810 );
811 assert_eq!(
812 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
813 AssetPath::from("martin#dave")
814 );
815 assert_eq!(
816 base.resolve("../martin/.#dave").unwrap(),
817 AssetPath::from("martin#dave")
818 );
819 assert_eq!(
820 base.resolve_embed("../martin/.#dave").unwrap(),
821 AssetPath::from("../martin#dave")
822 );
823 assert_eq!(
824 base.resolve("/martin/stephan/..#dave").unwrap(),
825 AssetPath::from("martin#dave")
826 );
827 assert_eq!(
828 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
829 AssetPath::from("martin#dave")
830 );
831 }
832
833 #[test]
834 fn test_resolve_canonicalize_with_source() {
835 let base = AssetPath::from("source://alice/bob#carol");
837 assert_eq!(
838 base.resolve("./martin/stephan/..#dave").unwrap(),
839 AssetPath::from("source://alice/bob/martin#dave")
840 );
841 assert_eq!(
842 base.resolve_embed("./martin/stephan/..#dave").unwrap(),
843 AssetPath::from("source://alice/martin#dave")
844 );
845 assert_eq!(
846 base.resolve("../martin/.#dave").unwrap(),
847 AssetPath::from("source://alice/martin#dave")
848 );
849 assert_eq!(
850 base.resolve_embed("../martin/.#dave").unwrap(),
851 AssetPath::from("source://martin#dave")
852 );
853 assert_eq!(
854 base.resolve("/martin/stephan/..#dave").unwrap(),
855 AssetPath::from("source://martin#dave")
856 );
857 assert_eq!(
858 base.resolve_embed("/martin/stephan/..#dave").unwrap(),
859 AssetPath::from("source://martin#dave")
860 );
861 }
862
863 #[test]
864 fn test_resolve_absolute() {
865 let base = AssetPath::from("alice/bob#carol");
867 assert_eq!(
868 base.resolve("/martin/stephan").unwrap(),
869 AssetPath::from("martin/stephan")
870 );
871 assert_eq!(
872 base.resolve_embed("/martin/stephan").unwrap(),
873 AssetPath::from("martin/stephan")
874 );
875 assert_eq!(
876 base.resolve("/martin/stephan#dave").unwrap(),
877 AssetPath::from("martin/stephan/#dave")
878 );
879 assert_eq!(
880 base.resolve_embed("/martin/stephan#dave").unwrap(),
881 AssetPath::from("martin/stephan/#dave")
882 );
883 }
884
885 #[test]
886 fn test_resolve_asset_source() {
887 let base = AssetPath::from("alice/bob#carol");
889 assert_eq!(
890 base.resolve("source://martin/stephan").unwrap(),
891 AssetPath::from("source://martin/stephan")
892 );
893 assert_eq!(
894 base.resolve_embed("source://martin/stephan").unwrap(),
895 AssetPath::from("source://martin/stephan")
896 );
897 assert_eq!(
898 base.resolve("source://martin/stephan#dave").unwrap(),
899 AssetPath::from("source://martin/stephan/#dave")
900 );
901 assert_eq!(
902 base.resolve_embed("source://martin/stephan#dave").unwrap(),
903 AssetPath::from("source://martin/stephan/#dave")
904 );
905 }
906
907 #[test]
908 fn test_resolve_label() {
909 let base = AssetPath::from("alice/bob#carol");
911 assert_eq!(
912 base.resolve("#dave").unwrap(),
913 AssetPath::from("alice/bob#dave")
914 );
915 assert_eq!(
916 base.resolve_embed("#dave").unwrap(),
917 AssetPath::from("alice/bob#dave")
918 );
919 }
920
921 #[test]
922 fn test_resolve_insufficient_elements() {
923 let base = AssetPath::from("alice/bob#carol");
925 assert_eq!(
926 base.resolve("../../joe/next").unwrap(),
927 AssetPath::from("joe/next")
928 );
929 assert_eq!(
930 base.resolve_embed("../../joe/next").unwrap(),
931 AssetPath::from("../joe/next")
932 );
933 }
934
935 #[test]
936 fn test_get_extension() {
937 let result = AssetPath::from("http://a.tar.gz#Foo");
938 assert_eq!(result.get_full_extension(), Some("tar.gz".to_string()));
939
940 let result = AssetPath::from("http://a#Foo");
941 assert_eq!(result.get_full_extension(), None);
942
943 let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
944 assert_eq!(result.get_full_extension(), Some("tar.bz2".to_string()));
945 }
946}