@@ -25,9 +25,16 @@ 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
3340/// A newly created shared-memory mapping.
@@ -36,7 +43,7 @@ pub struct CreatedShm {
3643 pub shm : Shm ,
3744}
3845
39- /// Creates a file-backed named mapping of `size` bytes.
46+ /// Creates a sparse, temporary file-backed named mapping of `size` bytes.
4047///
4148/// # Errors
4249///
@@ -60,14 +67,14 @@ fn create_with(size: usize, mut next_name: impl FnMut() -> String) -> io::Result
6067fn create_named ( backing_dir : & Path , mapping_name : & str , size : u64 ) -> io:: Result < Option < Shm > > {
6168 let path = backing_path ( backing_dir, mapping_name) ?;
6269 let id = encode_id ( & path, mapping_name) ?;
63- let backing_file = match BackingFile :: create ( path) {
70+ let backing_file = match create_backing_file ( & path) {
6471 Ok ( file) => file,
6572 Err ( error) if error. kind ( ) == io:: ErrorKind :: AlreadyExists => return Ok ( None ) ,
6673 Err ( error) => return Err ( error) ,
6774 } ;
68- backing_file . file . set_len ( size) ?;
75+ initialize_backing_file ( & backing_file , size) ?;
6976
70- let mapping = match sys:: create_file_mapping ( & backing_file. file , mapping_name) ? {
77+ let mapping = match sys:: create_file_mapping ( & backing_file, mapping_name) ? {
7178 CreatedMapping :: Created ( mapping) => mapping,
7279 CreatedMapping :: AlreadyExists => return Ok ( None ) ,
7380 } ;
@@ -76,29 +83,23 @@ fn create_named(backing_dir: &Path, mapping_name: &str, size: u64) -> io::Result
7683 } ) ?;
7784 let view = MappedView :: new ( & mapping, len) ?;
7885
79- Ok ( Some ( Shm { id, view, _mapping : mapping, backing_file } ) )
86+ Ok ( Some ( Shm { id, view, _mapping : Some ( mapping) , backing_file : Some ( backing_file ) } ) )
8087}
8188
8289/// Opens the named mapping identified by `id`.
8390///
8491/// # Errors
8592///
86- /// Returns an error if the identifier is invalid, the owner has torn down the
87- /// mapping, or its backing file has a different size .
93+ /// Returns an error if the identifier is invalid, the mapping is unavailable,
94+ /// or `size` cannot be mapped from it .
8895pub fn open ( id : & str , size : usize ) -> io:: Result < Shm > {
89- let expected_size = valid_size ( size) ?;
90- let DecodedId { backing_path, mapping_name } = decode_id ( id) ?;
91- let backing_file = BackingFile :: open ( backing_path) ?;
92- if backing_file. file . metadata ( ) ?. len ( ) != expected_size {
93- return Err ( io:: Error :: new (
94- io:: ErrorKind :: InvalidData ,
95- "shared-memory backing file has an unexpected size" ,
96- ) ) ;
97- }
96+ valid_size ( size) ?;
97+ let DecodedId { mapping_name, .. } = decode_id ( id) ?;
9898
9999 let mapping = sys:: open_file_mapping ( & mapping_name) ?;
100100 let view = MappedView :: new ( & mapping, size) ?;
101- Ok ( Shm { id : id. to_owned ( ) , view, _mapping : mapping, backing_file } )
101+ drop ( mapping) ;
102+ Ok ( Shm { id : id. to_owned ( ) , view, _mapping : None , backing_file : None } )
102103}
103104
104105fn valid_size ( size : usize ) -> io:: Result < u64 > {
@@ -144,6 +145,7 @@ fn encode_id(backing_path: &Path, mapping_name: &str) -> io::Result<String> {
144145}
145146
146147struct DecodedId {
148+ #[ cfg( test) ]
147149 backing_path : PathBuf ,
148150 mapping_name : String ,
149151}
@@ -172,7 +174,11 @@ fn decode_id(id: &str) -> io::Result<DecodedId> {
172174 let mapping_name =
173175 String :: from_utf8 ( decode_id_part ( encoded_mapping_name) ?) . map_err ( |_| invalid_id ( ) ) ?;
174176 validate_id_parts ( & backing_path, & mapping_name) ?;
175- Ok ( DecodedId { backing_path, mapping_name } )
177+ Ok ( DecodedId {
178+ #[ cfg( test) ]
179+ backing_path,
180+ mapping_name,
181+ } )
176182}
177183
178184fn decode_id_part ( encoded : & str ) -> io:: Result < Vec < u8 > > {
@@ -197,71 +203,32 @@ fn invalid_id() -> io::Error {
197203 io:: Error :: new ( io:: ErrorKind :: InvalidInput , "invalid Windows shared-memory identifier" )
198204}
199205
200- struct BackingFile {
201- file : File ,
202- owner_path : Option < PathBuf > ,
206+ fn create_backing_file ( path : & Path ) -> io:: Result < File > {
207+ OpenOptions :: new ( )
208+ . read ( true )
209+ . write ( true )
210+ . create_new ( true )
211+ . share_mode ( sys:: SHARE_ALL )
212+ . attributes ( sys:: TEMPORARY )
213+ . custom_flags ( sys:: DELETE_ON_CLOSE )
214+ . open ( path)
203215}
204216
205- impl BackingFile {
206- fn create ( path : PathBuf ) -> io:: Result < Self > {
207- let file = OpenOptions :: new ( )
208- . read ( true )
209- . write ( true )
210- . create_new ( true )
211- . share_mode ( sys:: SHARE_ALL )
212- . attributes ( sys:: TEMPORARY )
213- . open ( & path) ?;
214- Ok ( Self { file, owner_path : Some ( path) } )
215- }
216-
217- fn open ( path : PathBuf ) -> io:: Result < Self > {
218- let file = OpenOptions :: new ( )
219- . read ( true )
220- . write ( true )
221- . share_mode ( sys:: SHARE_ALL )
222- . attributes ( sys:: TEMPORARY )
223- . open ( path) ?;
224- Ok ( Self { file, owner_path : None } )
225- }
226-
227- fn unlink ( & mut self ) {
228- let Some ( path) = self . owner_path . take ( ) else {
229- return ;
230- } ;
231-
232- let deletion_file = OpenOptions :: new ( )
233- . access_mode ( sys:: DELETE_ACCESS )
234- . share_mode ( sys:: SHARE_ALL )
235- . attributes ( sys:: DELETE_ON_CLOSE )
236- . open ( & path) ;
237- if let Ok ( deletion_file) = deletion_file {
238- let deleted_path = deleted_path ( & path) ;
239- if fs:: rename ( & path, deleted_path) . is_err ( ) {
240- let _ = fs:: remove_file ( & path) ;
241- }
242- drop ( deletion_file) ;
243- } else {
244- let _ = fs:: remove_file ( path) ;
245- }
246- }
217+ fn initialize_backing_file ( file : & File , size : u64 ) -> io:: Result < ( ) > {
218+ initialize_backing_file_with ( file, size, sys:: set_sparse)
247219}
248220
249- impl Drop for BackingFile {
250- fn drop ( & mut self ) {
251- self . unlink ( ) ;
252- }
253- }
254-
255- fn deleted_path ( path : & Path ) -> PathBuf {
256- path. with_extension ( format ! ( "deleted-{}" , Uuid :: new_v4( ) ) )
257- }
258-
259- impl Drop for Shm {
260- fn drop ( & mut self ) {
261- // Remove the public backing-file name before the mapping handle drops,
262- // preventing opens that begin after owner teardown.
263- self . backing_file . unlink ( ) ;
221+ fn initialize_backing_file_with (
222+ file : & File ,
223+ size : u64 ,
224+ set_sparse : impl FnOnce ( & File ) -> io:: Result < ( ) > ,
225+ ) -> io:: Result < ( ) > {
226+ if let Err ( error) = set_sparse ( file)
227+ && !sys:: is_sparse_unsupported ( & error)
228+ {
229+ return Err ( error) ;
264230 }
231+ file. set_len ( size)
265232}
266233
267234#[ expect( clippy:: len_without_is_empty, reason = "shared-memory mappings are always non-empty" ) ]
@@ -303,6 +270,9 @@ mod tests {
303270 use std:: { ffi:: OsString , fs, process:: Command } ;
304271
305272 use subprocess_test:: command_for_fn;
273+ use windows_sys:: Win32 :: Foundation :: {
274+ ERROR_ACCESS_DENIED , ERROR_INVALID_FUNCTION , ERROR_INVALID_PARAMETER , ERROR_NOT_SUPPORTED ,
275+ } ;
306276
307277 use super :: * ;
308278
@@ -338,7 +308,7 @@ mod tests {
338308 }
339309
340310 #[ test]
341- fn malformed_ids_and_size_mismatches_are_rejected ( ) {
311+ fn malformed_ids_and_invalid_sizes_are_rejected ( ) {
342312 let owner = create ( SIZE ) . unwrap ( ) . shm ;
343313 let decoded = decode_id ( owner. id ( ) ) . unwrap ( ) ;
344314 let encoded_path = URL_SAFE_NO_PAD . encode (
@@ -384,8 +354,7 @@ mod tests {
384354 }
385355
386356 assert_eq ! ( open( owner. id( ) , 0 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidInput ) ;
387- assert_eq ! ( open( owner. id( ) , SIZE / 2 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidData ) ;
388- assert_eq ! ( open( owner. id( ) , SIZE + 1 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidData ) ;
357+ assert ! ( open( owner. id( ) , SIZE + 1 ) . is_err( ) ) ;
389358 }
390359
391360 #[ test]
@@ -394,10 +363,10 @@ mod tests {
394363 let collision_name = format ! ( "{MAPPING_NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
395364 let raw_backing_name = format ! ( "{MAPPING_NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
396365 let raw_backing_path = backing_path ( & backing_dir, & raw_backing_name) . unwrap ( ) ;
397- let raw_backing = BackingFile :: create ( raw_backing_path) . unwrap ( ) ;
398- raw_backing . file . set_len ( SIZE as u64 ) . unwrap ( ) ;
366+ let raw_backing = create_backing_file ( & raw_backing_path) . unwrap ( ) ;
367+ initialize_backing_file ( & raw_backing , SIZE as u64 ) . unwrap ( ) ;
399368 let collision_mapping =
400- match sys:: create_file_mapping ( & raw_backing. file , & collision_name) . unwrap ( ) {
369+ match sys:: create_file_mapping ( & raw_backing, & collision_name) . unwrap ( ) {
401370 CreatedMapping :: Created ( mapping) => mapping,
402371 CreatedMapping :: AlreadyExists => panic ! ( "random test mapping name collided" ) ,
403372 } ;
@@ -449,7 +418,16 @@ mod tests {
449418 }
450419
451420 #[ test]
452- fn owner_cleanup_removes_backing_file_after_existing_views_close ( ) {
421+ fn sender_opens_the_named_mapping_without_a_backing_file ( ) {
422+ let owner = create ( SIZE ) . unwrap ( ) . shm ;
423+ let opened = open ( owner. id ( ) , SIZE ) . unwrap ( ) ;
424+
425+ assert ! ( owner. backing_file. is_some( ) ) ;
426+ assert ! ( opened. backing_file. is_none( ) ) ;
427+ }
428+
429+ #[ test]
430+ fn owner_cleanup_deletes_backing_file_and_preserves_existing_views ( ) {
453431 let owner = create ( SIZE ) . unwrap ( ) . shm ;
454432 let id = owner. id ( ) . to_owned ( ) ;
455433 let DecodedId { backing_path : path, mapping_name } = decode_id ( & id) . unwrap ( ) ;
@@ -461,12 +439,12 @@ mod tests {
461439 drop ( owner) ;
462440
463441 assert ! ( !path. exists( ) ) ;
464- assert ! ( open( & id, SIZE ) . is_err( ) ) ;
465442 // SAFETY: The mapping remains live and no other test access is concurrent.
466443 unsafe { opened. as_ptr ( ) . write ( 17 ) } ;
467444 // SAFETY: The preceding write is complete and the mapping remains live.
468445 assert_eq ! ( unsafe { opened. as_ptr( ) . read( ) } , 17 ) ;
469446 drop ( opened) ;
447+ assert ! ( open( & id, SIZE ) . is_err( ) ) ;
470448
471449 assert ! (
472450 fs:: read_dir( backing_dir) . unwrap( ) . all( |entry| !entry
@@ -477,12 +455,45 @@ mod tests {
477455 ) ;
478456 }
479457
458+ #[ test]
459+ fn unsupported_sparse_errors_fall_back_to_extending_the_file ( ) {
460+ for code in [ ERROR_INVALID_FUNCTION , ERROR_NOT_SUPPORTED ] {
461+ let file = test_backing_file ( ) ;
462+ initialize_backing_file_with ( & file, SIZE as u64 , |_| {
463+ Err ( io:: Error :: from_raw_os_error ( code. cast_signed ( ) ) )
464+ } )
465+ . unwrap ( ) ;
466+
467+ assert_eq ! ( file. metadata( ) . unwrap( ) . len( ) , SIZE as u64 ) ;
468+ }
469+ }
470+
471+ #[ test]
472+ fn other_sparse_errors_do_not_extend_the_file ( ) {
473+ for code in [ ERROR_ACCESS_DENIED , ERROR_INVALID_PARAMETER ] {
474+ let file = test_backing_file ( ) ;
475+ let error = initialize_backing_file_with ( & file, SIZE as u64 , |_| {
476+ Err ( io:: Error :: from_raw_os_error ( code. cast_signed ( ) ) )
477+ } )
478+ . unwrap_err ( ) ;
479+
480+ assert_eq ! ( error. raw_os_error( ) , Some ( code. cast_signed( ) ) ) ;
481+ assert_eq ! ( file. metadata( ) . unwrap( ) . len( ) , 0 ) ;
482+ }
483+ }
484+
480485 #[ cfg( target_pointer_width = "64" ) ]
481486 #[ test]
482- fn four_gib_mapping_supports_endpoint_access ( ) {
487+ fn four_gib_mapping_is_sparse_and_supports_endpoint_access ( ) {
483488 const PRODUCTION_SIZE : usize = 4 * 1024 * 1024 * 1024 ;
489+ const MAX_ENDPOINT_ALLOCATION : u64 = 16 * 1024 * 1024 ;
484490
485491 let owner = create ( PRODUCTION_SIZE ) . unwrap ( ) . shm ;
492+ let backing_file = owner. backing_file . as_ref ( ) . unwrap ( ) ;
493+ let ( logical_size, initial_allocation) = sys:: file_sizes ( backing_file) . unwrap ( ) ;
494+ assert_eq ! ( logical_size, PRODUCTION_SIZE as u64 ) ;
495+ assert ! ( initial_allocation < MAX_ENDPOINT_ALLOCATION ) ;
496+
486497 let opened = open ( owner. id ( ) , PRODUCTION_SIZE ) . unwrap ( ) ;
487498 // SAFETY: Both endpoint indexes are within the exact mapped length and
488499 // accesses are synchronized within this test.
@@ -492,5 +503,16 @@ mod tests {
492503 assert_eq ! ( opened. as_ptr( ) . read( ) , 17 ) ;
493504 assert_eq ! ( opened. as_ptr( ) . add( PRODUCTION_SIZE - 1 ) . read( ) , 29 ) ;
494505 }
506+
507+ let ( logical_size, endpoint_allocation) = sys:: file_sizes ( backing_file) . unwrap ( ) ;
508+ assert_eq ! ( logical_size, PRODUCTION_SIZE as u64 ) ;
509+ assert ! ( endpoint_allocation < MAX_ENDPOINT_ALLOCATION ) ;
510+ }
511+
512+ fn test_backing_file ( ) -> File {
513+ let backing_dir = create_backing_dir ( ) . unwrap ( ) ;
514+ let mapping_name = format ! ( "{MAPPING_NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
515+ let path = backing_path ( & backing_dir, & mapping_name) . unwrap ( ) ;
516+ create_backing_file ( & path) . unwrap ( )
495517 }
496518}
0 commit comments