Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_asset/src/path.rs
6598 views
1
use crate::io::AssetSourceId;
2
use alloc::{
3
borrow::ToOwned,
4
string::{String, ToString},
5
};
6
use atomicow::CowArc;
7
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize};
8
use core::{
9
fmt::{Debug, Display},
10
hash::Hash,
11
ops::Deref,
12
};
13
use serde::{de::Visitor, Deserialize, Serialize};
14
use std::path::{Path, PathBuf};
15
use thiserror::Error;
16
17
/// Represents a path to an asset in a "virtual filesystem".
18
///
19
/// Asset paths consist of three main parts:
20
/// * [`AssetPath::source`]: The name of the [`AssetSource`](crate::io::AssetSource) to load the asset from.
21
/// This is optional. If one is not set the default source will be used (which is the `assets` folder by default).
22
/// * [`AssetPath::path`]: The "virtual filesystem path" pointing to an asset source file.
23
/// * [`AssetPath::label`]: An optional "named sub asset". When assets are loaded, they are
24
/// allowed to load "sub assets" of any type, which are identified by a named "label".
25
///
26
/// Asset paths are generally constructed (and visualized) as strings:
27
///
28
/// ```no_run
29
/// # use bevy_asset::{Asset, AssetServer, Handle};
30
/// # use bevy_reflect::TypePath;
31
/// #
32
/// # #[derive(Asset, TypePath, Default)]
33
/// # struct Mesh;
34
/// #
35
/// # #[derive(Asset, TypePath, Default)]
36
/// # struct Scene;
37
/// #
38
/// # let asset_server: AssetServer = panic!();
39
/// // This loads the `my_scene.scn` base asset from the default asset source.
40
/// let scene: Handle<Scene> = asset_server.load("my_scene.scn");
41
///
42
/// // This loads the `PlayerMesh` labeled asset from the `my_scene.scn` base asset in the default asset source.
43
/// let mesh: Handle<Mesh> = asset_server.load("my_scene.scn#PlayerMesh");
44
///
45
/// // This loads the `my_scene.scn` base asset from a custom 'remote' asset source.
46
/// let scene: Handle<Scene> = asset_server.load("remote://my_scene.scn");
47
/// ```
48
///
49
/// [`AssetPath`] implements [`From`] for `&'static str`, `&'static Path`, and `&'a String`,
50
/// which allows us to optimize the static cases.
51
/// This means that the common case of `asset_server.load("my_scene.scn")` when it creates and
52
/// clones internal owned [`AssetPaths`](AssetPath).
53
/// This also means that you should use [`AssetPath::parse`] in cases where `&str` is the explicit type.
54
#[derive(Eq, PartialEq, Hash, Clone, Default, Reflect)]
55
#[reflect(opaque)]
56
#[reflect(Debug, PartialEq, Hash, Clone, Serialize, Deserialize)]
57
pub struct AssetPath<'a> {
58
source: AssetSourceId<'a>,
59
path: CowArc<'a, Path>,
60
label: Option<CowArc<'a, str>>,
61
}
62
63
impl<'a> Debug for AssetPath<'a> {
64
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
65
Display::fmt(self, f)
66
}
67
}
68
69
impl<'a> Display for AssetPath<'a> {
70
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
71
if let AssetSourceId::Name(name) = self.source() {
72
write!(f, "{name}://")?;
73
}
74
write!(f, "{}", self.path.display())?;
75
if let Some(label) = &self.label {
76
write!(f, "#{label}")?;
77
}
78
Ok(())
79
}
80
}
81
82
/// An error that occurs when parsing a string type to create an [`AssetPath`] fails, such as during [`AssetPath::parse`].
83
#[derive(Error, Debug, PartialEq, Eq)]
84
pub enum ParseAssetPathError {
85
/// Error that occurs when the [`AssetPath::source`] section of a path string contains the [`AssetPath::label`] delimiter `#`. E.g. `bad#source://file.test`.
86
#[error("Asset source must not contain a `#` character")]
87
InvalidSourceSyntax,
88
/// Error that occurs when the [`AssetPath::label`] section of a path string contains the [`AssetPath::source`] delimiter `://`. E.g. `source://file.test#bad://label`.
89
#[error("Asset label must not contain a `://` substring")]
90
InvalidLabelSyntax,
91
/// Error that occurs when a path string has an [`AssetPath::source`] delimiter `://` with no characters preceding it. E.g. `://file.test`.
92
#[error("Asset source must be at least one character. Either specify the source before the '://' or remove the `://`")]
93
MissingSource,
94
/// Error that occurs when a path string has an [`AssetPath::label`] delimiter `#` with no characters succeeding it. E.g. `file.test#`
95
#[error("Asset label must be at least one character. Either specify the label after the '#' or remove the '#'")]
96
MissingLabel,
97
}
98
99
impl<'a> AssetPath<'a> {
100
/// Creates a new [`AssetPath`] from a string in the asset path format:
101
/// * An asset at the root: `"scene.gltf"`
102
/// * An asset nested in some folders: `"some/path/scene.gltf"`
103
/// * An asset with a "label": `"some/path/scene.gltf#Mesh0"`
104
/// * An asset with a custom "source": `"custom://some/path/scene.gltf#Mesh0"`
105
///
106
/// Prefer [`From<'static str>`] for static strings, as this will prevent allocations
107
/// and reference counting for [`AssetPath::into_owned`].
108
///
109
/// # Panics
110
/// Panics if the asset path is in an invalid format. Use [`AssetPath::try_parse`] for a fallible variant
111
pub fn parse(asset_path: &'a str) -> AssetPath<'a> {
112
Self::try_parse(asset_path).unwrap()
113
}
114
115
/// Creates a new [`AssetPath`] from a string in the asset path format:
116
/// * An asset at the root: `"scene.gltf"`
117
/// * An asset nested in some folders: `"some/path/scene.gltf"`
118
/// * An asset with a "label": `"some/path/scene.gltf#Mesh0"`
119
/// * An asset with a custom "source": `"custom://some/path/scene.gltf#Mesh0"`
120
///
121
/// Prefer [`From<'static str>`] for static strings, as this will prevent allocations
122
/// and reference counting for [`AssetPath::into_owned`].
123
///
124
/// This will return a [`ParseAssetPathError`] if `asset_path` is in an invalid format.
125
pub fn try_parse(asset_path: &'a str) -> Result<AssetPath<'a>, ParseAssetPathError> {
126
let (source, path, label) = Self::parse_internal(asset_path)?;
127
Ok(Self {
128
source: match source {
129
Some(source) => AssetSourceId::Name(CowArc::Borrowed(source)),
130
None => AssetSourceId::Default,
131
},
132
path: CowArc::Borrowed(path),
133
label: label.map(CowArc::Borrowed),
134
})
135
}
136
137
// Attempts to Parse a &str into an `AssetPath`'s `AssetPath::source`, `AssetPath::path`, and `AssetPath::label` components.
138
fn parse_internal(
139
asset_path: &str,
140
) -> Result<(Option<&str>, &Path, Option<&str>), ParseAssetPathError> {
141
let chars = asset_path.char_indices();
142
let mut source_range = None;
143
let mut path_range = 0..asset_path.len();
144
let mut label_range = None;
145
146
// Loop through the characters of the passed in &str to accomplish the following:
147
// 1. Search for the first instance of the `://` substring. If the `://` substring is found,
148
// store the range of indices representing everything before the `://` substring as the `source_range`.
149
// 2. Search for the last instance of the `#` character. If the `#` character is found,
150
// store the range of indices representing everything after the `#` character as the `label_range`
151
// 3. Set the `path_range` to be everything in between the `source_range` and `label_range`,
152
// excluding the `://` substring and `#` character.
153
// 4. Verify that there are no `#` characters in the `AssetPath::source` and no `://` substrings in the `AssetPath::label`
154
let mut source_delimiter_chars_matched = 0;
155
let mut last_found_source_index = 0;
156
for (index, char) in chars {
157
match char {
158
':' => {
159
source_delimiter_chars_matched = 1;
160
}
161
'/' => {
162
match source_delimiter_chars_matched {
163
1 => {
164
source_delimiter_chars_matched = 2;
165
}
166
2 => {
167
// If we haven't found our first `AssetPath::source` yet, check to make sure it is valid and then store it.
168
if source_range.is_none() {
169
// If the `AssetPath::source` contains a `#` character, it is invalid.
170
if label_range.is_some() {
171
return Err(ParseAssetPathError::InvalidSourceSyntax);
172
}
173
source_range = Some(0..index - 2);
174
path_range.start = index + 1;
175
}
176
last_found_source_index = index - 2;
177
source_delimiter_chars_matched = 0;
178
}
179
_ => {}
180
}
181
}
182
'#' => {
183
path_range.end = index;
184
label_range = Some(index + 1..asset_path.len());
185
source_delimiter_chars_matched = 0;
186
}
187
_ => {
188
source_delimiter_chars_matched = 0;
189
}
190
}
191
}
192
// If we found an `AssetPath::label`
193
if let Some(range) = label_range.clone() {
194
// If the `AssetPath::label` contained a `://` substring, it is invalid.
195
if range.start <= last_found_source_index {
196
return Err(ParseAssetPathError::InvalidLabelSyntax);
197
}
198
}
199
// Try to parse the range of indices that represents the `AssetPath::source` portion of the `AssetPath` to make sure it is not empty.
200
// This would be the case if the input &str was something like `://some/file.test`
201
let source = match source_range {
202
Some(source_range) => {
203
if source_range.is_empty() {
204
return Err(ParseAssetPathError::MissingSource);
205
}
206
Some(&asset_path[source_range])
207
}
208
None => None,
209
};
210
// Try to parse the range of indices that represents the `AssetPath::label` portion of the `AssetPath` to make sure it is not empty.
211
// This would be the case if the input &str was something like `some/file.test#`.
212
let label = match label_range {
213
Some(label_range) => {
214
if label_range.is_empty() {
215
return Err(ParseAssetPathError::MissingLabel);
216
}
217
Some(&asset_path[label_range])
218
}
219
None => None,
220
};
221
222
let path = Path::new(&asset_path[path_range]);
223
Ok((source, path, label))
224
}
225
226
/// Creates a new [`AssetPath`] from a [`PathBuf`].
227
#[inline]
228
pub fn from_path_buf(path_buf: PathBuf) -> AssetPath<'a> {
229
AssetPath {
230
path: CowArc::Owned(path_buf.into()),
231
source: AssetSourceId::Default,
232
label: None,
233
}
234
}
235
236
/// Creates a new [`AssetPath`] from a [`Path`].
237
#[inline]
238
pub fn from_path(path: &'a Path) -> AssetPath<'a> {
239
AssetPath {
240
path: CowArc::Borrowed(path),
241
source: AssetSourceId::Default,
242
label: None,
243
}
244
}
245
246
/// Gets the "asset source", if one was defined. If none was defined, the default source
247
/// will be used.
248
#[inline]
249
pub fn source(&self) -> &AssetSourceId<'_> {
250
&self.source
251
}
252
253
/// Gets the "sub-asset label".
254
#[inline]
255
pub fn label(&self) -> Option<&str> {
256
self.label.as_deref()
257
}
258
259
/// Gets the "sub-asset label".
260
#[inline]
261
pub fn label_cow(&self) -> Option<CowArc<'a, str>> {
262
self.label.clone()
263
}
264
265
/// Gets the path to the asset in the "virtual filesystem".
266
#[inline]
267
pub fn path(&self) -> &Path {
268
self.path.deref()
269
}
270
271
/// Gets the path to the asset in the "virtual filesystem" without a label (if a label is currently set).
272
#[inline]
273
pub fn without_label(&self) -> AssetPath<'_> {
274
Self {
275
source: self.source.clone(),
276
path: self.path.clone(),
277
label: None,
278
}
279
}
280
281
/// Removes a "sub-asset label" from this [`AssetPath`], if one was set.
282
#[inline]
283
pub fn remove_label(&mut self) {
284
self.label = None;
285
}
286
287
/// Takes the "sub-asset label" from this [`AssetPath`], if one was set.
288
#[inline]
289
pub fn take_label(&mut self) -> Option<CowArc<'a, str>> {
290
self.label.take()
291
}
292
293
/// Returns this asset path with the given label. This will replace the previous
294
/// label if it exists.
295
#[inline]
296
pub fn with_label(self, label: impl Into<CowArc<'a, str>>) -> AssetPath<'a> {
297
AssetPath {
298
source: self.source,
299
path: self.path,
300
label: Some(label.into()),
301
}
302
}
303
304
/// Returns this asset path with the given asset source. This will replace the previous asset
305
/// source if it exists.
306
#[inline]
307
pub fn with_source(self, source: impl Into<AssetSourceId<'a>>) -> AssetPath<'a> {
308
AssetPath {
309
source: source.into(),
310
path: self.path,
311
label: self.label,
312
}
313
}
314
315
/// Returns an [`AssetPath`] for the parent folder of this path, if there is a parent folder in the path.
316
pub fn parent(&self) -> Option<AssetPath<'a>> {
317
let path = match &self.path {
318
CowArc::Borrowed(path) => CowArc::Borrowed(path.parent()?),
319
CowArc::Static(path) => CowArc::Static(path.parent()?),
320
CowArc::Owned(path) => path.parent()?.to_path_buf().into(),
321
};
322
Some(AssetPath {
323
source: self.source.clone(),
324
label: None,
325
path,
326
})
327
}
328
329
/// Converts this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]".
330
/// If internally a value is a static reference, the static reference will be used unchanged.
331
/// If internally a value is an "owned [`Arc`]", it will remain unchanged.
332
///
333
/// [`Arc`]: alloc::sync::Arc
334
pub fn into_owned(self) -> AssetPath<'static> {
335
AssetPath {
336
source: self.source.into_owned(),
337
path: self.path.into_owned(),
338
label: self.label.map(CowArc::into_owned),
339
}
340
}
341
342
/// Clones this into an "owned" value. If internally a value is borrowed, it will be cloned into an "owned [`Arc`]".
343
/// If internally a value is a static reference, the static reference will be used unchanged.
344
/// If internally a value is an "owned [`Arc`]", the [`Arc`] will be cloned.
345
///
346
/// [`Arc`]: alloc::sync::Arc
347
#[inline]
348
pub fn clone_owned(&self) -> AssetPath<'static> {
349
self.clone().into_owned()
350
}
351
352
/// Resolves a relative asset path via concatenation. The result will be an `AssetPath` which
353
/// is resolved relative to this "base" path.
354
///
355
/// ```
356
/// # use bevy_asset::AssetPath;
357
/// assert_eq!(AssetPath::parse("a/b").resolve("c"), Ok(AssetPath::parse("a/b/c")));
358
/// assert_eq!(AssetPath::parse("a/b").resolve("./c"), Ok(AssetPath::parse("a/b/c")));
359
/// assert_eq!(AssetPath::parse("a/b").resolve("../c"), Ok(AssetPath::parse("a/c")));
360
/// assert_eq!(AssetPath::parse("a/b").resolve("c.png"), Ok(AssetPath::parse("a/b/c.png")));
361
/// assert_eq!(AssetPath::parse("a/b").resolve("/c"), Ok(AssetPath::parse("c")));
362
/// assert_eq!(AssetPath::parse("a/b.png").resolve("#c"), Ok(AssetPath::parse("a/b.png#c")));
363
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve("#d"), Ok(AssetPath::parse("a/b.png#d")));
364
/// ```
365
///
366
/// There are several cases:
367
///
368
/// If the `path` argument begins with `#`, then it is considered an asset label, in which case
369
/// the result is the base path with the label portion replaced.
370
///
371
/// If the path argument begins with '/', then it is considered a 'full' path, in which
372
/// case the result is a new `AssetPath` consisting of the base path asset source
373
/// (if there is one) with the path and label portions of the relative path. Note that a 'full'
374
/// asset path is still relative to the asset source root, and not necessarily an absolute
375
/// filesystem path.
376
///
377
/// If the `path` argument begins with an asset source (ex: `http://`) then the entire base
378
/// path is replaced - the result is the source, path and label (if any) of the `path`
379
/// argument.
380
///
381
/// Otherwise, the `path` argument is considered a relative path. The result is concatenated
382
/// using the following algorithm:
383
///
384
/// * The base path and the `path` argument are concatenated.
385
/// * Path elements consisting of "/." or "&lt;name&gt;/.." are removed.
386
///
387
/// If there are insufficient segments in the base path to match the ".." segments,
388
/// then any left-over ".." segments are left as-is.
389
pub fn resolve(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
390
self.resolve_internal(path, false)
391
}
392
393
/// Resolves an embedded asset path via concatenation. The result will be an `AssetPath` which
394
/// is resolved relative to this path. This is similar in operation to `resolve`, except that
395
/// the 'file' portion of the base path (that is, any characters after the last '/')
396
/// is removed before concatenation, in accordance with the behavior specified in
397
/// IETF RFC 1808 "Relative URIs".
398
///
399
/// The reason for this behavior is that embedded URIs which start with "./" or "../" are
400
/// relative to the *directory* containing the asset, not the asset file. This is consistent
401
/// with the behavior of URIs in `JavaScript`, CSS, HTML and other web file formats. The
402
/// primary use case for this method is resolving relative paths embedded within asset files,
403
/// which are relative to the asset in which they are contained.
404
///
405
/// ```
406
/// # use bevy_asset::AssetPath;
407
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c"), Ok(AssetPath::parse("a/c")));
408
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("./c"), Ok(AssetPath::parse("a/c")));
409
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("../c"), Ok(AssetPath::parse("c")));
410
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("c.png"), Ok(AssetPath::parse("a/c.png")));
411
/// assert_eq!(AssetPath::parse("a/b").resolve_embed("/c"), Ok(AssetPath::parse("c")));
412
/// assert_eq!(AssetPath::parse("a/b.png").resolve_embed("#c"), Ok(AssetPath::parse("a/b.png#c")));
413
/// assert_eq!(AssetPath::parse("a/b.png#c").resolve_embed("#d"), Ok(AssetPath::parse("a/b.png#d")));
414
/// ```
415
pub fn resolve_embed(&self, path: &str) -> Result<AssetPath<'static>, ParseAssetPathError> {
416
self.resolve_internal(path, true)
417
}
418
419
fn resolve_internal(
420
&self,
421
path: &str,
422
replace: bool,
423
) -> Result<AssetPath<'static>, ParseAssetPathError> {
424
if let Some(label) = path.strip_prefix('#') {
425
// It's a label only
426
Ok(self.clone_owned().with_label(label.to_owned()))
427
} else {
428
let (source, rpath, rlabel) = AssetPath::parse_internal(path)?;
429
let mut base_path = PathBuf::from(self.path());
430
if replace && !self.path.to_str().unwrap().ends_with('/') {
431
// No error if base is empty (per RFC 1808).
432
base_path.pop();
433
}
434
435
// Strip off leading slash
436
let mut is_absolute = false;
437
let rpath = match rpath.strip_prefix("/") {
438
Ok(p) => {
439
is_absolute = true;
440
p
441
}
442
_ => rpath,
443
};
444
445
let mut result_path = if !is_absolute && source.is_none() {
446
base_path
447
} else {
448
PathBuf::new()
449
};
450
result_path.push(rpath);
451
result_path = normalize_path(result_path.as_path());
452
453
Ok(AssetPath {
454
source: match source {
455
Some(source) => AssetSourceId::Name(CowArc::Owned(source.into())),
456
None => self.source.clone_owned(),
457
},
458
path: CowArc::Owned(result_path.into()),
459
label: rlabel.map(|l| CowArc::Owned(l.into())),
460
})
461
}
462
}
463
464
/// Returns the full extension (including multiple '.' values).
465
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
466
///
467
/// Also strips out anything following a `?` to handle query parameters in URIs
468
pub fn get_full_extension(&self) -> Option<String> {
469
let file_name = self.path().file_name()?.to_str()?;
470
let index = file_name.find('.')?;
471
let mut extension = file_name[index + 1..].to_owned();
472
473
// Strip off any query parameters
474
let query = extension.find('?');
475
if let Some(offset) = query {
476
extension.truncate(offset);
477
}
478
479
Some(extension)
480
}
481
482
pub(crate) fn iter_secondary_extensions(full_extension: &str) -> impl Iterator<Item = &str> {
483
full_extension.char_indices().filter_map(|(i, c)| {
484
if c == '.' {
485
Some(&full_extension[i + 1..])
486
} else {
487
None
488
}
489
})
490
}
491
492
/// Returns `true` if this [`AssetPath`] points to a file that is
493
/// outside of its [`AssetSource`](crate::io::AssetSource) folder.
494
///
495
/// ## Example
496
/// ```
497
/// # use bevy_asset::AssetPath;
498
/// // Inside the default AssetSource.
499
/// let path = AssetPath::parse("thingy.png");
500
/// assert!( ! path.is_unapproved());
501
/// let path = AssetPath::parse("gui/thingy.png");
502
/// assert!( ! path.is_unapproved());
503
///
504
/// // Inside a different AssetSource.
505
/// let path = AssetPath::parse("embedded://thingy.png");
506
/// assert!( ! path.is_unapproved());
507
///
508
/// // Exits the `AssetSource`s directory.
509
/// let path = AssetPath::parse("../thingy.png");
510
/// assert!(path.is_unapproved());
511
/// let path = AssetPath::parse("folder/../../thingy.png");
512
/// assert!(path.is_unapproved());
513
///
514
/// // This references the linux root directory.
515
/// let path = AssetPath::parse("/home/thingy.png");
516
/// assert!(path.is_unapproved());
517
/// ```
518
pub fn is_unapproved(&self) -> bool {
519
use std::path::Component;
520
let mut simplified = PathBuf::new();
521
for component in self.path.components() {
522
match component {
523
Component::Prefix(_) | Component::RootDir => return true,
524
Component::CurDir => {}
525
Component::ParentDir => {
526
if !simplified.pop() {
527
return true;
528
}
529
}
530
Component::Normal(os_str) => simplified.push(os_str),
531
}
532
}
533
534
false
535
}
536
}
537
538
// This is only implemented for static lifetimes to ensure `Path::clone` does not allocate
539
// by ensuring that this is stored as a `CowArc::Static`.
540
// Please read https://github.com/bevyengine/bevy/issues/19844 before changing this!
541
impl From<&'static str> for AssetPath<'static> {
542
#[inline]
543
fn from(asset_path: &'static str) -> Self {
544
let (source, path, label) = Self::parse_internal(asset_path).unwrap();
545
AssetPath {
546
source: source.into(),
547
path: CowArc::Static(path),
548
label: label.map(CowArc::Static),
549
}
550
}
551
}
552
553
impl<'a> From<&'a String> for AssetPath<'a> {
554
#[inline]
555
fn from(asset_path: &'a String) -> Self {
556
AssetPath::parse(asset_path.as_str())
557
}
558
}
559
560
impl From<String> for AssetPath<'static> {
561
#[inline]
562
fn from(asset_path: String) -> Self {
563
AssetPath::parse(asset_path.as_str()).into_owned()
564
}
565
}
566
567
impl From<&'static Path> for AssetPath<'static> {
568
#[inline]
569
fn from(path: &'static Path) -> Self {
570
Self {
571
source: AssetSourceId::Default,
572
path: CowArc::Static(path),
573
label: None,
574
}
575
}
576
}
577
578
impl From<PathBuf> for AssetPath<'static> {
579
#[inline]
580
fn from(path: PathBuf) -> Self {
581
Self {
582
source: AssetSourceId::Default,
583
path: path.into(),
584
label: None,
585
}
586
}
587
}
588
589
impl<'a, 'b> From<&'a AssetPath<'b>> for AssetPath<'b> {
590
fn from(value: &'a AssetPath<'b>) -> Self {
591
value.clone()
592
}
593
}
594
595
impl<'a> From<AssetPath<'a>> for PathBuf {
596
fn from(value: AssetPath<'a>) -> Self {
597
value.path().to_path_buf()
598
}
599
}
600
601
impl<'a> Serialize for AssetPath<'a> {
602
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
603
where
604
S: serde::Serializer,
605
{
606
self.to_string().serialize(serializer)
607
}
608
}
609
610
impl<'de> Deserialize<'de> for AssetPath<'static> {
611
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
612
where
613
D: serde::Deserializer<'de>,
614
{
615
deserializer.deserialize_string(AssetPathVisitor)
616
}
617
}
618
619
struct AssetPathVisitor;
620
621
impl<'de> Visitor<'de> for AssetPathVisitor {
622
type Value = AssetPath<'static>;
623
624
fn expecting(&self, formatter: &mut core::fmt::Formatter) -> core::fmt::Result {
625
formatter.write_str("string AssetPath")
626
}
627
628
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
629
where
630
E: serde::de::Error,
631
{
632
Ok(AssetPath::parse(v).into_owned())
633
}
634
635
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
636
where
637
E: serde::de::Error,
638
{
639
Ok(AssetPath::from(v))
640
}
641
}
642
643
/// Normalizes the path by collapsing all occurrences of '.' and '..' dot-segments where possible
644
/// as per [RFC 1808](https://datatracker.ietf.org/doc/html/rfc1808)
645
pub(crate) fn normalize_path(path: &Path) -> PathBuf {
646
let mut result_path = PathBuf::new();
647
for elt in path.iter() {
648
if elt == "." {
649
// Skip
650
} else if elt == ".." {
651
if !result_path.pop() {
652
// Preserve ".." if insufficient matches (per RFC 1808).
653
result_path.push(elt);
654
}
655
} else {
656
result_path.push(elt);
657
}
658
}
659
result_path
660
}
661
662
#[cfg(test)]
663
mod tests {
664
use crate::AssetPath;
665
use alloc::string::ToString;
666
use std::path::Path;
667
668
#[test]
669
fn parse_asset_path() {
670
let result = AssetPath::parse_internal("a/b.test");
671
assert_eq!(result, Ok((None, Path::new("a/b.test"), None)));
672
673
let result = AssetPath::parse_internal("http://a/b.test");
674
assert_eq!(result, Ok((Some("http"), Path::new("a/b.test"), None)));
675
676
let result = AssetPath::parse_internal("http://a/b.test#Foo");
677
assert_eq!(
678
result,
679
Ok((Some("http"), Path::new("a/b.test"), Some("Foo")))
680
);
681
682
let result = AssetPath::parse_internal("localhost:80/b.test");
683
assert_eq!(result, Ok((None, Path::new("localhost:80/b.test"), None)));
684
685
let result = AssetPath::parse_internal("http://localhost:80/b.test");
686
assert_eq!(
687
result,
688
Ok((Some("http"), Path::new("localhost:80/b.test"), None))
689
);
690
691
let result = AssetPath::parse_internal("http://localhost:80/b.test#Foo");
692
assert_eq!(
693
result,
694
Ok((Some("http"), Path::new("localhost:80/b.test"), Some("Foo")))
695
);
696
697
let result = AssetPath::parse_internal("#insource://a/b.test");
698
assert_eq!(result, Err(crate::ParseAssetPathError::InvalidSourceSyntax));
699
700
let result = AssetPath::parse_internal("source://a/b.test#://inlabel");
701
assert_eq!(result, Err(crate::ParseAssetPathError::InvalidLabelSyntax));
702
703
let result = AssetPath::parse_internal("#insource://a/b.test#://inlabel");
704
assert!(
705
result == Err(crate::ParseAssetPathError::InvalidSourceSyntax)
706
|| result == Err(crate::ParseAssetPathError::InvalidLabelSyntax)
707
);
708
709
let result = AssetPath::parse_internal("http://");
710
assert_eq!(result, Ok((Some("http"), Path::new(""), None)));
711
712
let result = AssetPath::parse_internal("://x");
713
assert_eq!(result, Err(crate::ParseAssetPathError::MissingSource));
714
715
let result = AssetPath::parse_internal("a/b.test#");
716
assert_eq!(result, Err(crate::ParseAssetPathError::MissingLabel));
717
}
718
719
#[test]
720
fn test_parent() {
721
// Parent consumes path segments, returns None when insufficient
722
let result = AssetPath::from("a/b.test");
723
assert_eq!(result.parent(), Some(AssetPath::from("a")));
724
assert_eq!(result.parent().unwrap().parent(), Some(AssetPath::from("")));
725
assert_eq!(result.parent().unwrap().parent().unwrap().parent(), None);
726
727
// Parent cannot consume asset source
728
let result = AssetPath::from("http://a");
729
assert_eq!(result.parent(), Some(AssetPath::from("http://")));
730
assert_eq!(result.parent().unwrap().parent(), None);
731
732
// Parent consumes labels
733
let result = AssetPath::from("http://a#Foo");
734
assert_eq!(result.parent(), Some(AssetPath::from("http://")));
735
}
736
737
#[test]
738
fn test_with_source() {
739
let result = AssetPath::from("http://a#Foo");
740
assert_eq!(result.with_source("ftp"), AssetPath::from("ftp://a#Foo"));
741
}
742
743
#[test]
744
fn test_without_label() {
745
let result = AssetPath::from("http://a#Foo");
746
assert_eq!(result.without_label(), AssetPath::from("http://a"));
747
}
748
749
#[test]
750
fn test_resolve_full() {
751
// A "full" path should ignore the base path.
752
let base = AssetPath::from("alice/bob#carol");
753
assert_eq!(
754
base.resolve("/joe/next").unwrap(),
755
AssetPath::from("joe/next")
756
);
757
assert_eq!(
758
base.resolve_embed("/joe/next").unwrap(),
759
AssetPath::from("joe/next")
760
);
761
assert_eq!(
762
base.resolve("/joe/next#dave").unwrap(),
763
AssetPath::from("joe/next#dave")
764
);
765
assert_eq!(
766
base.resolve_embed("/joe/next#dave").unwrap(),
767
AssetPath::from("joe/next#dave")
768
);
769
}
770
771
#[test]
772
fn test_resolve_implicit_relative() {
773
// A path with no initial directory separator should be considered relative.
774
let base = AssetPath::from("alice/bob#carol");
775
assert_eq!(
776
base.resolve("joe/next").unwrap(),
777
AssetPath::from("alice/bob/joe/next")
778
);
779
assert_eq!(
780
base.resolve_embed("joe/next").unwrap(),
781
AssetPath::from("alice/joe/next")
782
);
783
assert_eq!(
784
base.resolve("joe/next#dave").unwrap(),
785
AssetPath::from("alice/bob/joe/next#dave")
786
);
787
assert_eq!(
788
base.resolve_embed("joe/next#dave").unwrap(),
789
AssetPath::from("alice/joe/next#dave")
790
);
791
}
792
793
#[test]
794
fn test_resolve_explicit_relative() {
795
// A path which begins with "./" or "../" is treated as relative
796
let base = AssetPath::from("alice/bob#carol");
797
assert_eq!(
798
base.resolve("./martin#dave").unwrap(),
799
AssetPath::from("alice/bob/martin#dave")
800
);
801
assert_eq!(
802
base.resolve_embed("./martin#dave").unwrap(),
803
AssetPath::from("alice/martin#dave")
804
);
805
assert_eq!(
806
base.resolve("../martin#dave").unwrap(),
807
AssetPath::from("alice/martin#dave")
808
);
809
assert_eq!(
810
base.resolve_embed("../martin#dave").unwrap(),
811
AssetPath::from("martin#dave")
812
);
813
}
814
815
#[test]
816
fn test_resolve_trailing_slash() {
817
// A path which begins with "./" or "../" is treated as relative
818
let base = AssetPath::from("alice/bob/");
819
assert_eq!(
820
base.resolve("./martin#dave").unwrap(),
821
AssetPath::from("alice/bob/martin#dave")
822
);
823
assert_eq!(
824
base.resolve_embed("./martin#dave").unwrap(),
825
AssetPath::from("alice/bob/martin#dave")
826
);
827
assert_eq!(
828
base.resolve("../martin#dave").unwrap(),
829
AssetPath::from("alice/martin#dave")
830
);
831
assert_eq!(
832
base.resolve_embed("../martin#dave").unwrap(),
833
AssetPath::from("alice/martin#dave")
834
);
835
}
836
837
#[test]
838
fn test_resolve_canonicalize() {
839
// Test that ".." and "." are removed after concatenation.
840
let base = AssetPath::from("alice/bob#carol");
841
assert_eq!(
842
base.resolve("./martin/stephan/..#dave").unwrap(),
843
AssetPath::from("alice/bob/martin#dave")
844
);
845
assert_eq!(
846
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
847
AssetPath::from("alice/martin#dave")
848
);
849
assert_eq!(
850
base.resolve("../martin/.#dave").unwrap(),
851
AssetPath::from("alice/martin#dave")
852
);
853
assert_eq!(
854
base.resolve_embed("../martin/.#dave").unwrap(),
855
AssetPath::from("martin#dave")
856
);
857
assert_eq!(
858
base.resolve("/martin/stephan/..#dave").unwrap(),
859
AssetPath::from("martin#dave")
860
);
861
assert_eq!(
862
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
863
AssetPath::from("martin#dave")
864
);
865
}
866
867
#[test]
868
fn test_resolve_canonicalize_base() {
869
// Test that ".." and "." are removed after concatenation even from the base path.
870
let base = AssetPath::from("alice/../bob#carol");
871
assert_eq!(
872
base.resolve("./martin/stephan/..#dave").unwrap(),
873
AssetPath::from("bob/martin#dave")
874
);
875
assert_eq!(
876
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
877
AssetPath::from("martin#dave")
878
);
879
assert_eq!(
880
base.resolve("../martin/.#dave").unwrap(),
881
AssetPath::from("martin#dave")
882
);
883
assert_eq!(
884
base.resolve_embed("../martin/.#dave").unwrap(),
885
AssetPath::from("../martin#dave")
886
);
887
assert_eq!(
888
base.resolve("/martin/stephan/..#dave").unwrap(),
889
AssetPath::from("martin#dave")
890
);
891
assert_eq!(
892
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
893
AssetPath::from("martin#dave")
894
);
895
}
896
897
#[test]
898
fn test_resolve_canonicalize_with_source() {
899
// Test that ".." and "." are removed after concatenation.
900
let base = AssetPath::from("source://alice/bob#carol");
901
assert_eq!(
902
base.resolve("./martin/stephan/..#dave").unwrap(),
903
AssetPath::from("source://alice/bob/martin#dave")
904
);
905
assert_eq!(
906
base.resolve_embed("./martin/stephan/..#dave").unwrap(),
907
AssetPath::from("source://alice/martin#dave")
908
);
909
assert_eq!(
910
base.resolve("../martin/.#dave").unwrap(),
911
AssetPath::from("source://alice/martin#dave")
912
);
913
assert_eq!(
914
base.resolve_embed("../martin/.#dave").unwrap(),
915
AssetPath::from("source://martin#dave")
916
);
917
assert_eq!(
918
base.resolve("/martin/stephan/..#dave").unwrap(),
919
AssetPath::from("source://martin#dave")
920
);
921
assert_eq!(
922
base.resolve_embed("/martin/stephan/..#dave").unwrap(),
923
AssetPath::from("source://martin#dave")
924
);
925
}
926
927
#[test]
928
fn test_resolve_absolute() {
929
// Paths beginning with '/' replace the base path
930
let base = AssetPath::from("alice/bob#carol");
931
assert_eq!(
932
base.resolve("/martin/stephan").unwrap(),
933
AssetPath::from("martin/stephan")
934
);
935
assert_eq!(
936
base.resolve_embed("/martin/stephan").unwrap(),
937
AssetPath::from("martin/stephan")
938
);
939
assert_eq!(
940
base.resolve("/martin/stephan#dave").unwrap(),
941
AssetPath::from("martin/stephan/#dave")
942
);
943
assert_eq!(
944
base.resolve_embed("/martin/stephan#dave").unwrap(),
945
AssetPath::from("martin/stephan/#dave")
946
);
947
}
948
949
#[test]
950
fn test_resolve_asset_source() {
951
// Paths beginning with 'source://' replace the base path
952
let base = AssetPath::from("alice/bob#carol");
953
assert_eq!(
954
base.resolve("source://martin/stephan").unwrap(),
955
AssetPath::from("source://martin/stephan")
956
);
957
assert_eq!(
958
base.resolve_embed("source://martin/stephan").unwrap(),
959
AssetPath::from("source://martin/stephan")
960
);
961
assert_eq!(
962
base.resolve("source://martin/stephan#dave").unwrap(),
963
AssetPath::from("source://martin/stephan/#dave")
964
);
965
assert_eq!(
966
base.resolve_embed("source://martin/stephan#dave").unwrap(),
967
AssetPath::from("source://martin/stephan/#dave")
968
);
969
}
970
971
#[test]
972
fn test_resolve_label() {
973
// A relative path with only a label should replace the label portion
974
let base = AssetPath::from("alice/bob#carol");
975
assert_eq!(
976
base.resolve("#dave").unwrap(),
977
AssetPath::from("alice/bob#dave")
978
);
979
assert_eq!(
980
base.resolve_embed("#dave").unwrap(),
981
AssetPath::from("alice/bob#dave")
982
);
983
}
984
985
#[test]
986
fn test_resolve_insufficient_elements() {
987
// Ensure that ".." segments are preserved if there are insufficient elements to remove them.
988
let base = AssetPath::from("alice/bob#carol");
989
assert_eq!(
990
base.resolve("../../joe/next").unwrap(),
991
AssetPath::from("joe/next")
992
);
993
assert_eq!(
994
base.resolve_embed("../../joe/next").unwrap(),
995
AssetPath::from("../joe/next")
996
);
997
}
998
999
#[test]
1000
fn test_get_extension() {
1001
let result = AssetPath::from("http://a.tar.gz#Foo");
1002
assert_eq!(result.get_full_extension(), Some("tar.gz".to_string()));
1003
1004
let result = AssetPath::from("http://a#Foo");
1005
assert_eq!(result.get_full_extension(), None);
1006
1007
let result = AssetPath::from("http://a.tar.bz2?foo=bar#Baz");
1008
assert_eq!(result.get_full_extension(), Some("tar.bz2".to_string()));
1009
1010
let result = AssetPath::from("asset.Custom");
1011
assert_eq!(result.get_full_extension(), Some("Custom".to_string()));
1012
}
1013
}
1014
1015