1use std::fs::{File, OpenOptions};
7use std::io;
8use std::os::unix::fs::PermissionsExt;
9use std::os::unix::io::{AsRawFd, FromRawFd, IntoRawFd, OwnedFd, RawFd};
10use std::path::{Path, PathBuf};
11
12use crate::Region;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
16pub enum FileCleanup {
17 Manual,
19 Auto,
23}
24
25pub struct MmapRegion {
29 ptr: *mut u8,
31 len: usize,
33 #[allow(dead_code)]
35 file: File,
36 path: PathBuf,
38 owns_file: bool,
40}
41
42impl MmapRegion {
43 pub fn create(path: &Path, size: usize, cleanup: FileCleanup) -> io::Result<Self> {
51 if size == 0 {
52 return Err(io::Error::new(
53 io::ErrorKind::InvalidInput,
54 "size must be > 0",
55 ));
56 }
57
58 let file = OpenOptions::new()
60 .read(true)
61 .write(true)
62 .create(true)
63 .truncate(true)
64 .open(path)
65 .map_err(|e| {
66 let msg = std::format!("Failed to create SHM file at {}: {}", path.display(), e);
67 io::Error::new(e.kind(), msg)
68 })?;
69
70 file.set_permissions(std::fs::Permissions::from_mode(0o666))?;
74
75 file.set_len(size as u64)?;
77
78 let ptr = unsafe {
80 libc::mmap(
81 std::ptr::null_mut(),
82 size,
83 libc::PROT_READ | libc::PROT_WRITE,
84 libc::MAP_SHARED,
85 file.as_raw_fd(),
86 0,
87 )
88 };
89
90 if ptr == libc::MAP_FAILED {
91 return Err(io::Error::last_os_error());
92 }
93
94 let path_buf = path.to_path_buf();
95
96 if cleanup == FileCleanup::Auto {
100 std::fs::remove_file(&path_buf)?;
101 }
102
103 Ok(Self {
104 ptr: ptr as *mut u8,
105 len: size,
106 file,
107 path: path_buf,
108 owns_file: cleanup == FileCleanup::Manual,
109 })
110 }
111
112 pub fn attach(path: &Path) -> io::Result<Self> {
119 let file = OpenOptions::new()
121 .read(true)
122 .write(true)
123 .open(path)
124 .map_err(|e| {
125 let msg = std::format!("Failed to open SHM file at {}: {}", path.display(), e);
126 io::Error::new(e.kind(), msg)
127 })?;
128
129 let metadata = file.metadata()?;
131 let size = metadata.len() as usize;
132
133 if size == 0 {
134 return Err(io::Error::new(
135 io::ErrorKind::InvalidData,
136 "segment file is empty",
137 ));
138 }
139
140 let ptr = unsafe {
142 libc::mmap(
143 std::ptr::null_mut(),
144 size,
145 libc::PROT_READ | libc::PROT_WRITE,
146 libc::MAP_SHARED,
147 file.as_raw_fd(),
148 0,
149 )
150 };
151
152 if ptr == libc::MAP_FAILED {
153 return Err(io::Error::last_os_error());
154 }
155
156 Ok(Self {
157 ptr: ptr as *mut u8,
158 len: size,
159 file,
160 path: path.to_path_buf(),
161 owns_file: false, })
163 }
164
165 pub fn attach_fd(fd: OwnedFd, size: usize) -> io::Result<Self> {
170 if size == 0 {
171 return Err(io::Error::new(
172 io::ErrorKind::InvalidInput,
173 "size must be > 0",
174 ));
175 }
176
177 let raw_fd = fd.as_raw_fd();
178 let ptr = unsafe {
179 libc::mmap(
180 std::ptr::null_mut(),
181 size,
182 libc::PROT_READ | libc::PROT_WRITE,
183 libc::MAP_SHARED,
184 raw_fd,
185 0,
186 )
187 };
188
189 if ptr == libc::MAP_FAILED {
190 return Err(io::Error::last_os_error());
191 }
192
193 let file = unsafe { File::from_raw_fd(fd.into_raw_fd()) };
195
196 Ok(Self {
197 ptr: ptr as *mut u8,
198 len: size,
199 file,
200 path: PathBuf::new(),
201 owns_file: false,
202 })
203 }
204
205 pub fn as_raw_fd(&self) -> RawFd {
207 self.file.as_raw_fd()
208 }
209
210 #[inline]
212 pub fn region(&self) -> Region {
213 unsafe { Region::from_raw(self.ptr, self.len) }
215 }
216
217 #[inline]
219 pub fn len(&self) -> usize {
220 self.len
221 }
222
223 #[inline]
225 pub fn is_empty(&self) -> bool {
226 self.len == 0
227 }
228
229 #[inline]
231 pub fn path(&self) -> &Path {
232 &self.path
233 }
234
235 pub fn take_ownership(&mut self) {
239 self.owns_file = true;
240 }
241
242 pub fn release_ownership(&mut self) {
246 self.owns_file = false;
247 }
248
249 pub fn resize(&mut self, new_size: usize) -> io::Result<()> {
261 if new_size < self.len {
262 return Err(io::Error::new(
263 io::ErrorKind::InvalidInput,
264 "shrinking is not supported",
265 ));
266 }
267 if new_size == self.len {
268 return Ok(()); }
270
271 self.file.set_len(new_size as u64)?;
273
274 let new_ptr = unsafe {
277 libc::mmap(
278 std::ptr::null_mut(),
279 new_size,
280 libc::PROT_READ | libc::PROT_WRITE,
281 libc::MAP_SHARED,
282 self.file.as_raw_fd(),
283 0,
284 )
285 };
286
287 if new_ptr == libc::MAP_FAILED {
288 return Err(io::Error::last_os_error());
289 }
290
291 unsafe { libc::munmap(self.ptr as *mut libc::c_void, self.len) };
293
294 self.ptr = new_ptr as *mut u8;
295 self.len = new_size;
296 Ok(())
297 }
298
299 pub fn check_and_remap(&mut self) -> io::Result<bool> {
308 let file_size = self.file.metadata()?.len() as usize;
309 if file_size > self.len {
310 self.resize(file_size)?;
311 Ok(true)
312 } else {
313 Ok(false)
314 }
315 }
316}
317
318impl Drop for MmapRegion {
319 fn drop(&mut self) {
320 unsafe {
322 libc::munmap(self.ptr as *mut libc::c_void, self.len);
323 }
324
325 if self.owns_file {
328 let _ = std::fs::remove_file(&self.path);
329 }
330 }
331}
332
333unsafe impl Send for MmapRegion {}
336unsafe impl Sync for MmapRegion {}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341
342 #[test]
343 fn test_create_and_attach() {
344 let dir = tempfile::tempdir().unwrap();
345 let path = dir.path().join("test.shm");
346
347 let region1 = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
349 assert_eq!(region1.len(), 4096);
350 assert!(path.exists());
351
352 let data = region1.region();
354 unsafe {
355 std::ptr::write(data.as_ptr(), 0x42);
356 std::ptr::write(data.as_ptr().add(1), 0x43);
357 }
358
359 let region2 = MmapRegion::attach(&path).unwrap();
361 assert_eq!(region2.len(), 4096);
362
363 let data2 = region2.region();
365 unsafe {
366 assert_eq!(std::ptr::read(data2.as_ptr()), 0x42);
367 assert_eq!(std::ptr::read(data2.as_ptr().add(1)), 0x43);
368 }
369 }
370
371 #[test]
372 fn test_cleanup_on_drop() {
373 let dir = tempfile::tempdir().unwrap();
374 let path = dir.path().join("cleanup.shm");
375
376 {
377 let _region = MmapRegion::create(&path, 1024, FileCleanup::Manual).unwrap();
378 assert!(path.exists());
379 }
380
381 assert!(!path.exists());
383 }
384
385 #[test]
386 fn test_attached_does_not_cleanup() {
387 let dir = tempfile::tempdir().unwrap();
388 let path = dir.path().join("attached.shm");
389
390 let owner = MmapRegion::create(&path, 1024, FileCleanup::Manual).unwrap();
391
392 {
393 let _attached = MmapRegion::attach(&path).unwrap();
394 assert!(path.exists());
395 }
396
397 assert!(path.exists());
399
400 drop(owner);
402 assert!(!path.exists());
403 }
404
405 #[test]
406 fn test_shared_writes() {
407 let dir = tempfile::tempdir().unwrap();
408 let path = dir.path().join("shared.shm");
409
410 let region1 = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
411 let region2 = MmapRegion::attach(&path).unwrap();
412
413 let data2 = region2.region();
415 unsafe {
416 std::ptr::write(data2.as_ptr().add(100), 0xAB);
417 }
418
419 let data1 = region1.region();
421 unsafe {
422 assert_eq!(std::ptr::read(data1.as_ptr().add(100)), 0xAB);
423 }
424 }
425
426 #[test]
427 fn test_permissions() {
428 let dir = tempfile::tempdir().unwrap();
429 let path = dir.path().join("perms.shm");
430
431 let _region = MmapRegion::create(&path, 1024, FileCleanup::Manual).unwrap();
432
433 let metadata = std::fs::metadata(&path).unwrap();
434 let mode = metadata.permissions().mode() & 0o777;
435 assert_eq!(mode, 0o666);
436 }
437
438 #[test]
439 fn test_zero_size_rejected() {
440 let dir = tempfile::tempdir().unwrap();
441 let path = dir.path().join("zero.shm");
442
443 let result = MmapRegion::create(&path, 0, FileCleanup::Manual);
444 assert!(result.is_err());
445 }
446
447 #[test]
448 fn test_resize_grows_region() {
449 let dir = tempfile::tempdir().unwrap();
450 let path = dir.path().join("resize.shm");
451
452 let mut region = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
453 assert_eq!(region.len(), 4096);
454
455 unsafe {
457 std::ptr::write(region.region().as_ptr(), 0xAB);
458 }
459
460 region.resize(8192).unwrap();
462 assert_eq!(region.len(), 8192);
463
464 unsafe {
466 assert_eq!(std::ptr::read(region.region().as_ptr()), 0xAB);
467 }
468
469 unsafe {
471 std::ptr::write(region.region().as_ptr().add(5000), 0xCD);
472 assert_eq!(std::ptr::read(region.region().as_ptr().add(5000)), 0xCD);
473 }
474 }
475
476 #[test]
477 fn test_resize_shrink_rejected() {
478 let dir = tempfile::tempdir().unwrap();
479 let path = dir.path().join("shrink.shm");
480
481 let mut region = MmapRegion::create(&path, 8192, FileCleanup::Manual).unwrap();
482 let result = region.resize(4096);
483 assert!(result.is_err());
484 }
485
486 #[test]
487 fn test_check_and_remap() {
488 let dir = tempfile::tempdir().unwrap();
489 let path = dir.path().join("remap.shm");
490
491 let mut owner = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
493
494 let mut guest = MmapRegion::attach(&path).unwrap();
496 assert_eq!(guest.len(), 4096);
497
498 owner.resize(8192).unwrap();
500
501 let remapped = guest.check_and_remap().unwrap();
503 assert!(remapped);
504 assert_eq!(guest.len(), 8192);
505
506 let remapped2 = guest.check_and_remap().unwrap();
508 assert!(!remapped2);
509 }
510
511 #[test]
512 fn test_resize_preserves_shared_data() {
513 let dir = tempfile::tempdir().unwrap();
514 let path = dir.path().join("shared_resize.shm");
515
516 let mut owner = MmapRegion::create(&path, 4096, FileCleanup::Manual).unwrap();
517 let mut guest = MmapRegion::attach(&path).unwrap();
518
519 unsafe {
521 std::ptr::write(owner.region().as_ptr().add(100), 0x42);
522 }
523
524 unsafe {
526 assert_eq!(std::ptr::read(guest.region().as_ptr().add(100)), 0x42);
527 }
528
529 owner.resize(8192).unwrap();
531
532 guest.check_and_remap().unwrap();
534
535 unsafe {
537 assert_eq!(std::ptr::read(guest.region().as_ptr().add(100)), 0x42);
538 }
539
540 unsafe {
542 std::ptr::write(owner.region().as_ptr().add(5000), 0x99);
543 }
544
545 unsafe {
547 assert_eq!(std::ptr::read(guest.region().as_ptr().add(5000)), 0x99);
548 }
549 }
550}