change AssetPath to use &str

This commit is contained in:
lielfr 2025-05-09 03:05:31 +03:00
parent 0d620cdf29
commit 3d0309771d
2 changed files with 71 additions and 39 deletions

View File

@ -63,6 +63,7 @@ uuid = { version = "1.13.1", default-features = false, features = [
"serde", "serde",
] } ] }
tracing = { version = "0.1", default-features = false } tracing = { version = "0.1", default-features = false }
itertools = { version = "0.14.0", default-features = false }
[target.'cfg(target_os = "android")'.dependencies] [target.'cfg(target_os = "android")'.dependencies]
bevy_window = { path = "../bevy_window", version = "0.16.0-dev" } bevy_window = { path = "../bevy_window", version = "0.16.0-dev" }

View File

@ -2,6 +2,7 @@ use crate::io::AssetSourceId;
use alloc::{ use alloc::{
borrow::ToOwned, borrow::ToOwned,
string::{String, ToString}, string::{String, ToString},
vec::Vec,
}; };
use atomicow::CowArc; use atomicow::CowArc;
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize}; use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
@ -10,6 +11,7 @@ use core::{
hash::Hash, hash::Hash,
ops::Deref, ops::Deref,
}; };
use itertools::Itertools;
use serde::{de::Visitor, Deserialize, Serialize}; use serde::{de::Visitor, Deserialize, Serialize};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use thiserror::Error; use thiserror::Error;
@ -51,15 +53,34 @@ use thiserror::Error;
/// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and /// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and
/// clones internal owned [`AssetPaths`](AssetPath). /// clones internal owned [`AssetPaths`](AssetPath).
/// This also means that you should use [`AssetPath::parse`] in cases where `&str` is the explicit type. /// This also means that you should use [`AssetPath::parse`] in cases where `&str` is the explicit type.
#[derive(Eq, PartialEq, Hash, Clone, Default, Reflect)] #[derive(Eq, Hash, Clone, Default, Reflect)]
#[reflect(opaque)] #[reflect(opaque)]
#[reflect(Debug, PartialEq, Hash, Clone, Serialize, Deserialize)] #[reflect(Debug, PartialEq, Hash, Clone, Serialize, Deserialize)]
pub struct AssetPath<'a> { pub struct AssetPath<'a> {
source: AssetSourceId<'a>, source: AssetSourceId<'a>,
path: CowArc<'a, Path>, path: CowArc<'a, str>,
label: Option<CowArc<'a, str>>, label: Option<CowArc<'a, str>>,
} }
/// PartialEq needs to be derived manually for backwards compatibility.
/// As `path` used to be `std::path::Path`, equality was tricky with a trailing slash.
/// For example, "martin/stephan#dave" should be equal to "martin/stephan/#dave".
impl<'a> PartialEq for AssetPath<'a> {
fn eq(&self, other: &Self) -> bool {
let base_equality = self.source == other.source && self.label == other.label;
if !base_equality {
return false;
}
let self_trailing_slash = self.path.len() > 1
&& self.path.ends_with("/")
&& self.path[..self.path.len() - 1] == other.path[..other.path.len()];
let other_trailing_slash = other.path.len() > 1
&& other.path.ends_with("/")
&& self.path[..self.path.len()] == other.path[..other.path.len() - 1];
self.path == other.path || self_trailing_slash || other_trailing_slash
}
}
impl<'a> Debug for AssetPath<'a> { impl<'a> Debug for AssetPath<'a> {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Display::fmt(self, f) Display::fmt(self, f)
@ -71,7 +92,7 @@ impl<'a> Display for AssetPath<'a> {
if let AssetSourceId::Name(name) = self.source() { if let AssetSourceId::Name(name) = self.source() {
write!(f, "{name}://")?; write!(f, "{name}://")?;
} }
write!(f, "{}", self.path.display())?; f.write_str(self.path.as_ref())?;
if let Some(label) = &self.label { if let Some(label) = &self.label {
write!(f, "#{label}")?; write!(f, "#{label}")?;
} }
@ -137,7 +158,7 @@ impl<'a> AssetPath<'a> {
// Attempts to Parse a &str into an `AssetPath`'s `AssetPath::source`, `AssetPath::path`, and `AssetPath::label` components. // Attempts to Parse a &str into an `AssetPath`'s `AssetPath::source`, `AssetPath::path`, and `AssetPath::label` components.
fn parse_internal( fn parse_internal(
asset_path: &str, asset_path: &str,
) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> { ) -> Result<(Option<&str>, &str, Option<&str>), ParseAssetPathError> {
let chars = asset_path.char_indices(); let chars = asset_path.char_indices();
let mut source_range = None; let mut source_range = None;
let mut path_range = 0..asset_path.len(); let mut path_range = 0..asset_path.len();
@ -219,7 +240,7 @@ impl<'a> AssetPath<'a> {
None => None, None => None,
}; };
let path = Path::new(&asset_path[path_range]); let path = &asset_path[path_range];
Ok((source, path, label)) Ok((source, path, label))
} }
@ -237,7 +258,7 @@ impl<'a> AssetPath<'a> {
#[inline] #[inline]
pub fn from_path(path: &'a Path) -> AssetPath<'a> { pub fn from_path(path: &'a Path) -> AssetPath<'a> {
AssetPath { AssetPath {
path: CowArc::Borrowed(path), path: CowArc::Borrowed(path.to_str().unwrap()),
source: AssetSourceId::Default, source: AssetSourceId::Default,
label: None, label: None,
} }
@ -265,7 +286,7 @@ impl<'a> AssetPath<'a> {
/// Gets the path to the asset in the "virtual filesystem". /// Gets the path to the asset in the "virtual filesystem".
#[inline] #[inline]
pub fn path(&self) -> &Path { pub fn path(&self) -> &Path {
self.path.deref() Path::new(self.path.as_ref())
} }
/// Gets the path to the asset in the "virtual filesystem" without a label (if a label is currently set). /// Gets the path to the asset in the "virtual filesystem" without a label (if a label is currently set).
@ -312,17 +333,24 @@ impl<'a> AssetPath<'a> {
} }
} }
/// Splits the internal path into components
#[inline]
pub fn path_components(&self) -> impl Iterator<Item = &str> {
self.path.split('/')
}
/// Returns an [`AssetPath`] for the parent folder of this path, if there is a parent folder in the path. /// Returns an [`AssetPath`] for the parent folder of this path, if there is a parent folder in the path.
pub fn parent(&self) -> Option<AssetPath<'a>> { pub fn parent(&self) -> Option<AssetPath<'a>> {
let path = match &self.path { if self.path.as_ref() == "/" || self.path.starts_with('#') || self.path.is_empty() {
CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?), return None;
CowArc::Static(path) => CowArc::Static(path.parent()?), }
CowArc::Owned(path) => path.parent()?.to_path_buf().into(), let mut path: alloc::vec::Vec<_> = self.path_components().map(|s| s.to_string()).collect();
}; path.pop();
let path = path.join("/");
Some(AssetPath { Some(AssetPath {
source: self.source.clone(), source: self.source.clone(),
label: None, label: None,
path, path: CowArc::Owned(path.into()),
}) })
} }
@ -427,7 +455,7 @@ impl<'a> AssetPath<'a> {
} else { } else {
let (source, rpath, rlabel) = AssetPath::parse_internal(path)?; let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
let mut base_path = PathBuf::from(self.path()); let mut base_path = PathBuf::from(self.path());
if replace && !self.path.to_str().unwrap().ends_with('/') { if replace && !self.path.ends_with('/') {
// No error if base is empty (per RFC 1808). // No error if base is empty (per RFC 1808).
base_path.pop(); base_path.pop();
} }
@ -435,7 +463,7 @@ impl<'a> AssetPath<'a> {
// Strip off leading slash // Strip off leading slash
let mut is_absolute = false; let mut is_absolute = false;
let rpath = match rpath.strip_prefix("/") { let rpath = match rpath.strip_prefix("/") {
Ok(p) => { Some(p) => {
is_absolute = true; is_absolute = true;
p p
} }
@ -455,7 +483,7 @@ impl<'a> AssetPath<'a> {
Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())), Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
None => self.source.clone_owned(), None => self.source.clone_owned(),
}, },
path: CowArc::Owned(result_path.into()), path: CowArc::new_owned_from_arc(result_path.to_string_lossy()),
label: rlabel.map(|l| CowArc::Owned(l.into())), label: rlabel.map(|l| CowArc::Owned(l.into())),
}) })
} }
@ -516,18 +544,21 @@ impl<'a> AssetPath<'a> {
/// assert!(path.is_unapproved()); /// assert!(path.is_unapproved());
/// ``` /// ```
pub fn is_unapproved(&self) -> bool { pub fn is_unapproved(&self) -> bool {
use std::path::Component; if self.path.starts_with("/") {
let mut simplified = PathBuf::new(); return true;
for component in self.path.components() { }
let mut simplified = Vec::new();
for component in self.path_components() {
match component { match component {
Component::Prefix(_) | Component::RootDir => return true, "." => {}
Component::CurDir => {} ".." => {
Component::ParentDir => { if simplified.pop().is_none() {
if !simplified.pop() {
return true; return true;
} }
} }
Component::Normal(os_str) => simplified.push(os_str), _ => {
simplified.push(component);
}
} }
} }
@ -595,7 +626,7 @@ impl<'a> From<&'a Path> for AssetPath<'a> {
fn from(path: &'a Path) -> Self { fn from(path: &'a Path) -> Self {
Self { Self {
source: AssetSourceId::Default, source: AssetSourceId::Default,
path: CowArc::Borrowed(path), path: CowArc::from(path.to_string_lossy().to_string()),
label: None, label: None,
} }
} }
@ -606,7 +637,7 @@ impl From<PathBuf> for AssetPath<'static> {
fn from(path: PathBuf) -> Self { fn from(path: PathBuf) -> Self {
Self { Self {
source: AssetSourceId::Default, source: AssetSourceId::Default,
path: path.into(), path: CowArc::from(path.to_string_lossy().to_string()),
label: None, label: None,
} }
} }
@ -694,30 +725,24 @@ mod tests {
#[test] #[test]
fn parse_asset_path() { fn parse_asset_path() {
let result = AssetPath::parse_internal("a/b.test"); let result = AssetPath::parse_internal("a/b.test");
assert_eq!(result, Ok((None, Path::new("a/b.test"), None))); assert_eq!(result, Ok((None, "a/b.test", None)));
let result = AssetPath::parse_internal("http://a/b.test"); let result = AssetPath::parse_internal("http://a/b.test");
assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None))); assert_eq!(result, Ok((Some("http"), "a/b.test", None)));
let result = AssetPath::parse_internal("http://a/b.test#Foo"); let result = AssetPath::parse_internal("http://a/b.test#Foo");
assert_eq!( assert_eq!(result, Ok((Some("http"), "a/b.test", Some("Foo"))));
result,
Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
);
let result = AssetPath::parse_internal("localhost:80/b.test"); let result = AssetPath::parse_internal("localhost:80/b.test");
assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None))); assert_eq!(result, Ok((None, "localhost:80/b.test", None)));
let result = AssetPath::parse_internal("http://localhost:80/b.test"); let result = AssetPath::parse_internal("http://localhost:80/b.test");
assert_eq!( assert_eq!(result, Ok((Some("http"), "localhost:80/b.test", None)));
result,
Ok((Some("http"), Path::new("localhost:80/b.test"), None))
);
let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo"); let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
assert_eq!( assert_eq!(
result, result,
Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo"))) Ok((Some("http"), "localhost:80/b.test", Some("Foo")))
); );
let result = AssetPath::parse_internal("#insource://a/b.test"); let result = AssetPath::parse_internal("#insource://a/b.test");
@ -733,7 +758,7 @@ mod tests {
); );
let result = AssetPath::parse_internal("http://"); let result = AssetPath::parse_internal("http://");
assert_eq!(result, Ok((Some("http"), Path::new(""), None))); assert_eq!(result, Ok((Some("http"), "", None)));
let result = AssetPath::parse_internal("://x"); let result = AssetPath::parse_internal("://x");
assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource)); assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
@ -1036,4 +1061,10 @@ mod tests {
let result = AssetPath::from("asset.Custom"); let result = AssetPath::from("asset.Custom");
assert_eq!(result.get_full_extension(), Some("Custom".to_string())); assert_eq!(result.get_full_extension(), Some("Custom".to_string()));
} }
#[test]
fn test_trailing_slash_equality() {
assert_eq!(AssetPath::from("a/b/"), AssetPath::from("a/b"));
assert_eq!(AssetPath::from("a/b/#c"), AssetPath::from("a/b#c"));
}
} }