@@ -299,6 +299,139 @@ impl<'a> AssetPath<'a> {
299
299
self . clone ( ) . into_owned ( )
300
300
}
301
301
302
+ /// Resolves a relative asset path via concatenation. The result will be an `AssetPath` which
303
+ /// is resolved relative to this "base" path.
304
+ ///
305
+ /// ```rust
306
+ /// # use bevy_asset::AssetPath;
307
+ /// assert_eq!(AssetPath::parse("a/b").resolve("c"), Ok(AssetPath::parse("a/b/c")));
308
+ /// assert_eq!(AssetPath::parse("a/b").resolve("./c"), Ok(AssetPath::parse("a/b/c")));
309
+ /// assert_eq!(AssetPath::parse("a/b").resolve("../c"), Ok(AssetPath::parse("a/c")));
310
+ /// assert_eq!(AssetPath::parse("a/b").resolve("c.png"), Ok(AssetPath::parse("a/b/c.png")));
311
+ /// assert_eq!(AssetPath::parse("a/b").resolve("/c"), Ok(AssetPath::parse("c")));
312
+ /// assert_eq!(AssetPath::parse("a/b.png").resolve("#c"), Ok(AssetPath::parse("a/b.png#c")));
313
+ /// assert_eq!(AssetPath::parse("a/b.png#c").resolve("#d"), Ok(AssetPath::parse("a/b.png#d")));
314
+ /// ```
315
+ ///
316
+ /// There are several cases:
317
+ ///
318
+ /// If the `path` argument begins with `#`, then it is considered an asset label, in which case
319
+ /// the result is the base path with the label portion replaced.
320
+ ///
321
+ /// If the path argument begins with '/', then it is considered a 'full' path, in which
322
+ /// case the result is a new `AssetPath` consisting of the base path asset source
323
+ /// (if there is one) with the path and label portions of the relative path. Note that a 'full'
324
+ /// asset path is still relative to the asset source root, and not necessarily an absolute
325
+ /// filesystem path.
326
+ ///
327
+ /// If the `path` argument begins with an asset source (ex: `http://`) then the entire base
328
+ /// path is replaced - the result is the source, path and label (if any) of the `path`
329
+ /// argument.
330
+ ///
331
+ /// Otherwise, the `path` argument is considered a relative path. The result is concatenated
332
+ /// using the following algorithm:
333
+ ///
334
+ /// * The base path and the `path` argument are concatenated.
335
+ /// * Path elements consisting of "/." or "<name>/.." are removed.
336
+ ///
337
+ /// If there are insufficient segments in the base path to match the ".." segments,
338
+ /// then any left-over ".." segments are left as-is.
339
+ pub fn resolve ( & self , path : & str ) -> Result < AssetPath < ' static > , ParseAssetPathError > {
340
+ self . resolve_internal ( path, false )
341
+ }
342
+
343
+ /// Resolves an embedded asset path via concatenation. The result will be an `AssetPath` which
344
+ /// is resolved relative to this path. This is similar in operation to `resolve`, except that
345
+ /// the the 'file' portion of the base path (that is, any characters after the last '/')
346
+ /// is removed before concatenation, in accordance with the behavior specified in
347
+ /// IETF RFC 1808 "Relative URIs".
348
+ ///
349
+ /// The reason for this behavior is that embedded URIs which start with "./" or "../" are
350
+ /// relative to the *directory* containing the asset, not the asset file. This is consistent
351
+ /// with the behavior of URIs in `JavaScript`, CSS, HTML and other web file formats. The
352
+ /// primary use case for this method is resolving relative paths embedded within asset files,
353
+ /// which are relative to the asset in which they are contained.
354
+ ///
355
+ /// ```rust
356
+ /// # use bevy_asset::AssetPath;
357
+ /// assert_eq!(AssetPath::parse("a/b").resolve_embed("c"), Ok(AssetPath::parse("a/c")));
358
+ /// assert_eq!(AssetPath::parse("a/b").resolve_embed("./c"), Ok(AssetPath::parse("a/c")));
359
+ /// assert_eq!(AssetPath::parse("a/b").resolve_embed("../c"), Ok(AssetPath::parse("c")));
360
+ /// assert_eq!(AssetPath::parse("a/b").resolve_embed("c.png"), Ok(AssetPath::parse("a/c.png")));
361
+ /// assert_eq!(AssetPath::parse("a/b").resolve_embed("/c"), Ok(AssetPath::parse("c")));
362
+ /// assert_eq!(AssetPath::parse("a/b.png").resolve_embed("#c"), Ok(AssetPath::parse("a/b.png#c")));
363
+ /// assert_eq!(AssetPath::parse("a/b.png#c").resolve_embed("#d"), Ok(AssetPath::parse("a/b.png#d")));
364
+ /// ```
365
+ pub fn resolve_embed ( & self , path : & str ) -> Result < AssetPath < ' static > , ParseAssetPathError > {
366
+ self . resolve_internal ( path, true )
367
+ }
368
+
369
+ fn resolve_internal (
370
+ & self ,
371
+ path : & str ,
372
+ replace : bool ,
373
+ ) -> Result < AssetPath < ' static > , ParseAssetPathError > {
374
+ if let Some ( label) = path. strip_prefix ( '#' ) {
375
+ // It's a label only
376
+ Ok ( self . clone_owned ( ) . with_label ( label. to_owned ( ) ) )
377
+ } else {
378
+ let ( source, rpath, rlabel) = AssetPath :: parse_internal ( path) ?;
379
+ let mut base_path = PathBuf :: from ( self . path ( ) ) ;
380
+ if replace && !self . path . to_str ( ) . unwrap ( ) . ends_with ( '/' ) {
381
+ // No error if base is empty (per RFC 1808).
382
+ base_path. pop ( ) ;
383
+ }
384
+
385
+ // Strip off leading slash
386
+ let mut is_absolute = false ;
387
+ let rpath = match rpath. strip_prefix ( "/" ) {
388
+ Ok ( p) => {
389
+ is_absolute = true ;
390
+ p
391
+ }
392
+ _ => rpath,
393
+ } ;
394
+
395
+ let mut result_path = PathBuf :: new ( ) ;
396
+ if !is_absolute && source. is_none ( ) {
397
+ for elt in base_path. iter ( ) {
398
+ if elt == "." {
399
+ // Skip
400
+ } else if elt == ".." {
401
+ if !result_path. pop ( ) {
402
+ // Preserve ".." if insufficient matches (per RFC 1808).
403
+ result_path. push ( elt) ;
404
+ }
405
+ } else {
406
+ result_path. push ( elt) ;
407
+ }
408
+ }
409
+ }
410
+
411
+ for elt in rpath. iter ( ) {
412
+ if elt == "." {
413
+ // Skip
414
+ } else if elt == ".." {
415
+ if !result_path. pop ( ) {
416
+ // Preserve ".." if insufficient matches (per RFC 1808).
417
+ result_path. push ( elt) ;
418
+ }
419
+ } else {
420
+ result_path. push ( elt) ;
421
+ }
422
+ }
423
+
424
+ Ok ( AssetPath {
425
+ source : match source {
426
+ Some ( source) => AssetSourceId :: Name ( CowArc :: Owned ( source. into ( ) ) ) ,
427
+ None => self . source . clone_owned ( ) ,
428
+ } ,
429
+ path : CowArc :: Owned ( result_path. into ( ) ) ,
430
+ label : rlabel. map ( |l| CowArc :: Owned ( l. into ( ) ) ) ,
431
+ } )
432
+ }
433
+ }
434
+
302
435
/// Returns the full extension (including multiple '.' values).
303
436
/// Ex: Returns `"config.ron"` for `"my_asset.config.ron"`
304
437
pub fn get_full_extension ( & self ) -> Option < String > {
@@ -583,4 +716,254 @@ mod tests {
583
716
let result = AssetPath :: parse_internal ( "http:/" ) ;
584
717
assert_eq ! ( result, Err ( crate :: ParseAssetPathError :: InvalidSourceSyntax ) ) ;
585
718
}
719
+
720
+ #[ test]
721
+ fn test_resolve_full ( ) {
722
+ // A "full" path should ignore the base path.
723
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
724
+ assert_eq ! (
725
+ base. resolve( "/joe/next" ) . unwrap( ) ,
726
+ AssetPath :: from( "joe/next" )
727
+ ) ;
728
+ assert_eq ! (
729
+ base. resolve_embed( "/joe/next" ) . unwrap( ) ,
730
+ AssetPath :: from( "joe/next" )
731
+ ) ;
732
+ assert_eq ! (
733
+ base. resolve( "/joe/next#dave" ) . unwrap( ) ,
734
+ AssetPath :: from( "joe/next#dave" )
735
+ ) ;
736
+ assert_eq ! (
737
+ base. resolve_embed( "/joe/next#dave" ) . unwrap( ) ,
738
+ AssetPath :: from( "joe/next#dave" )
739
+ ) ;
740
+ }
741
+
742
+ #[ test]
743
+ fn test_resolve_implicit_relative ( ) {
744
+ // A path with no inital directory separator should be considered relative.
745
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
746
+ assert_eq ! (
747
+ base. resolve( "joe/next" ) . unwrap( ) ,
748
+ AssetPath :: from( "alice/bob/joe/next" )
749
+ ) ;
750
+ assert_eq ! (
751
+ base. resolve_embed( "joe/next" ) . unwrap( ) ,
752
+ AssetPath :: from( "alice/joe/next" )
753
+ ) ;
754
+ assert_eq ! (
755
+ base. resolve( "joe/next#dave" ) . unwrap( ) ,
756
+ AssetPath :: from( "alice/bob/joe/next#dave" )
757
+ ) ;
758
+ assert_eq ! (
759
+ base. resolve_embed( "joe/next#dave" ) . unwrap( ) ,
760
+ AssetPath :: from( "alice/joe/next#dave" )
761
+ ) ;
762
+ }
763
+
764
+ #[ test]
765
+ fn test_resolve_explicit_relative ( ) {
766
+ // A path which begins with "./" or "../" is treated as relative
767
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
768
+ assert_eq ! (
769
+ base. resolve( "./martin#dave" ) . unwrap( ) ,
770
+ AssetPath :: from( "alice/bob/martin#dave" )
771
+ ) ;
772
+ assert_eq ! (
773
+ base. resolve_embed( "./martin#dave" ) . unwrap( ) ,
774
+ AssetPath :: from( "alice/martin#dave" )
775
+ ) ;
776
+ assert_eq ! (
777
+ base. resolve( "../martin#dave" ) . unwrap( ) ,
778
+ AssetPath :: from( "alice/martin#dave" )
779
+ ) ;
780
+ assert_eq ! (
781
+ base. resolve_embed( "../martin#dave" ) . unwrap( ) ,
782
+ AssetPath :: from( "martin#dave" )
783
+ ) ;
784
+ }
785
+
786
+ #[ test]
787
+ fn test_resolve_trailing_slash ( ) {
788
+ // A path which begins with "./" or "../" is treated as relative
789
+ let base = AssetPath :: from ( "alice/bob/" ) ;
790
+ assert_eq ! (
791
+ base. resolve( "./martin#dave" ) . unwrap( ) ,
792
+ AssetPath :: from( "alice/bob/martin#dave" )
793
+ ) ;
794
+ assert_eq ! (
795
+ base. resolve_embed( "./martin#dave" ) . unwrap( ) ,
796
+ AssetPath :: from( "alice/bob/martin#dave" )
797
+ ) ;
798
+ assert_eq ! (
799
+ base. resolve( "../martin#dave" ) . unwrap( ) ,
800
+ AssetPath :: from( "alice/martin#dave" )
801
+ ) ;
802
+ assert_eq ! (
803
+ base. resolve_embed( "../martin#dave" ) . unwrap( ) ,
804
+ AssetPath :: from( "alice/martin#dave" )
805
+ ) ;
806
+ }
807
+
808
+ #[ test]
809
+ fn test_resolve_canonicalize ( ) {
810
+ // Test that ".." and "." are removed after concatenation.
811
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
812
+ assert_eq ! (
813
+ base. resolve( "./martin/stephan/..#dave" ) . unwrap( ) ,
814
+ AssetPath :: from( "alice/bob/martin#dave" )
815
+ ) ;
816
+ assert_eq ! (
817
+ base. resolve_embed( "./martin/stephan/..#dave" ) . unwrap( ) ,
818
+ AssetPath :: from( "alice/martin#dave" )
819
+ ) ;
820
+ assert_eq ! (
821
+ base. resolve( "../martin/.#dave" ) . unwrap( ) ,
822
+ AssetPath :: from( "alice/martin#dave" )
823
+ ) ;
824
+ assert_eq ! (
825
+ base. resolve_embed( "../martin/.#dave" ) . unwrap( ) ,
826
+ AssetPath :: from( "martin#dave" )
827
+ ) ;
828
+ assert_eq ! (
829
+ base. resolve( "/martin/stephan/..#dave" ) . unwrap( ) ,
830
+ AssetPath :: from( "martin#dave" )
831
+ ) ;
832
+ assert_eq ! (
833
+ base. resolve_embed( "/martin/stephan/..#dave" ) . unwrap( ) ,
834
+ AssetPath :: from( "martin#dave" )
835
+ ) ;
836
+ }
837
+
838
+ #[ test]
839
+ fn test_resolve_canonicalize_base ( ) {
840
+ // Test that ".." and "." are removed after concatenation even from the base path.
841
+ let base = AssetPath :: from ( "alice/../bob#carol" ) ;
842
+ assert_eq ! (
843
+ base. resolve( "./martin/stephan/..#dave" ) . unwrap( ) ,
844
+ AssetPath :: from( "bob/martin#dave" )
845
+ ) ;
846
+ assert_eq ! (
847
+ base. resolve_embed( "./martin/stephan/..#dave" ) . unwrap( ) ,
848
+ AssetPath :: from( "martin#dave" )
849
+ ) ;
850
+ assert_eq ! (
851
+ base. resolve( "../martin/.#dave" ) . unwrap( ) ,
852
+ AssetPath :: from( "martin#dave" )
853
+ ) ;
854
+ assert_eq ! (
855
+ base. resolve_embed( "../martin/.#dave" ) . unwrap( ) ,
856
+ AssetPath :: from( "../martin#dave" )
857
+ ) ;
858
+ assert_eq ! (
859
+ base. resolve( "/martin/stephan/..#dave" ) . unwrap( ) ,
860
+ AssetPath :: from( "martin#dave" )
861
+ ) ;
862
+ assert_eq ! (
863
+ base. resolve_embed( "/martin/stephan/..#dave" ) . unwrap( ) ,
864
+ AssetPath :: from( "martin#dave" )
865
+ ) ;
866
+ }
867
+
868
+ #[ test]
869
+ fn test_resolve_canonicalize_with_source ( ) {
870
+ // Test that ".." and "." are removed after concatenation.
871
+ let base = AssetPath :: from ( "source://alice/bob#carol" ) ;
872
+ assert_eq ! (
873
+ base. resolve( "./martin/stephan/..#dave" ) . unwrap( ) ,
874
+ AssetPath :: from( "source://alice/bob/martin#dave" )
875
+ ) ;
876
+ assert_eq ! (
877
+ base. resolve_embed( "./martin/stephan/..#dave" ) . unwrap( ) ,
878
+ AssetPath :: from( "source://alice/martin#dave" )
879
+ ) ;
880
+ assert_eq ! (
881
+ base. resolve( "../martin/.#dave" ) . unwrap( ) ,
882
+ AssetPath :: from( "source://alice/martin#dave" )
883
+ ) ;
884
+ assert_eq ! (
885
+ base. resolve_embed( "../martin/.#dave" ) . unwrap( ) ,
886
+ AssetPath :: from( "source://martin#dave" )
887
+ ) ;
888
+ assert_eq ! (
889
+ base. resolve( "/martin/stephan/..#dave" ) . unwrap( ) ,
890
+ AssetPath :: from( "source://martin#dave" )
891
+ ) ;
892
+ assert_eq ! (
893
+ base. resolve_embed( "/martin/stephan/..#dave" ) . unwrap( ) ,
894
+ AssetPath :: from( "source://martin#dave" )
895
+ ) ;
896
+ }
897
+
898
+ #[ test]
899
+ fn test_resolve_absolute ( ) {
900
+ // Paths beginning with '/' replace the base path
901
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
902
+ assert_eq ! (
903
+ base. resolve( "/martin/stephan" ) . unwrap( ) ,
904
+ AssetPath :: from( "martin/stephan" )
905
+ ) ;
906
+ assert_eq ! (
907
+ base. resolve_embed( "/martin/stephan" ) . unwrap( ) ,
908
+ AssetPath :: from( "martin/stephan" )
909
+ ) ;
910
+ assert_eq ! (
911
+ base. resolve( "/martin/stephan#dave" ) . unwrap( ) ,
912
+ AssetPath :: from( "martin/stephan/#dave" )
913
+ ) ;
914
+ assert_eq ! (
915
+ base. resolve_embed( "/martin/stephan#dave" ) . unwrap( ) ,
916
+ AssetPath :: from( "martin/stephan/#dave" )
917
+ ) ;
918
+ }
919
+
920
+ #[ test]
921
+ fn test_resolve_asset_source ( ) {
922
+ // Paths beginning with 'source://' replace the base path
923
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
924
+ assert_eq ! (
925
+ base. resolve( "source://martin/stephan" ) . unwrap( ) ,
926
+ AssetPath :: from( "source://martin/stephan" )
927
+ ) ;
928
+ assert_eq ! (
929
+ base. resolve_embed( "source://martin/stephan" ) . unwrap( ) ,
930
+ AssetPath :: from( "source://martin/stephan" )
931
+ ) ;
932
+ assert_eq ! (
933
+ base. resolve( "source://martin/stephan#dave" ) . unwrap( ) ,
934
+ AssetPath :: from( "source://martin/stephan/#dave" )
935
+ ) ;
936
+ assert_eq ! (
937
+ base. resolve_embed( "source://martin/stephan#dave" ) . unwrap( ) ,
938
+ AssetPath :: from( "source://martin/stephan/#dave" )
939
+ ) ;
940
+ }
941
+
942
+ #[ test]
943
+ fn test_resolve_label ( ) {
944
+ // A relative path with only a label should replace the label portion
945
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
946
+ assert_eq ! (
947
+ base. resolve( "#dave" ) . unwrap( ) ,
948
+ AssetPath :: from( "alice/bob#dave" )
949
+ ) ;
950
+ assert_eq ! (
951
+ base. resolve_embed( "#dave" ) . unwrap( ) ,
952
+ AssetPath :: from( "alice/bob#dave" )
953
+ ) ;
954
+ }
955
+
956
+ #[ test]
957
+ fn test_resolve_insufficient_elements ( ) {
958
+ // Ensure that ".." segments are preserved if there are insufficient elements to remove them.
959
+ let base = AssetPath :: from ( "alice/bob#carol" ) ;
960
+ assert_eq ! (
961
+ base. resolve( "../../joe/next" ) . unwrap( ) ,
962
+ AssetPath :: from( "joe/next" )
963
+ ) ;
964
+ assert_eq ! (
965
+ base. resolve_embed( "../../joe/next" ) . unwrap( ) ,
966
+ AssetPath :: from( "../joe/next" )
967
+ ) ;
968
+ }
586
969
}
0 commit comments