Skip to content

Commit c81e2bd

Browse files
viridiacart
andauthored
Adding AssetPath::resolve() method. (#9528)
# Objective Fixes #9473 ## Solution Added `resolve()` method to AssetPath. This method accepts a relative asset path string and returns a "full" path that has been resolved relative to the current (self) path. --------- Co-authored-by: Carter Anderson <mcanders1@gmail.com>
1 parent a830530 commit c81e2bd

File tree

1 file changed

+383
-0
lines changed

1 file changed

+383
-0
lines changed

crates/bevy_asset/src/path.rs

Lines changed: 383 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,139 @@ impl<'a> AssetPath<'a> {
299299
self.clone().into_owned()
300300
}
301301

302+
/// Resolves a relative asset path via concatenation. The result will be an `AssetPath` which
303+
/// is resolved relative to this "base" path.
304+
///
305+
/// ```rust
306+
/// # use bevy_asset::AssetPath;
307+
/// assert_eq!(AssetPath::parse("a/b").resolve("c"), Ok(AssetPath::parse("a/b/c")));
308+
/// assert_eq!(AssetPath::parse("a/b").resolve("./c"), Ok(AssetPath::parse("a/b/c")));
309+
/// assert_eq!(AssetPath::parse("a/b").resolve("../c"), Ok(AssetPath::parse("a/c")));
310+
/// assert_eq!(AssetPath::parse("a/b").resolve("c.png"), Ok(AssetPath::parse("a/b/c.png")));
311+
/// assert_eq!(AssetPath::parse("a/b").resolve("/c"), Ok(AssetPath::parse("c")));
312+
/// assert_eq!(AssetPath::parse("a/b.png").resolve("#c"), Ok(AssetPath::parse("a/b.png#c")));
313+
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve("#d"), Ok(AssetPath::parse("a/b.png#d")));
314+
/// ```
315+
///
316+
/// There are several cases:
317+
///
318+
/// If the `path` argument begins with `#`, then it is considered an asset label, in which case
319+
/// the result is the base path with the label portion replaced.
320+
///
321+
/// If the path argument begins with '/', then it is considered a 'full' path, in which
322+
/// case the result is a new `AssetPath` consisting of the base path asset source
323+
/// (if there is one) with the path and label portions of the relative path. Note that a 'full'
324+
/// asset path is still relative to the asset source root, and not necessarily an absolute
325+
/// filesystem path.
326+
///
327+
/// If the `path` argument begins with an asset source (ex: `http://`) then the entire base
328+
/// path is replaced - the result is the source, path and label (if any) of the `path`
329+
/// argument.
330+
///
331+
/// Otherwise, the `path` argument is considered a relative path. The result is concatenated
332+
/// using the following algorithm:
333+
///
334+
/// * The base path and the `path` argument are concatenated.
335+
/// * Path elements consisting of "/." or "&lt;name&gt;/.." are removed.
336+
///
337+
/// If there are insufficient segments in the base path to match the ".." segments,
338+
/// then any left-over ".." segments are left as-is.
339+
pub fn resolve(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
340+
self.resolve_internal(path, false)
341+
}
342+
343+
/// Resolves an embedded asset path via concatenation. The result will be an `AssetPath` which
344+
/// is resolved relative to this path. This is similar in operation to `resolve`, except that
345+
/// the the 'file' portion of the base path (that is, any characters after the last '/')
346+
/// is removed before concatenation, in accordance with the behavior specified in
347+
/// IETF RFC 1808 "Relative URIs".
348+
///
349+
/// The reason for this behavior is that embedded URIs which start with "./" or "../" are
350+
/// relative to the *directory* containing the asset, not the asset file. This is consistent
351+
/// with the behavior of URIs in `JavaScript`, CSS, HTML and other web file formats. The
352+
/// primary use case for this method is resolving relative paths embedded within asset files,
353+
/// which are relative to the asset in which they are contained.
354+
///
355+
/// ```rust
356+
/// # use bevy_asset::AssetPath;
357+
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c"), Ok(AssetPath::parse("a/c")));
358+
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("./c"), Ok(AssetPath::parse("a/c")));
359+
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("../c"), Ok(AssetPath::parse("c")));
360+
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c.png"), Ok(AssetPath::parse("a/c.png")));
361+
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("/c"), Ok(AssetPath::parse("c")));
362+
/// assert_eq!(AssetPath::parse("a/b.png").resolve_embed("#c"), Ok(AssetPath::parse("a/b.png#c")));
363+
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve_embed("#d"), Ok(AssetPath::parse("a/b.png#d")));
364+
/// ```
365+
pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
366+
self.resolve_internal(path, true)
367+
}
368+
369+
fn resolve_internal(
370+
&self,
371+
path: &str,
372+
replace: bool,
373+
) -> Result<AssetPath<'static>, ParseAssetPathError> {
374+
if let Some(label) = path.strip_prefix('#') {
375+
// It's a label only
376+
Ok(self.clone_owned().with_label(label.to_owned()))
377+
} else {
378+
let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
379+
let mut base_path = PathBuf::from(self.path());
380+
if replace && !self.path.to_str().unwrap().ends_with('/') {
381+
// No error if base is empty (per RFC 1808).
382+
base_path.pop();
383+
}
384+
385+
// Strip off leading slash
386+
let mut is_absolute = false;
387+
let rpath = match rpath.strip_prefix("/") {
388+
Ok(p) => {
389+
is_absolute = true;
390+
p
391+
}
392+
_ => rpath,
393+
};
394+
395+
let mut result_path = PathBuf::new();
396+
if !is_absolute && source.is_none() {
397+
for elt in base_path.iter() {
398+
if elt == "." {
399+
// Skip
400+
} else if elt == ".." {
401+
if !result_path.pop() {
402+
// Preserve ".." if insufficient matches (per RFC 1808).
403+
result_path.push(elt);
404+
}
405+
} else {
406+
result_path.push(elt);
407+
}
408+
}
409+
}
410+
411+
for elt in rpath.iter() {
412+
if elt == "." {
413+
// Skip
414+
} else if elt == ".." {
415+
if !result_path.pop() {
416+
// Preserve ".." if insufficient matches (per RFC 1808).
417+
result_path.push(elt);
418+
}
419+
} else {
420+
result_path.push(elt);
421+
}
422+
}
423+
424+
Ok(AssetPath {
425+
source: match source {
426+
Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
427+
None => self.source.clone_owned(),
428+
},
429+
path: CowArc::Owned(result_path.into()),
430+
label: rlabel.map(|l| CowArc::Owned(l.into())),
431+
})
432+
}
433+
}
434+
302435
/// Returns the full extension (including multiple '.' values).
303436
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
304437
pub fn get_full_extension(&self) -> Option<String> {
@@ -583,4 +716,254 @@ mod tests {
583716
let result = AssetPath::parse_internal("http:/");
584717
assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
585718
}
719+
720+
#[test]
721+
fn test_resolve_full() {
722+
// A "full" path should ignore the base path.
723+
let base = AssetPath::from("alice/bob#carol");
724+
assert_eq!(
725+
base.resolve("/joe/next").unwrap(),
726+
AssetPath::from("joe/next")
727+
);
728+
assert_eq!(
729+
base.resolve_embed("/joe/next").unwrap(),
730+
AssetPath::from("joe/next")
731+
);
732+
assert_eq!(
733+
base.resolve("/joe/next#dave").unwrap(),
734+
AssetPath::from("joe/next#dave")
735+
);
736+
assert_eq!(
737+
base.resolve_embed("/joe/next#dave").unwrap(),
738+
AssetPath::from("joe/next#dave")
739+
);
740+
}
741+
742+
#[test]
743+
fn test_resolve_implicit_relative() {
744+
// A path with no inital directory separator should be considered relative.
745+
let base = AssetPath::from("alice/bob#carol");
746+
assert_eq!(
747+
base.resolve("joe/next").unwrap(),
748+
AssetPath::from("alice/bob/joe/next")
749+
);
750+
assert_eq!(
751+
base.resolve_embed("joe/next").unwrap(),
752+
AssetPath::from("alice/joe/next")
753+
);
754+
assert_eq!(
755+
base.resolve("joe/next#dave").unwrap(),
756+
AssetPath::from("alice/bob/joe/next#dave")
757+
);
758+
assert_eq!(
759+
base.resolve_embed("joe/next#dave").unwrap(),
760+
AssetPath::from("alice/joe/next#dave")
761+
);
762+
}
763+
764+
#[test]
765+
fn test_resolve_explicit_relative() {
766+
// A path which begins with "./" or "../" is treated as relative
767+
let base = AssetPath::from("alice/bob#carol");
768+
assert_eq!(
769+
base.resolve("./martin#dave").unwrap(),
770+
AssetPath::from("alice/bob/martin#dave")
771+
);
772+
assert_eq!(
773+
base.resolve_embed("./martin#dave").unwrap(),
774+
AssetPath::from("alice/martin#dave")
775+
);
776+
assert_eq!(
777+
base.resolve("../martin#dave").unwrap(),
778+
AssetPath::from("alice/martin#dave")
779+
);
780+
assert_eq!(
781+
base.resolve_embed("../martin#dave").unwrap(),
782+
AssetPath::from("martin#dave")
783+
);
784+
}
785+
786+
#[test]
787+
fn test_resolve_trailing_slash() {
788+
// A path which begins with "./" or "../" is treated as relative
789+
let base = AssetPath::from("alice/bob/");
790+
assert_eq!(
791+
base.resolve("./martin#dave").unwrap(),
792+
AssetPath::from("alice/bob/martin#dave")
793+
);
794+
assert_eq!(
795+
base.resolve_embed("./martin#dave").unwrap(),
796+
AssetPath::from("alice/bob/martin#dave")
797+
);
798+
assert_eq!(
799+
base.resolve("../martin#dave").unwrap(),
800+
AssetPath::from("alice/martin#dave")
801+
);
802+
assert_eq!(
803+
base.resolve_embed("../martin#dave").unwrap(),
804+
AssetPath::from("alice/martin#dave")
805+
);
806+
}
807+
808+
#[test]
809+
fn test_resolve_canonicalize() {
810+
// Test that ".." and "." are removed after concatenation.
811+
let base = AssetPath::from("alice/bob#carol");
812+
assert_eq!(
813+
base.resolve("./martin/stephan/..#dave").unwrap(),
814+
AssetPath::from("alice/bob/martin#dave")
815+
);
816+
assert_eq!(
817+
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
818+
AssetPath::from("alice/martin#dave")
819+
);
820+
assert_eq!(
821+
base.resolve("../martin/.#dave").unwrap(),
822+
AssetPath::from("alice/martin#dave")
823+
);
824+
assert_eq!(
825+
base.resolve_embed("../martin/.#dave").unwrap(),
826+
AssetPath::from("martin#dave")
827+
);
828+
assert_eq!(
829+
base.resolve("/martin/stephan/..#dave").unwrap(),
830+
AssetPath::from("martin#dave")
831+
);
832+
assert_eq!(
833+
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
834+
AssetPath::from("martin#dave")
835+
);
836+
}
837+
838+
#[test]
839+
fn test_resolve_canonicalize_base() {
840+
// Test that ".." and "." are removed after concatenation even from the base path.
841+
let base = AssetPath::from("alice/../bob#carol");
842+
assert_eq!(
843+
base.resolve("./martin/stephan/..#dave").unwrap(),
844+
AssetPath::from("bob/martin#dave")
845+
);
846+
assert_eq!(
847+
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
848+
AssetPath::from("martin#dave")
849+
);
850+
assert_eq!(
851+
base.resolve("../martin/.#dave").unwrap(),
852+
AssetPath::from("martin#dave")
853+
);
854+
assert_eq!(
855+
base.resolve_embed("../martin/.#dave").unwrap(),
856+
AssetPath::from("../martin#dave")
857+
);
858+
assert_eq!(
859+
base.resolve("/martin/stephan/..#dave").unwrap(),
860+
AssetPath::from("martin#dave")
861+
);
862+
assert_eq!(
863+
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
864+
AssetPath::from("martin#dave")
865+
);
866+
}
867+
868+
#[test]
869+
fn test_resolve_canonicalize_with_source() {
870+
// Test that ".." and "." are removed after concatenation.
871+
let base = AssetPath::from("source://alice/bob#carol");
872+
assert_eq!(
873+
base.resolve("./martin/stephan/..#dave").unwrap(),
874+
AssetPath::from("source://alice/bob/martin#dave")
875+
);
876+
assert_eq!(
877+
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
878+
AssetPath::from("source://alice/martin#dave")
879+
);
880+
assert_eq!(
881+
base.resolve("../martin/.#dave").unwrap(),
882+
AssetPath::from("source://alice/martin#dave")
883+
);
884+
assert_eq!(
885+
base.resolve_embed("../martin/.#dave").unwrap(),
886+
AssetPath::from("source://martin#dave")
887+
);
888+
assert_eq!(
889+
base.resolve("/martin/stephan/..#dave").unwrap(),
890+
AssetPath::from("source://martin#dave")
891+
);
892+
assert_eq!(
893+
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
894+
AssetPath::from("source://martin#dave")
895+
);
896+
}
897+
898+
#[test]
899+
fn test_resolve_absolute() {
900+
// Paths beginning with '/' replace the base path
901+
let base = AssetPath::from("alice/bob#carol");
902+
assert_eq!(
903+
base.resolve("/martin/stephan").unwrap(),
904+
AssetPath::from("martin/stephan")
905+
);
906+
assert_eq!(
907+
base.resolve_embed("/martin/stephan").unwrap(),
908+
AssetPath::from("martin/stephan")
909+
);
910+
assert_eq!(
911+
base.resolve("/martin/stephan#dave").unwrap(),
912+
AssetPath::from("martin/stephan/#dave")
913+
);
914+
assert_eq!(
915+
base.resolve_embed("/martin/stephan#dave").unwrap(),
916+
AssetPath::from("martin/stephan/#dave")
917+
);
918+
}
919+
920+
#[test]
921+
fn test_resolve_asset_source() {
922+
// Paths beginning with 'source://' replace the base path
923+
let base = AssetPath::from("alice/bob#carol");
924+
assert_eq!(
925+
base.resolve("source://martin/stephan").unwrap(),
926+
AssetPath::from("source://martin/stephan")
927+
);
928+
assert_eq!(
929+
base.resolve_embed("source://martin/stephan").unwrap(),
930+
AssetPath::from("source://martin/stephan")
931+
);
932+
assert_eq!(
933+
base.resolve("source://martin/stephan#dave").unwrap(),
934+
AssetPath::from("source://martin/stephan/#dave")
935+
);
936+
assert_eq!(
937+
base.resolve_embed("source://martin/stephan#dave").unwrap(),
938+
AssetPath::from("source://martin/stephan/#dave")
939+
);
940+
}
941+
942+
#[test]
943+
fn test_resolve_label() {
944+
// A relative path with only a label should replace the label portion
945+
let base = AssetPath::from("alice/bob#carol");
946+
assert_eq!(
947+
base.resolve("#dave").unwrap(),
948+
AssetPath::from("alice/bob#dave")
949+
);
950+
assert_eq!(
951+
base.resolve_embed("#dave").unwrap(),
952+
AssetPath::from("alice/bob#dave")
953+
);
954+
}
955+
956+
#[test]
957+
fn test_resolve_insufficient_elements() {
958+
// Ensure that ".." segments are preserved if there are insufficient elements to remove them.
959+
let base = AssetPath::from("alice/bob#carol");
960+
assert_eq!(
961+
base.resolve("../../joe/next").unwrap(),
962+
AssetPath::from("joe/next")
963+
);
964+
assert_eq!(
965+
base.resolve_embed("../../joe/next").unwrap(),
966+
AssetPath::from("../joe/next")
967+
);
968+
}
586969
}

0 commit comments

Comments
 (0)