Fix embedded asset path manipulation (#10383)

# Objective

Fixes #10377

## Solution

Use `Path::strip_prefix` instead of `str::split`. Avoid any explicit "/"
characters in path manipulation.

---

## Changelog

- Added: example of embedded asset loading
- Added: support embedded assets in external crates
- Fixed: resolution of embedded assets
- Fixed: unexpected runtime panic during asset path resolution

## Migration Guide

No API changes.

---------

Co-authored-by: Shane Celis <shane.celis@gmail.com>
This commit is contained in:
Duncan 2024-02-02 06:49:05 -08:00 committed by GitHub
parent 6f2eec8f78
commit 176223b406
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 209 additions and 8 deletions

View file

@ -1193,6 +1193,17 @@ description = "Implements a custom AssetReader"
category = "Assets"
wasm = true
[[example]]
name = "embedded_asset"
path = "examples/asset/embedded_asset.rs"
doc-scrape-examples = true
[package.metadata.example.embedded_asset]
name = "Embedded Asset"
description = "Embed an asset in the application binary and load it"
category = "Assets"
wasm = true
[[example]]
name = "hot_asset_reloading"
path = "examples/asset/hot_asset_reloading.rs"

View file

@ -107,20 +107,48 @@ impl EmbeddedAssetRegistry {
#[macro_export]
macro_rules! embedded_path {
($path_str: expr) => {{
embedded_path!("/src/", $path_str)
embedded_path!("src", $path_str)
}};
($source_path: expr, $path_str: expr) => {{
let crate_name = module_path!().split(':').next().unwrap();
let after_src = file!().split($source_path).nth(1).unwrap();
let file_path = std::path::Path::new(after_src)
.parent()
.unwrap()
.join($path_str);
std::path::Path::new(crate_name).join(file_path)
$crate::io::embedded::_embedded_asset_path(
crate_name,
$source_path.as_ref(),
file!().as_ref(),
$path_str.as_ref(),
)
}};
}
/// Implementation detail of `embedded_path`, do not use this!
///
/// Returns an embedded asset path, given:
/// - `crate_name`: name of the crate where the asset is embedded
/// - `src_prefix`: path prefix of the crate's source directory, relative to the workspace root
/// - `file_path`: `std::file!()` path of the source file where `embedded_path!` is called
/// - `asset_path`: path of the embedded asset relative to `file_path`
#[doc(hidden)]
pub fn _embedded_asset_path(
crate_name: &str,
src_prefix: &Path,
file_path: &Path,
asset_path: &Path,
) -> PathBuf {
let mut maybe_parent = file_path.parent();
let after_src = loop {
let Some(parent) = maybe_parent else {
panic!("Failed to find src_prefix {src_prefix:?} in {file_path:?}")
};
if parent.ends_with(src_prefix) {
break file_path.strip_prefix(parent).unwrap();
}
maybe_parent = parent.parent();
};
let asset_path = after_src.parent().unwrap().join(asset_path);
Path::new(crate_name).join(asset_path)
}
/// Creates a new `embedded` asset by embedding the bytes of the given path into the current binary
/// and registering those bytes with the `embedded` [`AssetSource`].
///
@ -191,7 +219,7 @@ macro_rules! embedded_path {
#[macro_export]
macro_rules! embedded_asset {
($app: ident, $path: expr) => {{
embedded_asset!($app, "/src/", $path)
embedded_asset!($app, "src", $path)
}};
($app: ident, $source_path: expr, $path: expr) => {{
@ -269,3 +297,111 @@ macro_rules! load_internal_binary_asset {
);
}};
}
#[cfg(test)]
mod tests {
use super::_embedded_asset_path;
use std::path::Path;
// Relative paths show up if this macro is being invoked by a local crate.
// In this case we know the relative path is a sub- path of the workspace
// root.
#[test]
fn embedded_asset_path_from_local_crate() {
let asset_path = _embedded_asset_path(
"my_crate",
"src".as_ref(),
"src/foo/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
}
// A blank src_path removes the embedded's file path altogether only the
// asset path remains.
#[test]
fn embedded_asset_path_from_local_crate_blank_src_path_questionable() {
let asset_path = _embedded_asset_path(
"my_crate",
"".as_ref(),
"src/foo/some/deep/path/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
}
#[test]
#[should_panic(expected = "Failed to find src_prefix \"NOT-THERE\" in \"src")]
fn embedded_asset_path_from_local_crate_bad_src() {
let _asset_path = _embedded_asset_path(
"my_crate",
"NOT-THERE".as_ref(),
"src/foo/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
}
#[test]
fn embedded_asset_path_from_local_example_crate() {
let asset_path = _embedded_asset_path(
"example_name",
"examples/foo".as_ref(),
"examples/foo/example.rs".as_ref(),
"the/asset.png".as_ref(),
);
assert_eq!(asset_path, Path::new("example_name/the/asset.png"));
}
// Absolute paths show up if this macro is being invoked by an external
// dependency, e.g. one that's being checked out from a crates repo or git.
#[test]
fn embedded_asset_path_from_external_crate() {
let asset_path = _embedded_asset_path(
"my_crate",
"src".as_ref(),
"/path/to/crate/src/foo/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
}
#[test]
fn embedded_asset_path_from_external_crate_root_src_path() {
let asset_path = _embedded_asset_path(
"my_crate",
"/path/to/crate/src".as_ref(),
"/path/to/crate/src/foo/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
}
// Although extraneous slashes are permitted at the end, e.g., "src////",
// one or more slashes at the beginning are not.
#[test]
#[should_panic(expected = "Failed to find src_prefix \"////src\" in")]
fn embedded_asset_path_from_external_crate_extraneous_beginning_slashes() {
let asset_path = _embedded_asset_path(
"my_crate",
"////src".as_ref(),
"/path/to/crate/src/foo/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
assert_eq!(asset_path, Path::new("my_crate/foo/the/asset.png"));
}
// We don't handle this edge case because it is ambiguous with the
// information currently available to the embedded_path macro.
#[test]
fn embedded_asset_path_from_external_crate_is_ambiguous() {
let asset_path = _embedded_asset_path(
"my_crate",
"src".as_ref(),
"/path/to/.cargo/registry/src/crate/src/src/plugin.rs".as_ref(),
"the/asset.png".as_ref(),
);
// Really, should be "my_crate/src/the/asset.png"
assert_eq!(asset_path, Path::new("my_crate/the/asset.png"));
}
}

View file

@ -193,6 +193,7 @@ Example | Description
[Asset Processing](../examples/asset/processing/asset_processing.rs) | Demonstrates how to process and load custom assets
[Custom Asset](../examples/asset/custom_asset.rs) | Implements a custom asset loader
[Custom Asset IO](../examples/asset/custom_asset_reader.rs) | Implements a custom AssetReader
[Embedded Asset](../examples/asset/embedded_asset.rs) | Embed an asset in the application binary and load it
[Hot Reloading of Assets](../examples/asset/hot_asset_reloading.rs) | Demonstrates automatic reloading of assets when modified on disk
## Async Tasks

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 B

View file

@ -0,0 +1,53 @@
//! Example of loading an embedded asset.
use bevy::asset::{embedded_asset, io::AssetSourceId, AssetPath};
use bevy::prelude::*;
use std::path::Path;
fn main() {
App::new()
.add_plugins((DefaultPlugins, EmbeddedAssetPlugin))
.add_systems(Startup, setup)
.run();
}
struct EmbeddedAssetPlugin;
impl Plugin for EmbeddedAssetPlugin {
fn build(&self, app: &mut App) {
// We get to choose some prefix relative to the workspace root which
// will be ignored in "embedded://" asset paths.
let omit_prefix = "examples/asset";
// Path to asset must be relative to this file, because that's how
// include_bytes! works.
embedded_asset!(app, omit_prefix, "bevy_pixel_light.png");
}
}
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
commands.spawn(Camera2dBundle::default());
// Each example is its own crate (with name from [[example]] in Cargo.toml).
let crate_name = "embedded_asset";
// The actual file path relative to workspace root is
// "examples/asset/bevy_pixel_light.png".
//
// We omit the "examples/asset" from the embedded_asset! call and replace it
// with the crate name.
let path = Path::new(crate_name).join("bevy_pixel_light.png");
let source = AssetSourceId::from("embedded");
let asset_path = AssetPath::from_path(&path).with_source(source);
// You could also parse this URL-like string representation for the asset
// path.
assert_eq!(
asset_path,
"embedded://embedded_asset/bevy_pixel_light.png".into()
);
commands.spawn(SpriteBundle {
texture: asset_server.load(asset_path),
..default()
});
}