@@ -360,14 +360,18 @@ impl Client {
360360 }
361361 // Can unwrap because we got to 'entry' from walking 'source'
362362 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+
363367 tracing:: trace!( "Adding new layer for asset {rel_path:?}" ) ;
364368 // Construct and push layer, adding its digest to the locked component files Vec
365369 let layer = Self :: data_layer ( entry. path ( ) , DATA_MEDIATYPE . to_string ( ) ) . await ?;
366370 let content = self . content_ref_for_layer ( & layer) ;
367371 let content_inline = content. inline . is_some ( ) ;
368372 files. push ( ContentPath {
369373 content,
370- path : rel_path. into ( ) ,
374+ path : rel_path,
371375 } ) ;
372376 // As a workaround for OCI implementations that don't support very small blobs,
373377 // don't push very small content that has been inlined into the manifest:
@@ -461,14 +465,14 @@ impl Client {
461465 let p = self
462466 . cache
463467 . manifests_dir ( )
464- . join ( reference. registry ( ) )
468+ . join ( fs_safe_segment ( reference. registry ( ) ) )
465469 . join ( reference. repository ( ) )
466470 . join ( reference. tag ( ) . unwrap_or ( LATEST_TAG ) ) ;
467471
468472 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+ } ) ?;
472476 }
473477
474478 Ok ( p. join ( MANIFEST_FILE ) )
@@ -483,7 +487,7 @@ impl Client {
483487 let p = self
484488 . cache
485489 . manifests_dir ( )
486- . join ( reference. registry ( ) )
490+ . join ( fs_safe_segment ( reference. registry ( ) ) )
487491 . join ( reference. repository ( ) )
488492 . join ( reference. tag ( ) . unwrap_or ( LATEST_TAG ) ) ;
489493
@@ -782,6 +786,41 @@ fn add_inferred(map: &mut BTreeMap<String, String>, key: &str, value: Option<Str
782786 }
783787}
784788
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+
785824#[ cfg( test) ]
786825mod test {
787826 use super :: * ;
0 commit comments