@@ -25,12 +25,20 @@ const BACKING_DIR: &str = "vite-task-fspy";
2525/// An owned Windows shared-memory mapping.
2626pub struct Shm {
2727 id : String ,
28+ // Field order is significant: unmap the view, close the mapping, then close
29+ // the delete-on-close backing file.
2830 view : MappedView ,
29- _mapping : OwnedHandle ,
30- backing_file : BackingFile ,
31+ _mapping : Option < OwnedHandle > ,
32+ // Owner mappings keep this file alive until their view and mapping handle drop.
33+ #[ cfg_attr(
34+ not( test) ,
35+ expect( dead_code, reason = "the file is retained only for RAII cleanup" )
36+ ) ]
37+ backing_file : Option < File > ,
3138}
3239
33- /// Creates a file-backed named mapping of `size` bytes and returns its owner.
40+ /// Creates a sparse, temporary file-backed named mapping of `size` bytes and
41+ /// returns its owner.
3442///
3543/// # Errors
3644///
@@ -54,14 +62,14 @@ fn create_with(size: usize, mut next_name: impl FnMut() -> String) -> io::Result
5462fn create_named ( backing_dir : & Path , mapping_name : & str , size : u64 ) -> io:: Result < Option < Shm > > {
5563 let path = backing_path ( backing_dir, mapping_name) ?;
5664 let id = encode_id ( & path, mapping_name) ?;
57- let backing_file = match BackingFile :: create ( path) {
65+ let backing_file = match create_backing_file ( & path) {
5866 Ok ( file) => file,
5967 Err ( error) if error. kind ( ) == io:: ErrorKind :: AlreadyExists => return Ok ( None ) ,
6068 Err ( error) => return Err ( error) ,
6169 } ;
62- backing_file . file . set_len ( size) ?;
70+ initialize_backing_file ( & backing_file , size) ?;
6371
64- let mapping = match sys:: create_file_mapping ( & backing_file. file , mapping_name) ? {
72+ let mapping = match sys:: create_file_mapping ( & backing_file, mapping_name) ? {
6573 CreatedMapping :: Created ( mapping) => mapping,
6674 CreatedMapping :: AlreadyExists => return Ok ( None ) ,
6775 } ;
@@ -70,29 +78,23 @@ fn create_named(backing_dir: &Path, mapping_name: &str, size: u64) -> io::Result
7078 } ) ?;
7179 let view = MappedView :: new ( & mapping, len) ?;
7280
73- Ok ( Some ( Shm { id, view, _mapping : mapping, backing_file } ) )
81+ Ok ( Some ( Shm { id, view, _mapping : Some ( mapping) , backing_file : Some ( backing_file ) } ) )
7482}
7583
7684/// Opens the named mapping identified by `id`.
7785///
7886/// # Errors
7987///
80- /// Returns an error if the identifier is invalid, the owner has torn down the
81- /// mapping, or its backing file has a different size .
88+ /// Returns an error if the identifier is invalid, the mapping is unavailable,
89+ /// or `size` cannot be mapped from it .
8290pub fn open ( id : & str , size : usize ) -> io:: Result < Shm > {
83- let expected_size = valid_size ( size) ?;
84- let DecodedId { backing_path, mapping_name } = decode_id ( id) ?;
85- let backing_file = BackingFile :: open ( backing_path) ?;
86- if backing_file. file . metadata ( ) ?. len ( ) != expected_size {
87- return Err ( io:: Error :: new (
88- io:: ErrorKind :: InvalidData ,
89- "shared-memory backing file has an unexpected size" ,
90- ) ) ;
91- }
91+ valid_size ( size) ?;
92+ let DecodedId { mapping_name, .. } = decode_id ( id) ?;
9293
9394 let mapping = sys:: open_file_mapping ( & mapping_name) ?;
9495 let view = MappedView :: new ( & mapping, size) ?;
95- Ok ( Shm { id : id. to_owned ( ) , view, _mapping : mapping, backing_file } )
96+ drop ( mapping) ;
97+ Ok ( Shm { id : id. to_owned ( ) , view, _mapping : None , backing_file : None } )
9698}
9799
98100fn valid_size ( size : usize ) -> io:: Result < u64 > {
@@ -138,6 +140,7 @@ fn encode_id(backing_path: &Path, mapping_name: &str) -> io::Result<String> {
138140}
139141
140142struct DecodedId {
143+ #[ cfg( test) ]
141144 backing_path : PathBuf ,
142145 mapping_name : String ,
143146}
@@ -166,7 +169,11 @@ fn decode_id(id: &str) -> io::Result<DecodedId> {
166169 let mapping_name =
167170 String :: from_utf8 ( decode_id_part ( encoded_mapping_name) ?) . map_err ( |_| invalid_id ( ) ) ?;
168171 validate_id_parts ( & backing_path, & mapping_name) ?;
169- Ok ( DecodedId { backing_path, mapping_name } )
172+ Ok ( DecodedId {
173+ #[ cfg( test) ]
174+ backing_path,
175+ mapping_name,
176+ } )
170177}
171178
172179fn decode_id_part ( encoded : & str ) -> io:: Result < Vec < u8 > > {
@@ -191,71 +198,32 @@ fn invalid_id() -> io::Error {
191198 io:: Error :: new ( io:: ErrorKind :: InvalidInput , "invalid Windows shared-memory identifier" )
192199}
193200
194- struct BackingFile {
195- file : File ,
196- owner_path : Option < PathBuf > ,
201+ fn create_backing_file ( path : & Path ) -> io:: Result < File > {
202+ OpenOptions :: new ( )
203+ . read ( true )
204+ . write ( true )
205+ . create_new ( true )
206+ . share_mode ( sys:: SHARE_ALL )
207+ . attributes ( sys:: TEMPORARY )
208+ . custom_flags ( sys:: DELETE_ON_CLOSE )
209+ . open ( path)
197210}
198211
199- impl BackingFile {
200- fn create ( path : PathBuf ) -> io:: Result < Self > {
201- let file = OpenOptions :: new ( )
202- . read ( true )
203- . write ( true )
204- . create_new ( true )
205- . share_mode ( sys:: SHARE_ALL )
206- . attributes ( sys:: TEMPORARY )
207- . open ( & path) ?;
208- Ok ( Self { file, owner_path : Some ( path) } )
209- }
210-
211- fn open ( path : PathBuf ) -> io:: Result < Self > {
212- let file = OpenOptions :: new ( )
213- . read ( true )
214- . write ( true )
215- . share_mode ( sys:: SHARE_ALL )
216- . attributes ( sys:: TEMPORARY )
217- . open ( path) ?;
218- Ok ( Self { file, owner_path : None } )
219- }
220-
221- fn unlink ( & mut self ) {
222- let Some ( path) = self . owner_path . take ( ) else {
223- return ;
224- } ;
225-
226- let deletion_file = OpenOptions :: new ( )
227- . access_mode ( sys:: DELETE_ACCESS )
228- . share_mode ( sys:: SHARE_ALL )
229- . attributes ( sys:: DELETE_ON_CLOSE )
230- . open ( & path) ;
231- if let Ok ( deletion_file) = deletion_file {
232- let deleted_path = deleted_path ( & path) ;
233- if fs:: rename ( & path, deleted_path) . is_err ( ) {
234- let _ = fs:: remove_file ( & path) ;
235- }
236- drop ( deletion_file) ;
237- } else {
238- let _ = fs:: remove_file ( path) ;
239- }
240- }
241- }
242-
243- impl Drop for BackingFile {
244- fn drop ( & mut self ) {
245- self . unlink ( ) ;
246- }
212+ fn initialize_backing_file ( file : & File , size : u64 ) -> io:: Result < ( ) > {
213+ initialize_backing_file_with ( file, size, sys:: set_sparse)
247214}
248215
249- fn deleted_path ( path : & Path ) -> PathBuf {
250- path . with_extension ( format ! ( "deleted-{}" , Uuid :: new_v4 ( ) ) )
251- }
252-
253- impl Drop for Shm {
254- fn drop ( & mut self ) {
255- // Remove the public backing-file name before the mapping handle drops,
256- // preventing opens that begin after owner teardown.
257- self . backing_file . unlink ( ) ;
216+ fn initialize_backing_file_with (
217+ file : & File ,
218+ size : u64 ,
219+ set_sparse : impl FnOnce ( & File ) -> io :: Result < ( ) > ,
220+ ) -> io :: Result < ( ) > {
221+ if let Err ( error ) = set_sparse ( file )
222+ && !sys :: is_sparse_unsupported ( & error )
223+ {
224+ return Err ( error ) ;
258225 }
226+ file. set_len ( size)
259227}
260228
261229#[ expect( clippy:: len_without_is_empty, reason = "shared-memory mappings are always non-empty" ) ]
@@ -297,6 +265,9 @@ mod tests {
297265 use std:: { ffi:: OsString , fs, process:: Command } ;
298266
299267 use subprocess_test:: command_for_fn;
268+ use windows_sys:: Win32 :: Foundation :: {
269+ ERROR_ACCESS_DENIED , ERROR_INVALID_FUNCTION , ERROR_INVALID_PARAMETER , ERROR_NOT_SUPPORTED ,
270+ } ;
300271
301272 use super :: * ;
302273
@@ -332,7 +303,7 @@ mod tests {
332303 }
333304
334305 #[ test]
335- fn malformed_ids_and_size_mismatches_are_rejected ( ) {
306+ fn malformed_ids_and_invalid_sizes_are_rejected ( ) {
336307 let owner = create ( SIZE ) . unwrap ( ) ;
337308 let decoded = decode_id ( owner. id ( ) ) . unwrap ( ) ;
338309 let encoded_path = URL_SAFE_NO_PAD . encode (
@@ -378,8 +349,7 @@ mod tests {
378349 }
379350
380351 assert_eq ! ( open( owner. id( ) , 0 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidInput ) ;
381- assert_eq ! ( open( owner. id( ) , SIZE / 2 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidData ) ;
382- assert_eq ! ( open( owner. id( ) , SIZE + 1 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidData ) ;
352+ assert ! ( open( owner. id( ) , SIZE + 1 ) . is_err( ) ) ;
383353 }
384354
385355 #[ test]
@@ -388,10 +358,10 @@ mod tests {
388358 let collision_name = format ! ( "{MAPPING_NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
389359 let raw_backing_name = format ! ( "{MAPPING_NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
390360 let raw_backing_path = backing_path ( & backing_dir, & raw_backing_name) . unwrap ( ) ;
391- let raw_backing = BackingFile :: create ( raw_backing_path) . unwrap ( ) ;
392- raw_backing . file . set_len ( SIZE as u64 ) . unwrap ( ) ;
361+ let raw_backing = create_backing_file ( & raw_backing_path) . unwrap ( ) ;
362+ initialize_backing_file ( & raw_backing , SIZE as u64 ) . unwrap ( ) ;
393363 let collision_mapping =
394- match sys:: create_file_mapping ( & raw_backing. file , & collision_name) . unwrap ( ) {
364+ match sys:: create_file_mapping ( & raw_backing, & collision_name) . unwrap ( ) {
395365 CreatedMapping :: Created ( mapping) => mapping,
396366 CreatedMapping :: AlreadyExists => panic ! ( "random test mapping name collided" ) ,
397367 } ;
@@ -443,7 +413,7 @@ mod tests {
443413 }
444414
445415 #[ test]
446- fn owner_cleanup_removes_backing_file_after_existing_views_close ( ) {
416+ fn owner_cleanup_deletes_backing_file_and_preserves_existing_views ( ) {
447417 let owner = create ( SIZE ) . unwrap ( ) ;
448418 let id = owner. id ( ) . to_owned ( ) ;
449419 let DecodedId { backing_path : path, mapping_name } = decode_id ( & id) . unwrap ( ) ;
@@ -455,12 +425,12 @@ mod tests {
455425 drop ( owner) ;
456426
457427 assert ! ( !path. exists( ) ) ;
458- assert ! ( open( & id, SIZE ) . is_err( ) ) ;
459428 // SAFETY: The mapping remains live and no other test access is concurrent.
460429 unsafe { opened. as_ptr ( ) . write ( 17 ) } ;
461430 // SAFETY: The preceding write is complete and the mapping remains live.
462431 assert_eq ! ( unsafe { opened. as_ptr( ) . read( ) } , 17 ) ;
463432 drop ( opened) ;
433+ assert ! ( open( & id, SIZE ) . is_err( ) ) ;
464434
465435 assert ! (
466436 fs:: read_dir( backing_dir) . unwrap( ) . all( |entry| !entry
@@ -471,12 +441,45 @@ mod tests {
471441 ) ;
472442 }
473443
444+ #[ test]
445+ fn unsupported_sparse_errors_fall_back_to_extending_the_file ( ) {
446+ for code in [ ERROR_INVALID_FUNCTION , ERROR_NOT_SUPPORTED ] {
447+ let file = test_backing_file ( ) ;
448+ initialize_backing_file_with ( & file, SIZE as u64 , |_| {
449+ Err ( io:: Error :: from_raw_os_error ( code. cast_signed ( ) ) )
450+ } )
451+ . unwrap ( ) ;
452+
453+ assert_eq ! ( file. metadata( ) . unwrap( ) . len( ) , SIZE as u64 ) ;
454+ }
455+ }
456+
457+ #[ test]
458+ fn other_sparse_errors_do_not_extend_the_file ( ) {
459+ for code in [ ERROR_ACCESS_DENIED , ERROR_INVALID_PARAMETER ] {
460+ let file = test_backing_file ( ) ;
461+ let error = initialize_backing_file_with ( & file, SIZE as u64 , |_| {
462+ Err ( io:: Error :: from_raw_os_error ( code. cast_signed ( ) ) )
463+ } )
464+ . unwrap_err ( ) ;
465+
466+ assert_eq ! ( error. raw_os_error( ) , Some ( code. cast_signed( ) ) ) ;
467+ assert_eq ! ( file. metadata( ) . unwrap( ) . len( ) , 0 ) ;
468+ }
469+ }
470+
474471 #[ cfg( target_pointer_width = "64" ) ]
475472 #[ test]
476- fn four_gib_mapping_supports_endpoint_access ( ) {
473+ fn four_gib_mapping_is_sparse_and_supports_endpoint_access ( ) {
477474 const PRODUCTION_SIZE : usize = 4 * 1024 * 1024 * 1024 ;
475+ const MAX_ENDPOINT_ALLOCATION : u64 = 16 * 1024 * 1024 ;
478476
479477 let owner = create ( PRODUCTION_SIZE ) . unwrap ( ) ;
478+ let backing_file = owner. backing_file . as_ref ( ) . unwrap ( ) ;
479+ let ( logical_size, initial_allocation) = sys:: file_sizes ( backing_file) . unwrap ( ) ;
480+ assert_eq ! ( logical_size, PRODUCTION_SIZE as u64 ) ;
481+ assert ! ( initial_allocation < MAX_ENDPOINT_ALLOCATION ) ;
482+
480483 let opened = open ( owner. id ( ) , PRODUCTION_SIZE ) . unwrap ( ) ;
481484 // SAFETY: Both endpoint indexes are within the exact mapped length and
482485 // accesses are synchronized within this test.
@@ -486,5 +489,16 @@ mod tests {
486489 assert_eq ! ( opened. as_ptr( ) . read( ) , 17 ) ;
487490 assert_eq ! ( opened. as_ptr( ) . add( PRODUCTION_SIZE - 1 ) . read( ) , 29 ) ;
488491 }
492+
493+ let ( logical_size, endpoint_allocation) = sys:: file_sizes ( backing_file) . unwrap ( ) ;
494+ assert_eq ! ( logical_size, PRODUCTION_SIZE as u64 ) ;
495+ assert ! ( endpoint_allocation < MAX_ENDPOINT_ALLOCATION ) ;
496+ }
497+
498+ fn test_backing_file ( ) -> File {
499+ let backing_dir = create_backing_dir ( ) . unwrap ( ) ;
500+ let mapping_name = format ! ( "{MAPPING_NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
501+ let path = backing_path ( & backing_dir, & mapping_name) . unwrap ( ) ;
502+ create_backing_file ( & path) . unwrap ( )
489503 }
490504}
0 commit comments