diff --git a/crates/bevy_asset/src/path.rs b/crates/bevy_asset/src/path.rs index ed189a683b35d..05b75b4fac4bb 100644 --- a/crates/bevy_asset/src/path.rs +++ b/crates/bevy_asset/src/path.rs @@ -465,18 +465,17 @@ impl<'a> AssetPath<'a> { /// Ex: Returns `"config.ron"` for `"my_asset.config.ron"` /// /// Also strips out anything following a `?` to handle query parameters in URIs - pub fn get_full_extension(&self) -> Option { - let file_name = self.path().file_name()?.to_str()?; - let index = file_name.find('.')?; - let mut extension = file_name[index + 1..].to_owned(); + pub fn get_full_extension(&self) -> Option<&str> { + let file_name = self.path().file_name()?.as_encoded_bytes(); + let index = file_name.iter().position(|b| *b == b'.')?; + let mut extension = &file_name[index + 1..]; // Strip off any query parameters - let query = extension.find('?'); - if let Some(offset) = query { - extension.truncate(offset); + if let Some(offset) = extension.iter().position(|b| *b == b'?') { + extension = &extension[..offset]; } - Some(extension) + core::str::from_utf8(extension).ok() } pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator { @@ -688,7 +687,6 @@ pub(crate) fn normalize_path(path: &Path) -> PathBuf { #[cfg(test)] mod tests { use crate::AssetPath; - use alloc::string::ToString; use std::path::Path; #[test] @@ -1025,15 +1023,28 @@ mod tests { #[test] fn test_get_extension() { let result = AssetPath::from("http://a.tar.gz#Foo"); - assert_eq!(result.get_full_extension(), Some("tar.gz".to_string())); + assert_eq!(result.get_full_extension(), Some("tar.gz")); let result = AssetPath::from("http://a#Foo"); assert_eq!(result.get_full_extension(), None); let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz"); - assert_eq!(result.get_full_extension(), Some("tar.bz2".to_string())); + assert_eq!(result.get_full_extension(), Some("tar.bz2")); let result = AssetPath::from("asset.Custom"); - assert_eq!(result.get_full_extension(), Some("Custom".to_string())); + assert_eq!(result.get_full_extension(), Some("Custom")); + } + + // Regression test for non-UTF-8 file names. Creating an `OsStr` from bytes (without unsafe) is + // platform-dependent, so we just test Unix here. + #[test] + #[cfg(unix)] + fn test_get_full_extension_non_utf8() { + use std::ffi::OsStr; + use std::os::unix::ffi::OsStrExt; + + let path = OsStr::from_bytes(b"imag\xe9.png"); + let result = AssetPath::from_path(Path::new(path)); + assert_eq!(result.get_full_extension(), Some("png")); } } diff --git a/crates/bevy_asset/src/processor/mod.rs b/crates/bevy_asset/src/processor/mod.rs index a239d66a9b59a..df4fc9973ff68 100644 --- a/crates/bevy_asset/src/processor/mod.rs +++ b/crates/bevy_asset/src/processor/mod.rs @@ -276,7 +276,7 @@ impl AssetProcessor { let path = path.into(); let Some(processor) = path .get_full_extension() - .and_then(|extension| self.get_default_processor(&extension)) + .and_then(|extension| self.get_default_processor(extension)) else { return self .server @@ -841,7 +841,7 @@ impl AssetProcessor { Err(AssetReaderError::NotFound(_path)) => { let (meta, processor) = if let Some(processor) = asset_path .get_full_extension() - .and_then(|ext| self.get_default_processor(&ext)) + .and_then(|ext| self.get_default_processor(ext)) { let meta = processor.default_meta(); (meta, Some(processor)) diff --git a/crates/bevy_asset/src/server/loaders.rs b/crates/bevy_asset/src/server/loaders.rs index 08384e9efeaa4..8762999fb3b4c 100644 --- a/crates/bevy_asset/src/server/loaders.rs +++ b/crates/bevy_asset/src/server/loaders.rs @@ -226,12 +226,12 @@ impl AssetLoaders { // Try extracting the extension from the path if let Some(full_extension) = asset_path.and_then(AssetPath::get_full_extension) { - if let Some(&index) = try_extension(full_extension.as_str()) { + if let Some(&index) = try_extension(full_extension) { return self.get_by_index(index); } // Try secondary extensions from the path - for extension in AssetPath::iter_secondary_extensions(&full_extension) { + for extension in AssetPath::iter_secondary_extensions(full_extension) { if let Some(&index) = try_extension(extension) { return self.get_by_index(index); } @@ -279,8 +279,8 @@ impl AssetLoaders { pub(crate) fn get_by_path(&self, path: &AssetPath<'_>) -> Option { let extension = path.get_full_extension()?; - let result = core::iter::once(extension.as_str()) - .chain(AssetPath::iter_secondary_extensions(&extension)) + let result = core::iter::once(extension) + .chain(AssetPath::iter_secondary_extensions(extension)) .filter_map(|extension| self.extension_to_loaders.get(extension)?.last().copied()) .find_map(|index| self.get_by_index(index))?; diff --git a/crates/bevy_asset/src/server/mod.rs b/crates/bevy_asset/src/server/mod.rs index 0b64fe74e0afb..356e77f4d108c 100644 --- a/crates/bevy_asset/src/server/mod.rs +++ b/crates/bevy_asset/src/server/mod.rs @@ -244,9 +244,9 @@ impl AssetServer { }; }; - let mut extensions = vec![full_extension.clone()]; + let mut extensions = vec![full_extension.to_owned()]; extensions.extend( - AssetPath::iter_secondary_extensions(&full_extension).map(ToString::to_string), + AssetPath::iter_secondary_extensions(full_extension).map(ToString::to_string), ); MissingAssetLoaderForExtensionError { extensions } diff --git a/release-content/migration-guides/assetpath_get_full_extension.md b/release-content/migration-guides/assetpath_get_full_extension.md new file mode 100644 index 0000000000000..a95c8ed74a63e --- /dev/null +++ b/release-content/migration-guides/assetpath_get_full_extension.md @@ -0,0 +1,6 @@ +--- +title: `AssetPath::get_full_extension` returns `&str` +pull_requests: [19974] +--- + +`AssetPath::get_full_extension` now works on non-UTF-8 file names and returns `&str` instead of `String`.