@@ -360,14 +360,18 @@ impl Client {
360
360
}
361
361
// Can unwrap because we got to 'entry' from walking 'source'
362
362
let rel_path = entry. path ( ) . strip_prefix ( source) . unwrap ( ) ;
363
+ // Paths must be in portable (forward slash) format in the registry,
364
+ // so that they can be placed correctly on any host system
365
+ let rel_path = portable_path ( rel_path) ;
366
+
363
367
tracing:: trace!( "Adding new layer for asset {rel_path:?}" ) ;
364
368
// Construct and push layer, adding its digest to the locked component files Vec
365
369
let layer = Self :: data_layer ( entry. path ( ) , DATA_MEDIATYPE . to_string ( ) ) . await ?;
366
370
let content = self . content_ref_for_layer ( & layer) ;
367
371
let content_inline = content. inline . is_some ( ) ;
368
372
files. push ( ContentPath {
369
373
content,
370
- path : rel_path. into ( ) ,
374
+ path : rel_path,
371
375
} ) ;
372
376
// As a workaround for OCI implementations that don't support very small blobs,
373
377
// don't push very small content that has been inlined into the manifest:
@@ -461,14 +465,14 @@ impl Client {
461
465
let p = self
462
466
. cache
463
467
. manifests_dir ( )
464
- . join ( reference. registry ( ) )
468
+ . join ( fs_safe_segment ( reference. registry ( ) ) )
465
469
. join ( reference. repository ( ) )
466
470
. join ( reference. tag ( ) . unwrap_or ( LATEST_TAG ) ) ;
467
471
468
472
if !p. is_dir ( ) {
469
- fs:: create_dir_all ( & p)
470
- . await
471
- . context ( "cannot find directory for OCI manifest" ) ?;
473
+ fs:: create_dir_all ( & p) . await . with_context ( || {
474
+ format ! ( "cannot create directory {} for OCI manifest" , p . display ( ) )
475
+ } ) ?;
472
476
}
473
477
474
478
Ok ( p. join ( MANIFEST_FILE ) )
@@ -483,7 +487,7 @@ impl Client {
483
487
let p = self
484
488
. cache
485
489
. manifests_dir ( )
486
- . join ( reference. registry ( ) )
490
+ . join ( fs_safe_segment ( reference. registry ( ) ) )
487
491
. join ( reference. repository ( ) )
488
492
. join ( reference. tag ( ) . unwrap_or ( LATEST_TAG ) ) ;
489
493
@@ -782,6 +786,41 @@ fn add_inferred(map: &mut BTreeMap<String, String>, key: &str, value: Option<Str
782
786
}
783
787
}
784
788
789
+ /// Takes a relative path and turns it into a format that is safe
790
+ /// for putting into a registry where it might end up on any host.
791
+ #[ cfg( target_os = "windows" ) ]
792
+ fn portable_path ( rel_path : & Path ) -> PathBuf {
793
+ assert ! (
794
+ rel_path. is_relative( ) ,
795
+ "portable_path requires paths to be relative"
796
+ ) ;
797
+ let portable_path = rel_path. to_string_lossy ( ) . replace ( '\\' , "/" ) ;
798
+ PathBuf :: from ( portable_path)
799
+ }
800
+
801
+ /// Takes a relative path and turns it into a format that is safe
802
+ /// for putting into a registry where it might end up on any host.
803
+ /// This is a no-op on Unix systems, but is needed for Windows.
804
+ #[ cfg( not( target_os = "windows" ) ) ]
805
+ fn portable_path ( rel_path : & Path ) -> PathBuf {
806
+ rel_path. into ( )
807
+ }
808
+
809
+ /// Takes a string intended for use as part of a path and makes it
810
+ /// compatible with the local filesystem.
811
+ #[ cfg( target_os = "windows" ) ]
812
+ fn fs_safe_segment ( segment : & str ) -> impl AsRef < Path > {
813
+ segment. replace ( ':' , "_" )
814
+ }
815
+
816
+ /// Takes a string intended for use as part of a path and makes it
817
+ /// compatible with the local filesystem.
818
+ /// This is a no-op on Unix systems, but is needed for Windows.
819
+ #[ cfg( not( target_os = "windows" ) ) ]
820
+ fn fs_safe_segment ( segment : & str ) -> impl AsRef < Path > + ' _ {
821
+ segment
822
+ }
823
+
785
824
#[ cfg( test) ]
786
825
mod test {
787
826
use super :: * ;
0 commit comments