@@ -19,8 +19,13 @@ const BACKING_DIR: &str = "vite-task-fspy";
1919pub struct Shm {
2020 id : String ,
2121 view : MappedView ,
22- _mapping : OwnedHandle ,
23- backing_file : BackingFile ,
22+ _mapping : Option < OwnedHandle > ,
23+ // Owner mappings keep this file alive until their view and mapping handle drop.
24+ #[ cfg_attr(
25+ not( test) ,
26+ expect( dead_code, reason = "the file is retained only for RAII cleanup" )
27+ ) ]
28+ backing_file : Option < File > ,
2429}
2530
2631/// A newly created shared-memory mapping.
@@ -29,7 +34,7 @@ pub struct CreatedShm {
2934 pub shm : Shm ,
3035}
3136
32- /// Creates a file-backed named mapping of `size` bytes.
37+ /// Creates a sparse, temporary file-backed named mapping of `size` bytes.
3338///
3439/// # Errors
3540///
@@ -52,14 +57,14 @@ fn create_with(size: usize, mut next_name: impl FnMut() -> String) -> io::Result
5257
5358fn create_named ( id : & str , size : u64 ) -> io:: Result < Option < Shm > > {
5459 let path = backing_path ( id) ?;
55- let backing_file = match BackingFile :: create ( path) {
60+ let backing_file = match create_backing_file ( & path) {
5661 Ok ( file) => file,
5762 Err ( error) if error. kind ( ) == io:: ErrorKind :: AlreadyExists => return Ok ( None ) ,
5863 Err ( error) => return Err ( error) ,
5964 } ;
60- backing_file . file . set_len ( size) ?;
65+ initialize_backing_file ( & backing_file , size) ?;
6166
62- let mapping = match sys:: create_file_mapping ( & backing_file. file , id) ? {
67+ let mapping = match sys:: create_file_mapping ( & backing_file, id) ? {
6368 CreatedMapping :: Created ( mapping) => mapping,
6469 CreatedMapping :: AlreadyExists => return Ok ( None ) ,
6570 } ;
@@ -68,29 +73,28 @@ fn create_named(id: &str, size: u64) -> io::Result<Option<Shm>> {
6873 } ) ?;
6974 let view = MappedView :: new ( & mapping, len) ?;
7075
71- Ok ( Some ( Shm { id : id. to_owned ( ) , view, _mapping : mapping, backing_file } ) )
76+ Ok ( Some ( Shm {
77+ id : id. to_owned ( ) ,
78+ view,
79+ _mapping : Some ( mapping) ,
80+ backing_file : Some ( backing_file) ,
81+ } ) )
7282}
7383
7484/// Opens the named mapping identified by `id`.
7585///
7686/// # Errors
7787///
78- /// Returns an error if the identifier is invalid, the owner has torn down the
79- /// 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 .
8090pub fn open ( id : & str , size : usize ) -> io:: Result < Shm > {
81- let expected_size = valid_size ( size) ?;
82- let path = backing_path ( id) ?;
83- let backing_file = BackingFile :: open ( path) ?;
84- if backing_file. file . metadata ( ) ?. len ( ) != expected_size {
85- return Err ( io:: Error :: new (
86- io:: ErrorKind :: InvalidData ,
87- "shared-memory backing file has an unexpected size" ,
88- ) ) ;
89- }
91+ valid_size ( size) ?;
92+ validate_id ( id) ?;
9093
9194 let mapping = sys:: open_file_mapping ( id) ?;
9295 let view = MappedView :: new ( & mapping, size) ?;
93- 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 } )
9498}
9599
96100fn valid_size ( size : usize ) -> io:: Result < u64 > {
@@ -109,83 +113,37 @@ fn backing_dir() -> PathBuf {
109113}
110114
111115fn backing_path ( id : & str ) -> io:: Result < PathBuf > {
116+ let suffix = validate_id ( id) ?;
117+ Ok ( backing_dir ( ) . join ( format ! ( "{suffix}.shm" ) ) )
118+ }
119+
120+ fn validate_id ( id : & str ) -> io:: Result < & str > {
112121 let suffix = id. strip_prefix ( NAME_PREFIX ) . ok_or_else ( invalid_id) ?;
113122 let uuid = Uuid :: parse_str ( suffix) . map_err ( |_| invalid_id ( ) ) ?;
114123 if uuid. hyphenated ( ) . to_string ( ) != suffix {
115124 return Err ( invalid_id ( ) ) ;
116125 }
117- Ok ( backing_dir ( ) . join ( format ! ( "{ suffix}.shm" ) ) )
126+ Ok ( suffix)
118127}
119128
120129fn invalid_id ( ) -> io:: Error {
121130 io:: Error :: new ( io:: ErrorKind :: InvalidInput , "invalid Windows shared-memory identifier" )
122131}
123132
124- struct BackingFile {
125- file : File ,
126- owner_path : Option < PathBuf > ,
127- }
128-
129- impl BackingFile {
130- fn create ( path : PathBuf ) -> io:: Result < Self > {
131- let file = OpenOptions :: new ( )
132- . read ( true )
133- . write ( true )
134- . create_new ( true )
135- . share_mode ( sys:: SHARE_ALL )
136- . attributes ( sys:: TEMPORARY )
137- . open ( & path) ?;
138- Ok ( Self { file, owner_path : Some ( path) } )
139- }
140-
141- fn open ( path : PathBuf ) -> io:: Result < Self > {
142- let file = OpenOptions :: new ( )
143- . read ( true )
144- . write ( true )
145- . share_mode ( sys:: SHARE_ALL )
146- . attributes ( sys:: TEMPORARY )
147- . open ( path) ?;
148- Ok ( Self { file, owner_path : None } )
149- }
150-
151- fn unlink ( & mut self ) {
152- let Some ( path) = self . owner_path . take ( ) else {
153- return ;
154- } ;
155-
156- let deletion_file = OpenOptions :: new ( )
157- . access_mode ( sys:: DELETE_ACCESS )
158- . share_mode ( sys:: SHARE_ALL )
159- . attributes ( sys:: DELETE_ON_CLOSE )
160- . open ( & path) ;
161- if let Ok ( deletion_file) = deletion_file {
162- let deleted_path = deleted_path ( & path) ;
163- if fs:: rename ( & path, deleted_path) . is_err ( ) {
164- let _ = fs:: remove_file ( & path) ;
165- }
166- drop ( deletion_file) ;
167- } else {
168- let _ = fs:: remove_file ( path) ;
169- }
170- }
171- }
172-
173- impl Drop for BackingFile {
174- fn drop ( & mut self ) {
175- self . unlink ( ) ;
176- }
177- }
178-
179- fn deleted_path ( path : & Path ) -> PathBuf {
180- path. with_extension ( format ! ( "deleted-{}" , Uuid :: new_v4( ) ) )
133+ fn create_backing_file ( path : & Path ) -> io:: Result < File > {
134+ OpenOptions :: new ( )
135+ . read ( true )
136+ . write ( true )
137+ . create_new ( true )
138+ . share_mode ( sys:: SHARE_ALL )
139+ . attributes ( sys:: TEMPORARY )
140+ . custom_flags ( sys:: DELETE_ON_CLOSE )
141+ . open ( path)
181142}
182143
183- impl Drop for Shm {
184- fn drop ( & mut self ) {
185- // Remove the public backing-file name before the mapping handle drops,
186- // preventing opens that begin after owner teardown.
187- self . backing_file . unlink ( ) ;
188- }
144+ fn initialize_backing_file ( file : & File , size : u64 ) -> io:: Result < ( ) > {
145+ sys:: set_sparse ( file) ?;
146+ file. set_len ( size)
189147}
190148
191149#[ expect( clippy:: len_without_is_empty, reason = "shared-memory mappings are always non-empty" ) ]
@@ -240,7 +198,7 @@ mod tests {
240198 }
241199
242200 #[ test]
243- fn malformed_ids_and_size_mismatches_are_rejected ( ) {
201+ fn malformed_ids_and_invalid_sizes_are_rejected ( ) {
244202 for id in [
245203 "vite-task-fspy-not-local" ,
246204 r"Local\vite-task-fspy-not-a-uuid" ,
@@ -252,8 +210,7 @@ mod tests {
252210
253211 let owner = create ( SIZE ) . unwrap ( ) . shm ;
254212 assert_eq ! ( open( owner. id( ) , 0 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidInput ) ;
255- assert_eq ! ( open( owner. id( ) , SIZE / 2 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidData ) ;
256- assert_eq ! ( open( owner. id( ) , SIZE + 1 ) . err( ) . unwrap( ) . kind( ) , io:: ErrorKind :: InvalidData ) ;
213+ assert ! ( open( owner. id( ) , SIZE + 1 ) . is_err( ) ) ;
257214 }
258215
259216 #[ test]
@@ -262,10 +219,10 @@ mod tests {
262219 let collision_name = format ! ( "{NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
263220 let raw_backing_name = format ! ( "{NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
264221 let raw_backing_path = backing_path ( & raw_backing_name) . unwrap ( ) ;
265- let raw_backing = BackingFile :: create ( raw_backing_path) . unwrap ( ) ;
266- raw_backing . file . set_len ( SIZE as u64 ) . unwrap ( ) ;
222+ let raw_backing = create_backing_file ( & raw_backing_path) . unwrap ( ) ;
223+ initialize_backing_file ( & raw_backing , SIZE as u64 ) . unwrap ( ) ;
267224 let collision_mapping =
268- match sys:: create_file_mapping ( & raw_backing. file , & collision_name) . unwrap ( ) {
225+ match sys:: create_file_mapping ( & raw_backing, & collision_name) . unwrap ( ) {
269226 CreatedMapping :: Created ( mapping) => mapping,
270227 CreatedMapping :: AlreadyExists => panic ! ( "random test mapping name collided" ) ,
271228 } ;
@@ -282,7 +239,16 @@ mod tests {
282239 }
283240
284241 #[ test]
285- fn owner_cleanup_removes_backing_file_after_existing_views_close ( ) {
242+ fn sender_opens_the_named_mapping_without_a_backing_file ( ) {
243+ let owner = create ( SIZE ) . unwrap ( ) . shm ;
244+ let opened = open ( owner. id ( ) , SIZE ) . unwrap ( ) ;
245+
246+ assert ! ( owner. backing_file. is_some( ) ) ;
247+ assert ! ( opened. backing_file. is_none( ) ) ;
248+ }
249+
250+ #[ test]
251+ fn owner_cleanup_deletes_backing_file_and_preserves_existing_views ( ) {
286252 let owner = create ( SIZE ) . unwrap ( ) . shm ;
287253 let id = owner. id ( ) . to_owned ( ) ;
288254 let path = backing_path ( & id) . unwrap ( ) ;
@@ -292,12 +258,12 @@ mod tests {
292258 drop ( owner) ;
293259
294260 assert ! ( !path. exists( ) ) ;
295- assert ! ( open( & id, SIZE ) . is_err( ) ) ;
296261 // SAFETY: The mapping remains live and no other test access is concurrent.
297262 unsafe { opened. as_ptr ( ) . write ( 17 ) } ;
298263 // SAFETY: The preceding write is complete and the mapping remains live.
299264 assert_eq ! ( unsafe { opened. as_ptr( ) . read( ) } , 17 ) ;
300265 drop ( opened) ;
266+ assert ! ( open( & id, SIZE ) . is_err( ) ) ;
301267
302268 let suffix = id. strip_prefix ( NAME_PREFIX ) . unwrap ( ) ;
303269 assert ! (
@@ -309,12 +275,34 @@ mod tests {
309275 ) ;
310276 }
311277
278+ #[ test]
279+ fn sparse_failure_does_not_extend_the_file ( ) {
280+ fs:: create_dir_all ( backing_dir ( ) ) . unwrap ( ) ;
281+ let id = format ! ( "{NAME_PREFIX}{}" , Uuid :: new_v4( ) ) ;
282+ let path = backing_path ( & id) . unwrap ( ) ;
283+ File :: create ( & path) . unwrap ( ) ;
284+ let read_only =
285+ OpenOptions :: new ( ) . read ( true ) . share_mode ( sys:: SHARE_ALL ) . open ( & path) . unwrap ( ) ;
286+
287+ assert ! ( initialize_backing_file( & read_only, 4 * 1024 * 1024 * 1024 ) . is_err( ) ) ;
288+ assert_eq ! ( read_only. metadata( ) . unwrap( ) . len( ) , 0 ) ;
289+
290+ drop ( read_only) ;
291+ fs:: remove_file ( path) . unwrap ( ) ;
292+ }
293+
312294 #[ cfg( target_pointer_width = "64" ) ]
313295 #[ test]
314- fn four_gib_mapping_supports_endpoint_access ( ) {
296+ fn four_gib_mapping_is_sparse_and_supports_endpoint_access ( ) {
315297 const PRODUCTION_SIZE : usize = 4 * 1024 * 1024 * 1024 ;
298+ const MAX_ENDPOINT_ALLOCATION : u64 = 16 * 1024 * 1024 ;
316299
317300 let owner = create ( PRODUCTION_SIZE ) . unwrap ( ) . shm ;
301+ let backing_file = owner. backing_file . as_ref ( ) . unwrap ( ) ;
302+ let ( logical_size, initial_allocation) = sys:: file_sizes ( backing_file) . unwrap ( ) ;
303+ assert_eq ! ( logical_size, PRODUCTION_SIZE as u64 ) ;
304+ assert ! ( initial_allocation < MAX_ENDPOINT_ALLOCATION ) ;
305+
318306 let opened = open ( owner. id ( ) , PRODUCTION_SIZE ) . unwrap ( ) ;
319307 // SAFETY: Both endpoint indexes are within the exact mapped length and
320308 // accesses are synchronized within this test.
@@ -324,5 +312,9 @@ mod tests {
324312 assert_eq ! ( opened. as_ptr( ) . read( ) , 17 ) ;
325313 assert_eq ! ( opened. as_ptr( ) . add( PRODUCTION_SIZE - 1 ) . read( ) , 29 ) ;
326314 }
315+
316+ let ( logical_size, endpoint_allocation) = sys:: file_sizes ( backing_file) . unwrap ( ) ;
317+ assert_eq ! ( logical_size, PRODUCTION_SIZE as u64 ) ;
318+ assert ! ( endpoint_allocation < MAX_ENDPOINT_ALLOCATION ) ;
327319 }
328320}
0 commit comments