33
44use std:: {
55 cell:: RefCell ,
6- collections:: BTreeMap ,
6+ collections:: { BTreeMap , HashMap } ,
77 ffi:: OsStr ,
88 path:: { Component , Path } ,
99 rc:: Rc ,
@@ -12,7 +12,7 @@ use std::{
1212use thiserror:: Error ;
1313
1414/// File metadata similar to `struct stat` from POSIX.
15- #[ derive( Debug ) ]
15+ #[ derive( Debug , Clone ) ]
1616pub struct Stat {
1717 /// File mode and permissions bits.
1818 pub st_mode : u32 ,
@@ -63,6 +63,21 @@ pub enum LeafContent<T> {
6363 Symlink ( Box < OsStr > ) ,
6464}
6565
66+ impl < T > LeafContent < T > {
67+ /// Maps `Regular(&T)` to `Regular(U)` via a fallible function,
68+ /// passing all other variants through unchanged.
69+ fn try_map_ref < U , E > ( & self , f : impl FnOnce ( & T ) -> Result < U , E > ) -> Result < LeafContent < U > , E > {
70+ match self {
71+ LeafContent :: Regular ( t) => Ok ( LeafContent :: Regular ( f ( t) ?) ) ,
72+ LeafContent :: BlockDevice ( rdev) => Ok ( LeafContent :: BlockDevice ( * rdev) ) ,
73+ LeafContent :: CharacterDevice ( rdev) => Ok ( LeafContent :: CharacterDevice ( * rdev) ) ,
74+ LeafContent :: Fifo => Ok ( LeafContent :: Fifo ) ,
75+ LeafContent :: Socket => Ok ( LeafContent :: Socket ) ,
76+ LeafContent :: Symlink ( target) => Ok ( LeafContent :: Symlink ( target. clone ( ) ) ) ,
77+ }
78+ }
79+ }
80+
6681/// A leaf node representing a non-directory file.
6782#[ derive( Debug ) ]
6883pub struct Leaf < T > {
@@ -118,9 +133,50 @@ impl<T> Inode<T> {
118133 Inode :: Leaf ( leaf) => & leaf. stat ,
119134 }
120135 }
136+
137+ fn try_map_regular_impl < U , E > (
138+ self ,
139+ f : & mut impl FnMut ( & T ) -> Result < U , E > ,
140+ rc_map : & mut HashMap < * const Leaf < T > , Rc < Leaf < U > > > ,
141+ ) -> Result < Inode < U > , E > {
142+ match self {
143+ Inode :: Directory ( dir) => {
144+ let new_dir = dir. try_map_regular_impl ( f, rc_map) ?;
145+ Ok ( Inode :: Directory ( Box :: new ( new_dir) ) )
146+ }
147+ Inode :: Leaf ( leaf_rc) => {
148+ let ptr = Rc :: as_ptr ( & leaf_rc) ;
149+ if let Some ( existing) = rc_map. get ( & ptr) {
150+ return Ok ( Inode :: Leaf ( Rc :: clone ( existing) ) ) ;
151+ }
152+ let new_content = leaf_rc. content . try_map_ref ( f) ?;
153+ let new_leaf = Rc :: new ( Leaf {
154+ stat : leaf_rc. stat . clone ( ) ,
155+ content : new_content,
156+ } ) ;
157+ rc_map. insert ( ptr, Rc :: clone ( & new_leaf) ) ;
158+ Ok ( Inode :: Leaf ( new_leaf) )
159+ }
160+ }
161+ }
121162}
122163
123164impl < T > Directory < T > {
165+ fn try_map_regular_impl < U , E > (
166+ self ,
167+ f : & mut impl FnMut ( & T ) -> Result < U , E > ,
168+ rc_map : & mut HashMap < * const Leaf < T > , Rc < Leaf < U > > > ,
169+ ) -> Result < Directory < U > , E > {
170+ let mut new_entries = BTreeMap :: new ( ) ;
171+ for ( name, inode) in self . entries {
172+ new_entries. insert ( name, inode. try_map_regular_impl ( f, rc_map) ?) ;
173+ }
174+ Ok ( Directory {
175+ stat : self . stat ,
176+ entries : new_entries,
177+ } )
178+ }
179+
124180 /// Creates a new directory with the given metadata.
125181 pub fn new ( stat : Stat ) -> Self {
126182 Self {
@@ -593,6 +649,23 @@ impl<T> FileSystem<T> {
593649 self . canonicalize_run ( ) ?;
594650 Ok ( ( ) )
595651 }
652+
653+ /// Converts `FileSystem<T>` to `FileSystem<U>` by mapping regular file content.
654+ ///
655+ /// Applies `f` to each `LeafContent::Regular(T)` to produce `LeafContent::Regular(U)`.
656+ /// All other leaf content variants (symlinks, devices, etc.) are passed through unchanged.
657+ ///
658+ /// Hardlink sharing is preserved: if multiple directory entries point to the same
659+ /// `Rc<Leaf<T>>`, the resulting tree will have entries pointing to the same `Rc<Leaf<U>>`.
660+ /// The mapping function is called exactly once per unique leaf.
661+ pub fn try_map_regular < U , E > (
662+ self ,
663+ mut f : impl FnMut ( & T ) -> Result < U , E > ,
664+ ) -> Result < FileSystem < U > , E > {
665+ let mut rc_map: HashMap < * const Leaf < T > , Rc < Leaf < U > > > = HashMap :: new ( ) ;
666+ let root = self . root . try_map_regular_impl ( & mut f, & mut rc_map) ?;
667+ Ok ( FileSystem { root } )
668+ }
596669}
597670
598671#[ cfg( test) ]
@@ -1021,6 +1094,167 @@ mod tests {
10211094 fs. canonicalize_run ( ) . unwrap ( ) ;
10221095 }
10231096
1097+ #[ test]
1098+ fn test_try_map_regular_basic ( ) {
1099+ let mut fs = FileSystem :: < u32 > :: new ( stat_with_mtime ( 1 ) ) ;
1100+ let leaf = Rc :: new ( Leaf {
1101+ stat : stat_with_mtime ( 10 ) ,
1102+ content : LeafContent :: Regular ( 42u32 ) ,
1103+ } ) ;
1104+ fs. root . insert ( OsStr :: new ( "file.txt" ) , Inode :: Leaf ( leaf) ) ;
1105+
1106+ let mapped = fs
1107+ . try_map_regular ( |v : & u32 | Ok :: < String , std:: fmt:: Error > ( format ! ( "val={v}" ) ) )
1108+ . unwrap ( ) ;
1109+
1110+ let content = mapped. root . get_file ( OsStr :: new ( "file.txt" ) ) . unwrap ( ) ;
1111+ assert_eq ! ( content, "val=42" ) ;
1112+ assert_eq ! ( mapped. root. stat. st_mtim_sec, 1 ) ;
1113+ }
1114+
1115+ #[ test]
1116+ fn test_try_map_regular_non_regular_passthrough ( ) {
1117+ let mut fs = FileSystem :: < u32 > :: new ( default_stat ( ) ) ;
1118+ fs. root . insert (
1119+ OsStr :: new ( "link" ) ,
1120+ Inode :: Leaf ( Rc :: new ( Leaf {
1121+ stat : stat_with_mtime ( 1 ) ,
1122+ content : LeafContent :: Symlink ( OsString :: from ( "/target" ) . into_boxed_os_str ( ) ) ,
1123+ } ) ) ,
1124+ ) ;
1125+ fs. root . insert (
1126+ OsStr :: new ( "fifo" ) ,
1127+ Inode :: Leaf ( Rc :: new ( Leaf {
1128+ stat : stat_with_mtime ( 2 ) ,
1129+ content : LeafContent :: Fifo ,
1130+ } ) ) ,
1131+ ) ;
1132+ fs. root . insert (
1133+ OsStr :: new ( "sock" ) ,
1134+ Inode :: Leaf ( Rc :: new ( Leaf {
1135+ stat : stat_with_mtime ( 3 ) ,
1136+ content : LeafContent :: Socket ,
1137+ } ) ) ,
1138+ ) ;
1139+ fs. root . insert (
1140+ OsStr :: new ( "blk" ) ,
1141+ Inode :: Leaf ( Rc :: new ( Leaf {
1142+ stat : stat_with_mtime ( 4 ) ,
1143+ content : LeafContent :: BlockDevice ( 0x0801 ) ,
1144+ } ) ) ,
1145+ ) ;
1146+ fs. root . insert (
1147+ OsStr :: new ( "chr" ) ,
1148+ Inode :: Leaf ( Rc :: new ( Leaf {
1149+ stat : stat_with_mtime ( 5 ) ,
1150+ content : LeafContent :: CharacterDevice ( 0x0501 ) ,
1151+ } ) ) ,
1152+ ) ;
1153+
1154+ let mapped = fs
1155+ . try_map_regular ( |_: & u32 | Ok :: < String , std:: fmt:: Error > ( "unused" . into ( ) ) )
1156+ . unwrap ( ) ;
1157+
1158+ // Verify each non-regular variant is preserved
1159+ match mapped. root . lookup ( OsStr :: new ( "link" ) ) {
1160+ Some ( Inode :: Leaf ( l) ) => match & l. content {
1161+ LeafContent :: Symlink ( t) => assert_eq ! ( t. as_ref( ) , OsStr :: new( "/target" ) ) ,
1162+ other => panic ! ( "Expected Symlink, got {other:?}" ) ,
1163+ } ,
1164+ other => panic ! ( "Expected Leaf, got {other:?}" ) ,
1165+ }
1166+ match mapped. root . lookup ( OsStr :: new ( "fifo" ) ) {
1167+ Some ( Inode :: Leaf ( l) ) => assert ! ( matches!( l. content, LeafContent :: Fifo ) ) ,
1168+ other => panic ! ( "Expected Leaf/Fifo, got {other:?}" ) ,
1169+ }
1170+ match mapped. root . lookup ( OsStr :: new ( "sock" ) ) {
1171+ Some ( Inode :: Leaf ( l) ) => assert ! ( matches!( l. content, LeafContent :: Socket ) ) ,
1172+ other => panic ! ( "Expected Leaf/Socket, got {other:?}" ) ,
1173+ }
1174+ match mapped. root . lookup ( OsStr :: new ( "blk" ) ) {
1175+ Some ( Inode :: Leaf ( l) ) => match & l. content {
1176+ LeafContent :: BlockDevice ( rdev) => assert_eq ! ( * rdev, 0x0801 ) ,
1177+ other => panic ! ( "Expected BlockDevice, got {other:?}" ) ,
1178+ } ,
1179+ other => panic ! ( "Expected Leaf, got {other:?}" ) ,
1180+ }
1181+ match mapped. root . lookup ( OsStr :: new ( "chr" ) ) {
1182+ Some ( Inode :: Leaf ( l) ) => match & l. content {
1183+ LeafContent :: CharacterDevice ( rdev) => assert_eq ! ( * rdev, 0x0501 ) ,
1184+ other => panic ! ( "Expected CharacterDevice, got {other:?}" ) ,
1185+ } ,
1186+ other => panic ! ( "Expected Leaf, got {other:?}" ) ,
1187+ }
1188+ }
1189+
1190+ #[ test]
1191+ fn test_try_map_regular_hardlink_sharing ( ) {
1192+ let mut fs = FileSystem :: < u32 > :: new ( default_stat ( ) ) ;
1193+ let shared_leaf = Rc :: new ( Leaf {
1194+ stat : stat_with_mtime ( 10 ) ,
1195+ content : LeafContent :: Regular ( 99u32 ) ,
1196+ } ) ;
1197+ // Insert the same Rc under two names (hardlink)
1198+ fs. root
1199+ . insert ( OsStr :: new ( "a" ) , Inode :: Leaf ( Rc :: clone ( & shared_leaf) ) ) ;
1200+ fs. root
1201+ . insert ( OsStr :: new ( "b" ) , Inode :: Leaf ( Rc :: clone ( & shared_leaf) ) ) ;
1202+
1203+ // Track how many times the mapping function is called
1204+ let call_count = RefCell :: new ( 0u32 ) ;
1205+ let mapped = fs
1206+ . try_map_regular ( |v : & u32 | {
1207+ * call_count. borrow_mut ( ) += 1 ;
1208+ Ok :: < String , std:: fmt:: Error > ( format ! ( "mapped={v}" ) )
1209+ } )
1210+ . unwrap ( ) ;
1211+
1212+ // The mapping function should be called exactly once for the shared leaf
1213+ assert_eq ! ( * call_count. borrow( ) , 1 ) ;
1214+
1215+ // Both entries should point to the same Rc
1216+ let rc_a = match mapped. root . lookup ( OsStr :: new ( "a" ) ) {
1217+ Some ( Inode :: Leaf ( l) ) => Rc :: clone ( l) ,
1218+ other => panic ! ( "Expected Leaf, got {other:?}" ) ,
1219+ } ;
1220+ let rc_b = match mapped. root . lookup ( OsStr :: new ( "b" ) ) {
1221+ Some ( Inode :: Leaf ( l) ) => Rc :: clone ( l) ,
1222+ other => panic ! ( "Expected Leaf, got {other:?}" ) ,
1223+ } ;
1224+ assert ! ( Rc :: ptr_eq( & rc_a, & rc_b) ) ;
1225+ assert_eq ! ( mapped. root. get_file( OsStr :: new( "a" ) ) . unwrap( ) , "mapped=99" ) ;
1226+ }
1227+
1228+ #[ test]
1229+ fn test_try_map_regular_error_propagation ( ) {
1230+ let mut fs = FileSystem :: < u32 > :: new ( default_stat ( ) ) ;
1231+ fs. root . insert (
1232+ OsStr :: new ( "ok" ) ,
1233+ Inode :: Leaf ( Rc :: new ( Leaf {
1234+ stat : stat_with_mtime ( 1 ) ,
1235+ content : LeafContent :: Regular ( 1u32 ) ,
1236+ } ) ) ,
1237+ ) ;
1238+ fs. root . insert (
1239+ OsStr :: new ( "fail" ) ,
1240+ Inode :: Leaf ( Rc :: new ( Leaf {
1241+ stat : stat_with_mtime ( 2 ) ,
1242+ content : LeafContent :: Regular ( 0u32 ) ,
1243+ } ) ) ,
1244+ ) ;
1245+
1246+ let result = fs. try_map_regular ( |v : & u32 | {
1247+ if * v == 0 {
1248+ Err ( "cannot map zero" )
1249+ } else {
1250+ Ok ( v * 10 )
1251+ }
1252+ } ) ;
1253+
1254+ assert ! ( result. is_err( ) ) ;
1255+ assert_eq ! ( result. unwrap_err( ) , "cannot map zero" ) ;
1256+ }
1257+
10241258 #[ test]
10251259 fn test_transform_for_oci ( ) {
10261260 let mut fs = FileSystem :: < FileContents > :: new ( default_stat ( ) ) ;
0 commit comments