@@ -547,6 +547,52 @@ impl<T> FileSystem<T> {
547547 stat. xattrs . borrow_mut ( ) . retain ( |k, _| predicate ( k) ) ;
548548 } ) ;
549549 }
550+
551+ /// Empties the `/run` directory if present, using `/usr`'s mtime.
552+ ///
553+ /// `/run` is a tmpfs at runtime and should always be empty in container images.
554+ /// This also works around podman/buildah's `RUN --mount` behavior where bind
555+ /// mount targets leave directory stubs in the filesystem that shouldn't be
556+ /// part of the image content.
557+ ///
558+ /// The mtime is set to match `/usr` for consistency with [`Self::copy_root_metadata_from_usr`].
559+ ///
560+ /// NOTE: If changing this behavior, also update `doc/oci.md`.
561+ ///
562+ /// # Errors
563+ ///
564+ /// Returns an error if `/usr` does not exist (needed to get the mtime).
565+ pub fn canonicalize_run ( & mut self ) -> Result < ( ) , ImageError > {
566+ if self . root . get_directory_opt ( OsStr :: new ( "run" ) ) ?. is_some ( ) {
567+ let usr_mtime = self . root . get_directory ( OsStr :: new ( "usr" ) ) ?. stat . st_mtim_sec ;
568+ let run_dir = self . root . get_directory_mut ( OsStr :: new ( "run" ) ) ?;
569+ run_dir. stat . st_mtim_sec = usr_mtime;
570+ run_dir. clear ( ) ;
571+ }
572+ Ok ( ( ) )
573+ }
574+
575+ /// Transforms the filesystem for OCI container image consistency.
576+ ///
577+ /// This applies the standard transformations needed to ensure consistent
578+ /// composefs digests between build-time (mounted filesystem) and install-time
579+ /// (OCI tar layers) views:
580+ ///
581+ /// 1. [`Self::copy_root_metadata_from_usr`] - copies `/usr` metadata to root directory
582+ /// 2. [`Self::canonicalize_run`] - empties `/run` directory
583+ ///
584+ /// This is the recommended single entry point for OCI container processing.
585+ ///
586+ /// NOTE: If changing this behavior, also update `doc/oci.md`.
587+ ///
588+ /// # Errors
589+ ///
590+ /// Returns an error if `/usr` does not exist.
591+ pub fn transform_for_oci ( & mut self ) -> Result < ( ) , ImageError > {
592+ self . copy_root_metadata_from_usr ( ) ?;
593+ self . canonicalize_run ( ) ?;
594+ Ok ( ( ) )
595+ }
550596}
551597
552598#[ cfg( test) ]
@@ -921,4 +967,93 @@ mod tests {
921967 assert_eq ! ( root_xattrs. len( ) , 1 ) ;
922968 assert ! ( root_xattrs. contains_key( OsStr :: new( "user.custom" ) ) ) ;
923969 }
970+
971+ #[ test]
972+ fn test_canonicalize_run ( ) {
973+ let mut fs = FileSystem :: < FileContents > :: new ( default_stat ( ) ) ;
974+
975+ // Create /usr with specific mtime
976+ let usr_dir = Directory :: new ( stat_with_mtime ( 12345 ) ) ;
977+ fs. root
978+ . insert ( OsStr :: new ( "usr" ) , Inode :: Directory ( Box :: new ( usr_dir) ) ) ;
979+
980+ // Create /run with content and different mtime
981+ let mut run_dir = Directory :: new ( stat_with_mtime ( 99999 ) ) ;
982+ run_dir. insert ( OsStr :: new ( "somefile" ) , Inode :: Leaf ( new_leaf_file ( 11111 ) ) ) ;
983+ let mut subdir = Directory :: new ( stat_with_mtime ( 22222 ) ) ;
984+ subdir. insert ( OsStr :: new ( "nested" ) , Inode :: Leaf ( new_leaf_file ( 33333 ) ) ) ;
985+ run_dir. insert ( OsStr :: new ( "subdir" ) , Inode :: Directory ( Box :: new ( subdir) ) ) ;
986+ fs. root
987+ . insert ( OsStr :: new ( "run" ) , Inode :: Directory ( Box :: new ( run_dir) ) ) ;
988+
989+ // Verify /run has content before
990+ assert_eq ! (
991+ fs. root
992+ . get_directory( OsStr :: new( "run" ) )
993+ . unwrap( )
994+ . entries
995+ . len( ) ,
996+ 2
997+ ) ;
998+
999+ // Canonicalize
1000+ fs. canonicalize_run ( ) . unwrap ( ) ;
1001+
1002+ // Verify /run is now empty with /usr's mtime
1003+ let run = fs. root . get_directory ( OsStr :: new ( "run" ) ) . unwrap ( ) ;
1004+ assert ! ( run. entries. is_empty( ) ) ;
1005+ assert_eq ! ( run. stat. st_mtim_sec, 12345 ) ;
1006+ }
1007+
1008+ #[ test]
1009+ fn test_canonicalize_run_no_run_dir ( ) {
1010+ let mut fs = FileSystem :: < FileContents > :: new ( default_stat ( ) ) ;
1011+
1012+ // Create /usr but no /run
1013+ let usr_dir = Directory :: new ( stat_with_mtime ( 12345 ) ) ;
1014+ fs. root
1015+ . insert ( OsStr :: new ( "usr" ) , Inode :: Directory ( Box :: new ( usr_dir) ) ) ;
1016+
1017+ // Should succeed without error
1018+ fs. canonicalize_run ( ) . unwrap ( ) ;
1019+ }
1020+
1021+ #[ test]
1022+ fn test_transform_for_oci ( ) {
1023+ let mut fs = FileSystem :: < FileContents > :: new ( default_stat ( ) ) ;
1024+
1025+ // Create /usr with specific metadata
1026+ let usr_stat = Stat {
1027+ st_mode : 0o750 ,
1028+ st_uid : 100 ,
1029+ st_gid : 200 ,
1030+ st_mtim_sec : 54321 ,
1031+ xattrs : RefCell :: new ( BTreeMap :: from ( [ (
1032+ Box :: from ( OsStr :: new ( "user.test" ) ) ,
1033+ Box :: from ( b"val" . as_slice ( ) ) ,
1034+ ) ] ) ) ,
1035+ } ;
1036+ fs. root
1037+ . insert ( OsStr :: new ( "usr" ) , new_dir_inode_with_stat ( usr_stat) ) ;
1038+
1039+ // Create /run with content
1040+ let mut run_dir = Directory :: new ( stat_with_mtime ( 99999 ) ) ;
1041+ run_dir. insert ( OsStr :: new ( "file" ) , Inode :: Leaf ( new_leaf_file ( 11111 ) ) ) ;
1042+ fs. root
1043+ . insert ( OsStr :: new ( "run" ) , Inode :: Directory ( Box :: new ( run_dir) ) ) ;
1044+
1045+ // Transform for OCI
1046+ fs. transform_for_oci ( ) . unwrap ( ) ;
1047+
1048+ // Verify root metadata copied from /usr
1049+ assert_eq ! ( fs. root. stat. st_mode, 0o750 ) ;
1050+ assert_eq ! ( fs. root. stat. st_uid, 100 ) ;
1051+ assert_eq ! ( fs. root. stat. st_gid, 200 ) ;
1052+ assert_eq ! ( fs. root. stat. st_mtim_sec, 54321 ) ;
1053+
1054+ // Verify /run is emptied with /usr's mtime
1055+ let run = fs. root . get_directory ( OsStr :: new ( "run" ) ) . unwrap ( ) ;
1056+ assert ! ( run. entries. is_empty( ) ) ;
1057+ assert_eq ! ( run. stat. st_mtim_sec, 54321 ) ;
1058+ }
9241059}
0 commit comments