Skip to content

Commit

Permalink
Fix embedded asset path manipulation (bevyengine#10383)
Browse files Browse the repository at this point in the history
# Objective

Fixes bevyengine#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 <[email protected]>
  • Loading branch information
2 people authored and tjamaan committed Feb 6, 2024
1 parent 44b7260 commit 9959312
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 8 deletions.
11 changes: 11 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
152 changes: 144 additions & 8 deletions crates/bevy_asset/src/io/embedded/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`].
///
Expand Down Expand Up @@ -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) => {{
Expand Down Expand Up @@ -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"));
}
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Binary file added examples/asset/bevy_pixel_light.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
53 changes: 53 additions & 0 deletions examples/asset/embedded_asset.rs
Original file line number Diff line number Diff line change
@@ -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()
});
}

0 comments on commit 9959312

Please sign in to comment.