mirror of
https://github.com/bevyengine/bevy
synced 2024-11-10 07:04:33 +00:00
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>
This commit is contained in:
parent
a830530be4
commit
c81e2bd586
1 changed files with 383 additions and 0 deletions
|
@ -299,6 +299,139 @@ impl<'a> AssetPath<'a> {
|
|||
self.clone().into_owned()
|
||||
}
|
||||
|
||||
/// Resolves a relative asset path via concatenation. The result will be an `AssetPath` which
|
||||
/// is resolved relative to this "base" path.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bevy_asset::AssetPath;
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve("c"), Ok(AssetPath::parse("a/b/c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve("./c"), Ok(AssetPath::parse("a/b/c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve("../c"), Ok(AssetPath::parse("a/c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve("c.png"), Ok(AssetPath::parse("a/b/c.png")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve("/c"), Ok(AssetPath::parse("c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b.png").resolve("#c"), Ok(AssetPath::parse("a/b.png#c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve("#d"), Ok(AssetPath::parse("a/b.png#d")));
|
||||
/// ```
|
||||
///
|
||||
/// There are several cases:
|
||||
///
|
||||
/// If the `path` argument begins with `#`, then it is considered an asset label, in which case
|
||||
/// the result is the base path with the label portion replaced.
|
||||
///
|
||||
/// If the path argument begins with '/', then it is considered a 'full' path, in which
|
||||
/// case the result is a new `AssetPath` consisting of the base path asset source
|
||||
/// (if there is one) with the path and label portions of the relative path. Note that a 'full'
|
||||
/// asset path is still relative to the asset source root, and not necessarily an absolute
|
||||
/// filesystem path.
|
||||
///
|
||||
/// If the `path` argument begins with an asset source (ex: `http://`) then the entire base
|
||||
/// path is replaced - the result is the source, path and label (if any) of the `path`
|
||||
/// argument.
|
||||
///
|
||||
/// Otherwise, the `path` argument is considered a relative path. The result is concatenated
|
||||
/// using the following algorithm:
|
||||
///
|
||||
/// * The base path and the `path` argument are concatenated.
|
||||
/// * Path elements consisting of "/." or "<name>/.." are removed.
|
||||
///
|
||||
/// If there are insufficient segments in the base path to match the ".." segments,
|
||||
/// then any left-over ".." segments are left as-is.
|
||||
pub fn resolve(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
|
||||
self.resolve_internal(path, false)
|
||||
}
|
||||
|
||||
/// Resolves an embedded asset path via concatenation. The result will be an `AssetPath` which
|
||||
/// is resolved relative to this path. This is similar in operation to `resolve`, except that
|
||||
/// the the 'file' portion of the base path (that is, any characters after the last '/')
|
||||
/// is removed before concatenation, in accordance with the behavior specified in
|
||||
/// IETF RFC 1808 "Relative URIs".
|
||||
///
|
||||
/// The reason for this behavior is that embedded URIs which start with "./" or "../" are
|
||||
/// relative to the *directory* containing the asset, not the asset file. This is consistent
|
||||
/// with the behavior of URIs in `JavaScript`, CSS, HTML and other web file formats. The
|
||||
/// primary use case for this method is resolving relative paths embedded within asset files,
|
||||
/// which are relative to the asset in which they are contained.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bevy_asset::AssetPath;
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c"), Ok(AssetPath::parse("a/c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("./c"), Ok(AssetPath::parse("a/c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("../c"), Ok(AssetPath::parse("c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c.png"), Ok(AssetPath::parse("a/c.png")));
|
||||
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("/c"), Ok(AssetPath::parse("c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b.png").resolve_embed("#c"), Ok(AssetPath::parse("a/b.png#c")));
|
||||
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve_embed("#d"), Ok(AssetPath::parse("a/b.png#d")));
|
||||
/// ```
|
||||
pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
|
||||
self.resolve_internal(path, true)
|
||||
}
|
||||
|
||||
fn resolve_internal(
|
||||
&self,
|
||||
path: &str,
|
||||
replace: bool,
|
||||
) -> Result<AssetPath<'static>, ParseAssetPathError> {
|
||||
if let Some(label) = path.strip_prefix('#') {
|
||||
// It's a label only
|
||||
Ok(self.clone_owned().with_label(label.to_owned()))
|
||||
} else {
|
||||
let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
|
||||
let mut base_path = PathBuf::from(self.path());
|
||||
if replace && !self.path.to_str().unwrap().ends_with('/') {
|
||||
// No error if base is empty (per RFC 1808).
|
||||
base_path.pop();
|
||||
}
|
||||
|
||||
// Strip off leading slash
|
||||
let mut is_absolute = false;
|
||||
let rpath = match rpath.strip_prefix("/") {
|
||||
Ok(p) => {
|
||||
is_absolute = true;
|
||||
p
|
||||
}
|
||||
_ => rpath,
|
||||
};
|
||||
|
||||
let mut result_path = PathBuf::new();
|
||||
if !is_absolute && source.is_none() {
|
||||
for elt in base_path.iter() {
|
||||
if elt == "." {
|
||||
// Skip
|
||||
} else if elt == ".." {
|
||||
if !result_path.pop() {
|
||||
// Preserve ".." if insufficient matches (per RFC 1808).
|
||||
result_path.push(elt);
|
||||
}
|
||||
} else {
|
||||
result_path.push(elt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for elt in rpath.iter() {
|
||||
if elt == "." {
|
||||
// Skip
|
||||
} else if elt == ".." {
|
||||
if !result_path.pop() {
|
||||
// Preserve ".." if insufficient matches (per RFC 1808).
|
||||
result_path.push(elt);
|
||||
}
|
||||
} else {
|
||||
result_path.push(elt);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(AssetPath {
|
||||
source: match source {
|
||||
Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
|
||||
None => self.source.clone_owned(),
|
||||
},
|
||||
path: CowArc::Owned(result_path.into()),
|
||||
label: rlabel.map(|l| CowArc::Owned(l.into())),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the full extension (including multiple '.' values).
|
||||
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
|
||||
pub fn get_full_extension(&self) -> Option<String> {
|
||||
|
@ -583,4 +716,254 @@ mod tests {
|
|||
let result = AssetPath::parse_internal("http:/");
|
||||
assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_full() {
|
||||
// A "full" path should ignore the base path.
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("/joe/next").unwrap(),
|
||||
AssetPath::from("joe/next")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/joe/next").unwrap(),
|
||||
AssetPath::from("joe/next")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("/joe/next#dave").unwrap(),
|
||||
AssetPath::from("joe/next#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/joe/next#dave").unwrap(),
|
||||
AssetPath::from("joe/next#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_implicit_relative() {
|
||||
// A path with no inital directory separator should be considered relative.
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("joe/next").unwrap(),
|
||||
AssetPath::from("alice/bob/joe/next")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("joe/next").unwrap(),
|
||||
AssetPath::from("alice/joe/next")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("joe/next#dave").unwrap(),
|
||||
AssetPath::from("alice/bob/joe/next#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("joe/next#dave").unwrap(),
|
||||
AssetPath::from("alice/joe/next#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_explicit_relative() {
|
||||
// A path which begins with "./" or "../" is treated as relative
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("./martin#dave").unwrap(),
|
||||
AssetPath::from("alice/bob/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("./martin#dave").unwrap(),
|
||||
AssetPath::from("alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("../martin#dave").unwrap(),
|
||||
AssetPath::from("alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("../martin#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_trailing_slash() {
|
||||
// A path which begins with "./" or "../" is treated as relative
|
||||
let base = AssetPath::from("alice/bob/");
|
||||
assert_eq!(
|
||||
base.resolve("./martin#dave").unwrap(),
|
||||
AssetPath::from("alice/bob/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("./martin#dave").unwrap(),
|
||||
AssetPath::from("alice/bob/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("../martin#dave").unwrap(),
|
||||
AssetPath::from("alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("../martin#dave").unwrap(),
|
||||
AssetPath::from("alice/martin#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_canonicalize() {
|
||||
// Test that ".." and "." are removed after concatenation.
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("./martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("alice/bob/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("../martin/.#dave").unwrap(),
|
||||
AssetPath::from("alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("../martin/.#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("/martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_canonicalize_base() {
|
||||
// Test that ".." and "." are removed after concatenation even from the base path.
|
||||
let base = AssetPath::from("alice/../bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("./martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("bob/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("../martin/.#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("../martin/.#dave").unwrap(),
|
||||
AssetPath::from("../martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("/martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("martin#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_canonicalize_with_source() {
|
||||
// Test that ".." and "." are removed after concatenation.
|
||||
let base = AssetPath::from("source://alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("./martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("source://alice/bob/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("source://alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("../martin/.#dave").unwrap(),
|
||||
AssetPath::from("source://alice/martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("../martin/.#dave").unwrap(),
|
||||
AssetPath::from("source://martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("/martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("source://martin#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
|
||||
AssetPath::from("source://martin#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_absolute() {
|
||||
// Paths beginning with '/' replace the base path
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("/martin/stephan").unwrap(),
|
||||
AssetPath::from("martin/stephan")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/martin/stephan").unwrap(),
|
||||
AssetPath::from("martin/stephan")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("/martin/stephan#dave").unwrap(),
|
||||
AssetPath::from("martin/stephan/#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("/martin/stephan#dave").unwrap(),
|
||||
AssetPath::from("martin/stephan/#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_asset_source() {
|
||||
// Paths beginning with 'source://' replace the base path
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("source://martin/stephan").unwrap(),
|
||||
AssetPath::from("source://martin/stephan")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("source://martin/stephan").unwrap(),
|
||||
AssetPath::from("source://martin/stephan")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve("source://martin/stephan#dave").unwrap(),
|
||||
AssetPath::from("source://martin/stephan/#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("source://martin/stephan#dave").unwrap(),
|
||||
AssetPath::from("source://martin/stephan/#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_label() {
|
||||
// A relative path with only a label should replace the label portion
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("#dave").unwrap(),
|
||||
AssetPath::from("alice/bob#dave")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("#dave").unwrap(),
|
||||
AssetPath::from("alice/bob#dave")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_insufficient_elements() {
|
||||
// Ensure that ".." segments are preserved if there are insufficient elements to remove them.
|
||||
let base = AssetPath::from("alice/bob#carol");
|
||||
assert_eq!(
|
||||
base.resolve("../../joe/next").unwrap(),
|
||||
AssetPath::from("joe/next")
|
||||
);
|
||||
assert_eq!(
|
||||
base.resolve_embed("../../joe/next").unwrap(),
|
||||
AssetPath::from("../joe/next")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue