1#[cfg(unix)]
2use std::ffi::OsStr;
3use std::ffi::OsString;
4use std::io::{self, Write};
5use std::path::{Path, PathBuf};
6use std::sync::atomic::{AtomicBool, Ordering};
7
8#[derive(Clone, Debug, Eq, Hash, PartialEq)]
10pub enum DurableFileIdentity {
11 #[cfg(unix)]
12 Unix { device: u64, inode: u64 },
13 #[cfg(windows)]
14 Windows { volume_serial: u32, file_index: u64 },
15}
16
17pub struct DurableRoot {
35 canonical_path: PathBuf,
36 #[cfg(any(
37 windows,
38 all(
39 unix,
40 any(target_os = "linux", target_os = "android", target_vendor = "apple")
41 )
42 ))]
43 directory: std::fs::File,
44 sync_eagerly: AtomicBool,
51}
52
53impl std::fmt::Debug for DurableRoot {
54 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 formatter
56 .debug_struct("DurableRoot")
57 .field("canonical_path", &self.canonical_path)
58 .finish_non_exhaustive()
59 }
60}
61
62impl DurableRoot {
63 pub fn try_clone(&self) -> io::Result<Self> {
64 Ok(Self {
65 canonical_path: self.canonical_path.clone(),
66 #[cfg(any(
67 windows,
68 all(
69 unix,
70 any(target_os = "linux", target_os = "android", target_vendor = "apple")
71 )
72 ))]
73 directory: self.directory.try_clone()?,
74 sync_eagerly: AtomicBool::new(self.sync_eagerly.load(Ordering::Relaxed)),
75 })
76 }
77
78 pub fn open(root: impl AsRef<Path>) -> io::Result<Self> {
79 #[cfg(all(
80 unix,
81 any(target_os = "linux", target_os = "android", target_vendor = "apple")
82 ))]
83 {
84 #[cfg(any(target_os = "linux", target_os = "android"))]
85 if let Ok(relative) = root.as_ref().strip_prefix("/proc/self/fd") {
86 let mut components =
87 relative
88 .components()
89 .filter_map(|component| match component {
90 std::path::Component::Normal(value) => Some(value),
91 _ => None,
92 });
93 if let Some(fd) = components.next().filter(|value| {
94 value
95 .to_str()
96 .is_some_and(|value| value.parse::<i32>().is_ok())
97 }) {
98 use rustix::fs::{openat, Mode, OFlags};
99 let mut directory = std::fs::File::open(Path::new("/proc/self/fd").join(fd))?;
100 for component in components {
101 let opened = openat(
102 &directory,
103 Path::new(component),
104 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
105 Mode::empty(),
106 )
107 .map_err(io::Error::from)?;
108 directory = std::fs::File::from(opened);
109 }
110 use std::os::fd::AsRawFd;
111 let canonical_path = std::fs::read_link(
112 Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string()),
113 )?;
114 return Ok(Self {
115 canonical_path,
116 directory,
117 sync_eagerly: AtomicBool::new(true),
118 });
119 }
120 }
121 let directory = open_unix_directory_path(root.as_ref())?;
122 let directory = std::fs::File::from(directory);
123 #[cfg(any(target_os = "linux", target_os = "android"))]
124 let canonical_path = {
125 use std::os::fd::AsRawFd;
126 std::fs::read_link(
127 Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string()),
128 )?
129 };
130 #[cfg(target_vendor = "apple")]
131 let canonical_path = {
132 use std::os::unix::ffi::OsStrExt;
133 let path = rustix::fs::getpath(&directory).map_err(io::Error::from)?;
134 PathBuf::from(OsStr::from_bytes(path.to_bytes()))
135 };
136 Ok(Self {
137 canonical_path,
138 directory,
139 sync_eagerly: AtomicBool::new(true),
140 })
141 }
142
143 #[cfg(windows)]
144 {
145 let directory = open_windows_nofollow(root.as_ref())?;
146 ensure_windows_directory(&directory, root.as_ref()).map_err(mongrel_error_to_io)?;
147 let canonical_path = root.as_ref().canonicalize()?;
148 return Ok(Self {
149 canonical_path,
150 directory,
151 sync_eagerly: AtomicBool::new(true),
152 });
153 }
154
155 #[cfg(not(any(
156 windows,
157 all(
158 unix,
159 any(target_os = "linux", target_os = "android", target_vendor = "apple")
160 )
161 )))]
162 Err(io::Error::new(
163 io::ErrorKind::Unsupported,
164 "descriptor-relative durable files are unsupported on this platform",
165 ))
166 }
167
168 pub fn canonical_path(&self) -> &Path {
169 &self.canonical_path
170 }
171
172 pub fn file_identity(&self) -> io::Result<DurableFileIdentity> {
174 #[cfg(all(
175 unix,
176 any(target_os = "linux", target_os = "android", target_vendor = "apple")
177 ))]
178 {
179 use std::os::unix::fs::MetadataExt;
180 let metadata = self.directory.metadata()?;
181 return Ok(DurableFileIdentity::Unix {
182 device: metadata.dev(),
183 inode: metadata.ino(),
184 });
185 }
186
187 #[cfg(windows)]
188 {
189 use std::os::windows::io::AsRawHandle;
190 use windows_sys::Win32::Storage::FileSystem::{
191 GetFileInformationByHandle, BY_HANDLE_FILE_INFORMATION,
192 };
193
194 let mut metadata = BY_HANDLE_FILE_INFORMATION::default();
195 if unsafe { GetFileInformationByHandle(self.directory.as_raw_handle(), &mut metadata) }
196 == 0
197 {
198 return Err(io::Error::last_os_error());
199 }
200 return Ok(DurableFileIdentity::Windows {
201 volume_serial: metadata.dwVolumeSerialNumber,
202 file_index: (u64::from(metadata.nFileIndexHigh) << 32)
203 | u64::from(metadata.nFileIndexLow),
204 });
205 }
206
207 #[allow(unreachable_code)]
208 Err(io::Error::new(
209 io::ErrorKind::Unsupported,
210 "descriptor-relative durable files are unsupported on this platform",
211 ))
212 }
213
214 pub fn open_deferred(root: impl AsRef<Path>) -> io::Result<Self> {
230 let this = Self::open(root)?;
231 this.sync_eagerly.store(false, Ordering::Relaxed);
232 Ok(this)
233 }
234
235 pub fn finalize_deferred_sync(&self) -> io::Result<()> {
241 mongreldb_types::durability::sync_tree_recursive(&self.canonical_path)?;
242 self.sync_eagerly.store(true, Ordering::Relaxed);
243 Ok(())
244 }
245
246 pub fn finalize_deferred_sync_shallow(&self) -> io::Result<()> {
262 if !self.should_sync_eagerly() {
263 for entry in (std::fs::read_dir(&self.canonical_path)?).flatten() {
264 let path = entry.path();
265 let meta = match entry.metadata() {
266 Ok(m) => m,
267 Err(e) if e.kind() == io::ErrorKind::NotFound => continue,
268 Err(e) => return Err(e),
269 };
270 if meta.is_file() {
271 std::fs::File::open(&path)?.sync_all()?;
272 } else if meta.is_dir() {
273 let _ = sync_directory(&path);
276 }
277 }
278 sync_directory(&self.canonical_path)?;
279 self.sync_eagerly.store(true, Ordering::Relaxed);
280 }
281 Ok(())
282 }
283
284 #[inline]
285 fn should_sync_eagerly(&self) -> bool {
286 self.sync_eagerly.load(Ordering::Relaxed)
287 }
288
289 pub fn io_path(&self) -> io::Result<PathBuf> {
292 #[cfg(any(target_os = "linux", target_os = "android"))]
293 {
294 use std::os::fd::AsRawFd;
295 let prefix = "/proc/self/fd";
296 return Ok(Path::new(prefix)
297 .join(self.directory.as_raw_fd().to_string())
298 .join("."));
299 }
300
301 #[cfg(target_vendor = "apple")]
302 {
303 use std::os::unix::ffi::OsStrExt;
304 let path = rustix::fs::getpath(&self.directory).map_err(io::Error::from)?;
305 return Ok(PathBuf::from(OsStr::from_bytes(path.to_bytes())));
306 }
307
308 #[cfg(windows)]
309 return Ok(self.canonical_path.clone());
310
311 #[allow(unreachable_code)]
312 Err(io::Error::new(
313 io::ErrorKind::Unsupported,
314 "durable root has no supported operational path",
315 ))
316 }
317
318 pub fn open_directory(&self, relative: impl AsRef<Path>) -> io::Result<DurableRoot> {
319 let relative = relative.as_ref();
320 let components = checked_components(relative)?;
321 let canonical_path = components
322 .iter()
323 .fold(self.canonical_path.clone(), |path, component| {
324 path.join(component)
325 });
326
327 #[cfg(all(
328 unix,
329 any(target_os = "linux", target_os = "android", target_vendor = "apple")
330 ))]
331 {
332 let directory = self.unix_directory(relative)?;
333 return Ok(DurableRoot {
334 canonical_path,
335 directory: std::fs::File::from(directory),
336 sync_eagerly: AtomicBool::new(true),
337 });
338 }
339
340 #[cfg(windows)]
341 {
342 let (path, _ancestors) = self.windows_path(relative)?;
343 let directory = open_windows_nofollow(&path)?;
344 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
345 return Ok(DurableRoot {
346 canonical_path,
347 directory,
348 sync_eagerly: AtomicBool::new(true),
349 });
350 }
351
352 #[allow(unreachable_code)]
353 Err(io::Error::new(
354 io::ErrorKind::Unsupported,
355 "descriptor-relative durable files are unsupported on this platform",
356 ))
357 }
358
359 pub fn create_directory_all(&self, relative: impl AsRef<Path>) -> io::Result<()> {
360 self.create_directory_all_pinned(relative).map(|_| ())
361 }
362
363 pub fn create_directory_all_pinned(
364 &self,
365 relative: impl AsRef<Path>,
366 ) -> io::Result<DurableRoot> {
367 let components = checked_components(relative.as_ref())?;
368 let canonical_path = components
369 .iter()
370 .fold(self.canonical_path.clone(), |path, component| {
371 path.join(component)
372 });
373
374 #[cfg(all(
375 unix,
376 any(target_os = "linux", target_os = "android", target_vendor = "apple")
377 ))]
378 {
379 use rustix::fs::{fsync, mkdirat, openat, Mode, OFlags};
380 let mut directory = self.duplicate_unix_root()?;
381 for component in components {
382 let opened = openat(
383 &directory,
384 Path::new(&component),
385 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
386 Mode::empty(),
387 );
388 directory = match opened {
389 Ok(opened) => opened,
390 Err(error) if error == rustix::io::Errno::NOENT => {
391 mkdirat(
392 &directory,
393 Path::new(&component),
394 Mode::from_raw_mode(0o700),
395 )
396 .map_err(io::Error::from)?;
397 if self.should_sync_eagerly() {
398 fsync(&directory).map_err(io::Error::from)?;
399 }
400 openat(
401 &directory,
402 Path::new(&component),
403 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
404 Mode::empty(),
405 )
406 .map_err(io::Error::from)?
407 }
408 Err(error) => return Err(io::Error::from(error)),
409 };
410 }
411 return Ok(DurableRoot {
412 canonical_path,
413 directory: std::fs::File::from(directory),
414 sync_eagerly: AtomicBool::new(true),
415 });
416 }
417
418 #[cfg(windows)]
419 {
420 let mut path = self.canonical_path.clone();
421 let mut ancestors = Vec::new();
422 for component in components {
423 path.push(component);
424 match open_windows_nofollow(&path) {
425 Ok(directory) => {
426 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
427 ancestors.push(directory);
428 }
429 Err(error) if error.kind() == io::ErrorKind::NotFound => {
430 std::fs::create_dir(&path)?;
431 sync_directory(path.parent().unwrap_or(&self.canonical_path))?;
432 let directory = open_windows_nofollow(&path)?;
433 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
434 ancestors.push(directory);
435 }
436 Err(error) => return Err(error),
437 }
438 }
439 let directory = ancestors.pop().ok_or_else(|| {
440 io::Error::new(
441 io::ErrorKind::InvalidInput,
442 "durable relative path is empty",
443 )
444 })?;
445 return Ok(DurableRoot {
446 canonical_path,
447 directory,
448 sync_eagerly: AtomicBool::new(true),
449 });
450 }
451
452 #[allow(unreachable_code)]
453 Err(io::Error::new(
454 io::ErrorKind::Unsupported,
455 "descriptor-relative durable files are unsupported on this platform",
456 ))
457 }
458
459 pub fn create_directory_new(&self, relative: impl AsRef<Path>) -> io::Result<()> {
460 #[cfg(all(
461 unix,
462 any(target_os = "linux", target_os = "android", target_vendor = "apple")
463 ))]
464 {
465 use rustix::fs::{fsync, mkdirat, Mode};
466 let (directory, name) = self.unix_parent(relative.as_ref())?;
467 mkdirat(&directory, Path::new(&name), Mode::from_raw_mode(0o700))
468 .map_err(io::Error::from)?;
469 if self.should_sync_eagerly() {
470 return fsync(directory).map_err(io::Error::from);
471 }
472 return Ok(());
473 }
474
475 #[cfg(windows)]
476 {
477 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
478 std::fs::create_dir(&path)?;
479 return sync_directory(path.parent().unwrap_or(&self.canonical_path));
480 }
481
482 #[allow(unreachable_code)]
483 Err(io::Error::new(
484 io::ErrorKind::Unsupported,
485 "descriptor-relative durable files are unsupported on this platform",
486 ))
487 }
488
489 pub fn open_regular(&self, relative: impl AsRef<Path>) -> io::Result<std::fs::File> {
490 #[cfg(all(
491 unix,
492 any(target_os = "linux", target_os = "android", target_vendor = "apple")
493 ))]
494 {
495 use rustix::fs::{fstat, openat, FileType, Mode, OFlags};
496 let (directory, name) = self.unix_parent(relative.as_ref())?;
497 let file = openat(
498 &directory,
499 Path::new(&name),
500 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
501 Mode::empty(),
502 )
503 .map_err(io::Error::from)?;
504 if FileType::from_raw_mode(fstat(&file).map_err(io::Error::from)?.st_mode)
505 != FileType::RegularFile
506 {
507 return Err(io::Error::new(
508 io::ErrorKind::InvalidInput,
509 "durable entry is not a regular file",
510 ));
511 }
512 return Ok(std::fs::File::from(file));
513 }
514
515 #[cfg(windows)]
516 {
517 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
518 let file = open_windows_nofollow(&path)?;
519 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
520 return Ok(file);
521 }
522
523 #[allow(unreachable_code)]
524 Err(io::Error::new(
525 io::ErrorKind::Unsupported,
526 "descriptor-relative durable files are unsupported on this platform",
527 ))
528 }
529
530 pub(crate) fn open_regular_read_write(
531 &self,
532 relative: impl AsRef<Path>,
533 ) -> io::Result<std::fs::File> {
534 #[cfg(all(
535 unix,
536 any(target_os = "linux", target_os = "android", target_vendor = "apple")
537 ))]
538 {
539 use rustix::fs::{fstat, openat, FileType, Mode, OFlags};
540 let (directory, name) = self.unix_parent(relative.as_ref())?;
541 let file = openat(
542 &directory,
543 Path::new(&name),
544 OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
545 Mode::empty(),
546 )
547 .map_err(io::Error::from)?;
548 if FileType::from_raw_mode(fstat(&file).map_err(io::Error::from)?.st_mode)
549 != FileType::RegularFile
550 {
551 return Err(io::Error::new(
552 io::ErrorKind::InvalidInput,
553 "durable entry is not a regular file",
554 ));
555 }
556 return Ok(std::fs::File::from(file));
557 }
558
559 #[cfg(windows)]
560 {
561 use std::os::windows::fs::OpenOptionsExt;
562 use windows_sys::Win32::Storage::FileSystem::{
563 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
564 };
565 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
566 let file = std::fs::OpenOptions::new()
567 .read(true)
568 .write(true)
569 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
570 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
571 .open(&path)?;
572 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
573 return Ok(file);
574 }
575
576 #[allow(unreachable_code)]
577 Err(io::Error::new(
578 io::ErrorKind::Unsupported,
579 "descriptor-relative durable files are unsupported on this platform",
580 ))
581 }
582
583 pub fn entry_exists(&self, relative: impl AsRef<Path>) -> io::Result<bool> {
584 #[cfg(all(
585 unix,
586 any(target_os = "linux", target_os = "android", target_vendor = "apple")
587 ))]
588 {
589 use rustix::fs::{openat, Mode, OFlags};
590 let (directory, name) = self.unix_parent(relative.as_ref())?;
591 return match openat(
592 &directory,
593 Path::new(&name),
594 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
595 Mode::empty(),
596 ) {
597 Ok(_) => Ok(true),
598 Err(error) if error == rustix::io::Errno::NOENT => Ok(false),
599 Err(error) => Err(io::Error::from(error)),
600 };
601 }
602
603 #[cfg(windows)]
604 {
605 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
606 return match open_windows_nofollow(&path) {
607 Ok(file) => {
608 ensure_windows_not_reparse(&file, &path).map_err(mongrel_error_to_io)?;
609 Ok(true)
610 }
611 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
612 Err(error) => Err(error),
613 };
614 }
615
616 #[allow(unreachable_code)]
617 Err(io::Error::new(
618 io::ErrorKind::Unsupported,
619 "descriptor-relative durable files are unsupported on this platform",
620 ))
621 }
622
623 pub fn write_new(&self, relative: impl AsRef<Path>, bytes: &[u8]) -> io::Result<()> {
624 let relative = relative.as_ref();
625 let mut file = self.open_create_new(relative)?;
626 let result = (|| {
627 file.write_all(bytes)?;
628 if self.should_sync_eagerly() {
629 file.sync_all()?;
630 }
631 Ok::<(), io::Error>(())
632 })();
633 if result.is_err() {
634 let _ = self.remove_file(relative);
635 return result;
636 }
637 self.sync_relative_parent(relative)
638 }
639
640 pub fn write_new_atomic(&self, relative: impl AsRef<Path>, bytes: &[u8]) -> io::Result<()> {
644 let relative = relative.as_ref();
645 let (_, name) = checked_parent(relative)?;
646 for _ in 0..128 {
647 let mut nonce = [0_u8; 8];
648 getrandom::getrandom(&mut nonce)
649 .map_err(|error| io::Error::other(error.to_string()))?;
650 let suffix = nonce
651 .iter()
652 .map(|byte| format!("{byte:02x}"))
653 .collect::<String>();
654 let temporary = relative.with_file_name(format!(
655 ".{}.tmp-{}-{suffix}",
656 name.to_string_lossy(),
657 std::process::id()
658 ));
659 match self.write_new(&temporary, bytes) {
660 Ok(()) => {
661 let result = self.rename_file_new(&temporary, relative);
662 if result.is_err() {
663 let _ = self.remove_file(&temporary);
664 }
665 return result;
666 }
667 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
668 Err(error) => return Err(error),
669 }
670 }
671 Err(io::Error::new(
672 io::ErrorKind::AlreadyExists,
673 "could not allocate durable temporary file",
674 ))
675 }
676
677 pub(crate) fn create_regular_new(
678 &self,
679 relative: impl AsRef<Path>,
680 ) -> io::Result<std::fs::File> {
681 self.open_create_new(relative.as_ref())
682 }
683
684 pub(crate) fn sync_entry_parent(&self, relative: impl AsRef<Path>) -> io::Result<()> {
685 self.sync_relative_parent(relative.as_ref())
686 }
687
688 pub fn copy_new_from(
689 &self,
690 relative: impl AsRef<Path>,
691 source: &mut std::fs::File,
692 ) -> io::Result<u64> {
693 let relative = relative.as_ref();
694 let mut destination = self.open_create_new(relative)?;
695 let result = (|| {
696 let bytes = io::copy(source, &mut destination)?;
697 if self.should_sync_eagerly() {
698 destination.sync_all()?;
699 }
700 Ok(bytes)
701 })();
702 if result.is_err() {
703 let _ = self.remove_file(relative);
704 return result;
705 }
706 self.sync_relative_parent(relative)?;
707 result
708 }
709
710 pub fn write_atomic(&self, relative: impl AsRef<Path>, bytes: &[u8]) -> io::Result<()> {
711 self.write_atomic_with_after(relative, bytes, || {})
712 }
713
714 pub(crate) fn write_atomic_with_after<F>(
717 &self,
718 relative: impl AsRef<Path>,
719 bytes: &[u8],
720 after_publish: F,
721 ) -> io::Result<()>
722 where
723 F: FnOnce(),
724 {
725 self.write_atomic_controlled_with_after(
726 relative,
727 bytes,
728 || Ok::<(), io::Error>(()),
729 after_publish,
730 )
731 }
732
733 pub(crate) fn write_atomic_controlled_with_after<B, A, E>(
737 &self,
738 relative: impl AsRef<Path>,
739 bytes: &[u8],
740 before_publish: B,
741 after_publish: A,
742 ) -> std::result::Result<(), E>
743 where
744 B: FnOnce() -> std::result::Result<(), E>,
745 A: FnOnce(),
746 E: From<io::Error>,
747 {
748 let relative = relative.as_ref();
749 let (_, name) = checked_parent(relative).map_err(E::from)?;
750 let mut before_publish = Some(before_publish);
751 let mut after_publish = Some(after_publish);
752 for _ in 0..128 {
753 let mut nonce = [0_u8; 8];
754 getrandom::getrandom(&mut nonce)
755 .map_err(|error| E::from(io::Error::other(error.to_string())))?;
756 let suffix = nonce
757 .iter()
758 .map(|byte| format!("{byte:02x}"))
759 .collect::<String>();
760 let temporary = relative.with_file_name(format!(
761 ".{}.tmp-{}-{suffix}",
762 name.to_string_lossy(),
763 std::process::id()
764 ));
765 match self.write_new(&temporary, bytes) {
766 Ok(()) => {
767 let prepare = before_publish.take().ok_or_else(|| {
768 E::from(io::Error::other(
769 "durable atomic prepare callback already consumed",
770 ))
771 })?;
772 if let Err(error) = prepare() {
773 let _ = self.remove_file(&temporary);
774 return Err(error);
775 }
776 let publish = after_publish.take().ok_or_else(|| {
777 E::from(io::Error::other(
778 "durable atomic publish callback already consumed",
779 ))
780 })?;
781 let result = self.replace_file_with_after(&temporary, relative, publish);
782 if result.is_err() {
783 let _ = self.remove_file(&temporary);
784 }
785 return result.map_err(E::from);
786 }
787 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
788 Err(error) => return Err(E::from(error)),
789 }
790 }
791 Err(E::from(io::Error::new(
792 io::ErrorKind::AlreadyExists,
793 "could not allocate durable temporary file",
794 )))
795 }
796
797 pub fn open_lock_file(&self, relative: impl AsRef<Path>) -> io::Result<std::fs::File> {
798 #[cfg(all(
799 unix,
800 any(target_os = "linux", target_os = "android", target_vendor = "apple")
801 ))]
802 {
803 use rustix::fs::{fstat, openat, FileType, Mode, OFlags};
804 let (directory, name) = self.unix_parent(relative.as_ref())?;
805 let file = openat(
806 &directory,
807 Path::new(&name),
808 OFlags::RDWR | OFlags::CREATE | OFlags::CLOEXEC | OFlags::NOFOLLOW,
809 Mode::from_raw_mode(0o600),
810 )
811 .map_err(io::Error::from)?;
812 if FileType::from_raw_mode(fstat(&file).map_err(io::Error::from)?.st_mode)
813 != FileType::RegularFile
814 {
815 return Err(io::Error::new(
816 io::ErrorKind::InvalidInput,
817 "durable lock is not a regular file",
818 ));
819 }
820 return Ok(std::fs::File::from(file));
821 }
822
823 #[cfg(windows)]
824 {
825 use std::os::windows::fs::OpenOptionsExt;
826 use windows_sys::Win32::Storage::FileSystem::{
827 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
828 };
829 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
830 let file = std::fs::OpenOptions::new()
831 .read(true)
832 .write(true)
833 .create(true)
834 .truncate(false)
835 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
836 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
837 .open(&path)?;
838 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
839 return Ok(file);
840 }
841
842 #[allow(unreachable_code)]
843 Err(io::Error::new(
844 io::ErrorKind::Unsupported,
845 "descriptor-relative durable files are unsupported on this platform",
846 ))
847 }
848
849 pub fn list_regular_files(&self, relative: impl AsRef<Path>) -> io::Result<Vec<OsString>> {
850 #[cfg(all(
851 unix,
852 any(target_os = "linux", target_os = "android", target_vendor = "apple")
853 ))]
854 {
855 use rustix::fs::{fstat, openat, Dir, FileType, Mode, OFlags};
856 let relative = relative.as_ref();
857 let directory = if relative.as_os_str().is_empty() || relative == Path::new(".") {
858 self.duplicate_unix_root()?
859 } else {
860 self.unix_directory(relative)?
861 };
862 let mut entries = Dir::read_from(&directory).map_err(io::Error::from)?;
863 let mut names = Vec::new();
864 for entry in &mut entries {
865 let entry = entry.map_err(io::Error::from)?;
866 use std::os::unix::ffi::OsStrExt;
867 let bytes = entry.file_name().to_bytes();
868 if bytes == b"." || bytes == b".." {
869 continue;
870 }
871 let name = OsStr::from_bytes(bytes).to_os_string();
872 let child = openat(
873 &directory,
874 Path::new(&name),
875 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
876 Mode::empty(),
877 )
878 .map_err(io::Error::from)?;
879 if FileType::from_raw_mode(fstat(&child).map_err(io::Error::from)?.st_mode)
880 != FileType::RegularFile
881 {
882 return Err(io::Error::new(
883 io::ErrorKind::InvalidInput,
884 "durable directory contains a non-regular entry",
885 ));
886 }
887 names.push(name);
888 }
889 names.sort();
890 return Ok(names);
891 }
892
893 #[cfg(windows)]
894 {
895 let relative = relative.as_ref();
896 let (path, mut ancestors) =
897 if relative.as_os_str().is_empty() || relative == Path::new(".") {
898 (self.canonical_path.clone(), Vec::new())
899 } else {
900 self.windows_path(relative)?
901 };
902 let directory = open_windows_nofollow(&path)?;
903 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
904 ancestors.push(directory);
905 let mut names = Vec::new();
906 for entry in std::fs::read_dir(&path)? {
907 let entry = entry?;
908 let child = open_windows_nofollow(&entry.path())?;
909 ensure_windows_regular(&child, &entry.path()).map_err(mongrel_error_to_io)?;
910 names.push(entry.file_name());
911 }
912 names.sort();
913 return Ok(names);
914 }
915
916 #[allow(unreachable_code)]
917 Err(io::Error::new(
918 io::ErrorKind::Unsupported,
919 "descriptor-relative durable files are unsupported on this platform",
920 ))
921 }
922
923 pub(crate) fn walk_regular_files<P, D, F>(
926 &self,
927 mut include: P,
928 mut on_directory: D,
929 mut on_file: F,
930 ) -> crate::Result<()>
931 where
932 P: FnMut(&Path, bool) -> crate::Result<bool>,
933 D: FnMut(&Path) -> crate::Result<()>,
934 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
935 {
936 #[cfg(all(
937 unix,
938 any(target_os = "linux", target_os = "android", target_vendor = "apple")
939 ))]
940 {
941 return walk_unix_directory(
942 self.duplicate_unix_root()?,
943 Path::new(""),
944 &mut include,
945 &mut on_directory,
946 &mut on_file,
947 );
948 }
949
950 #[cfg(windows)]
951 {
952 return walk_windows_directory(
953 &self.canonical_path,
954 Path::new(""),
955 self.directory.try_clone()?,
956 &mut include,
957 &mut on_directory,
958 &mut on_file,
959 );
960 }
961
962 #[allow(unreachable_code)]
963 {
964 let _ = (&mut include, &mut on_directory, &mut on_file);
965 Err(crate::MongrelError::Other(
966 "no-follow directory traversal is unsupported on this platform".into(),
967 ))
968 }
969 }
970
971 pub fn remove_file(&self, relative: impl AsRef<Path>) -> io::Result<()> {
972 #[cfg(all(
973 unix,
974 any(target_os = "linux", target_os = "android", target_vendor = "apple")
975 ))]
976 {
977 use rustix::fs::{fsync, unlinkat, AtFlags};
978 let (directory, name) = self.unix_parent(relative.as_ref())?;
979 match unlinkat(&directory, Path::new(&name), AtFlags::empty()) {
980 Ok(()) => fsync(&directory).map_err(io::Error::from),
981 Err(error) if error == rustix::io::Errno::NOENT => Ok(()),
982 Err(error) => Err(io::Error::from(error)),
983 }
984 }
985
986 #[cfg(windows)]
987 {
988 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
989 let file = match open_windows_for_delete(&path) {
990 Ok(file) => file,
991 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
992 Err(error) => return Err(error),
993 };
994 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
995 delete_windows_handle(file)?;
996 sync_directory(path.parent().unwrap_or(&self.canonical_path))
997 }
998
999 #[cfg(not(any(
1000 windows,
1001 all(
1002 unix,
1003 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1004 )
1005 )))]
1006 Err(io::Error::new(
1007 io::ErrorKind::Unsupported,
1008 "descriptor-relative durable files are unsupported on this platform",
1009 ))
1010 }
1011
1012 pub(crate) fn rename_file_new(
1013 &self,
1014 source: impl AsRef<Path>,
1015 destination: impl AsRef<Path>,
1016 ) -> io::Result<()> {
1017 #[cfg(all(
1018 unix,
1019 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1020 ))]
1021 {
1022 use rustix::fs::{fsync, renameat_with, RenameFlags};
1023 use std::os::fd::AsRawFd;
1024 let (source_parent, source_name) = self.unix_parent(source.as_ref())?;
1025 let (destination_parent, destination_name) = self.unix_parent(destination.as_ref())?;
1026 renameat_with(
1027 &source_parent,
1028 Path::new(&source_name),
1029 &destination_parent,
1030 Path::new(&destination_name),
1031 RenameFlags::NOREPLACE,
1032 )
1033 .map_err(io::Error::from)?;
1034 fsync(&destination_parent).map_err(io::Error::from)?;
1035 if source_parent.as_raw_fd() != destination_parent.as_raw_fd() {
1036 fsync(&source_parent).map_err(io::Error::from)?;
1037 }
1038 return Ok(());
1039 }
1040
1041 #[cfg(windows)]
1042 {
1043 let (source, _source_ancestors) = self.windows_path(source.as_ref())?;
1044 let (destination, _destination_ancestors) = self.windows_path(destination.as_ref())?;
1045 return rename(&source, &destination);
1046 }
1047
1048 #[allow(unreachable_code)]
1049 Err(io::Error::new(
1050 io::ErrorKind::Unsupported,
1051 "descriptor-relative file rename is unsupported on this platform",
1052 ))
1053 }
1054
1055 pub fn rename_directory_new(
1056 &self,
1057 source: impl AsRef<Path>,
1058 destination_root: &DurableRoot,
1059 destination: impl AsRef<Path>,
1060 ) -> io::Result<()> {
1061 self.rename_directory_new_with_after(source, destination_root, destination, || {})
1062 }
1063
1064 pub fn rename_directory_new_with_after<F>(
1065 &self,
1066 source: impl AsRef<Path>,
1067 destination_root: &DurableRoot,
1068 destination: impl AsRef<Path>,
1069 after_publish: F,
1070 ) -> io::Result<()>
1071 where
1072 F: FnOnce(),
1073 {
1074 #[cfg(all(
1075 unix,
1076 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1077 ))]
1078 {
1079 use rustix::fs::{fsync, renameat_with, RenameFlags};
1080 let (source_parent, source_name) = self.unix_parent(source.as_ref())?;
1081 let (destination_parent, destination_name) =
1082 destination_root.unix_parent(destination.as_ref())?;
1083 renameat_with(
1084 &source_parent,
1085 Path::new(&source_name),
1086 &destination_parent,
1087 Path::new(&destination_name),
1088 RenameFlags::NOREPLACE,
1089 )
1090 .map_err(io::Error::from)?;
1091 after_publish();
1092 fsync(&destination_parent).map_err(io::Error::from)?;
1093 return fsync(&source_parent).map_err(io::Error::from);
1094 }
1095
1096 #[cfg(windows)]
1097 {
1098 let (source, mut source_ancestors) = self.windows_path(source.as_ref())?;
1099 let (destination, destination_ancestors) =
1100 destination_root.windows_path(destination.as_ref())?;
1101 source_ancestors.extend(destination_ancestors);
1102 let result = rename(&source, &destination);
1103 if result.is_ok() {
1104 after_publish();
1105 }
1106 return result;
1107 }
1108
1109 #[allow(unreachable_code)]
1110 Err(io::Error::new(
1111 io::ErrorKind::Unsupported,
1112 "descriptor-relative durable files are unsupported on this platform",
1113 ))
1114 }
1115
1116 pub fn remove_directory_all(&self, relative: impl AsRef<Path>) -> io::Result<()> {
1117 #[cfg(all(
1118 unix,
1119 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1120 ))]
1121 {
1122 use rustix::fs::{fsync, unlinkat, AtFlags};
1123 let (parent, name) = self.unix_parent(relative.as_ref())?;
1124 let directory = match self.unix_directory(relative.as_ref()) {
1125 Ok(directory) => directory,
1126 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
1127 Err(error) => return Err(error),
1128 };
1129 remove_unix_directory_contents(&directory)?;
1130 unlinkat(&parent, Path::new(&name), AtFlags::REMOVEDIR).map_err(io::Error::from)?;
1131 return fsync(parent).map_err(io::Error::from);
1132 }
1133
1134 #[cfg(windows)]
1135 {
1136 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
1137 let directory = match open_windows_for_delete(&path) {
1138 Ok(directory) => directory,
1139 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
1140 Err(error) => return Err(error),
1141 };
1142 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
1143 remove_windows_directory_contents(&path, &directory)?;
1144 delete_windows_handle(directory)?;
1145 return sync_directory(path.parent().unwrap_or(&self.canonical_path));
1146 }
1147
1148 #[allow(unreachable_code)]
1149 Err(io::Error::new(
1150 io::ErrorKind::Unsupported,
1151 "descriptor-relative durable files are unsupported on this platform",
1152 ))
1153 }
1154
1155 #[cfg(all(
1156 unix,
1157 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1158 ))]
1159 fn duplicate_unix_root(&self) -> io::Result<rustix::fd::OwnedFd> {
1160 use rustix::fs::{openat, Mode, OFlags};
1161 openat(
1162 &self.directory,
1163 Path::new("."),
1164 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
1165 Mode::empty(),
1166 )
1167 .map_err(io::Error::from)
1168 }
1169
1170 #[cfg(all(
1171 unix,
1172 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1173 ))]
1174 fn unix_directory(&self, relative: &Path) -> io::Result<rustix::fd::OwnedFd> {
1175 use rustix::fs::{openat, Mode, OFlags};
1176 let mut directory = self.duplicate_unix_root()?;
1177 for component in checked_components_allow_empty(relative)? {
1178 directory = openat(
1179 &directory,
1180 Path::new(&component),
1181 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
1182 Mode::empty(),
1183 )
1184 .map_err(io::Error::from)?;
1185 }
1186 Ok(directory)
1187 }
1188
1189 #[cfg(all(
1190 unix,
1191 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1192 ))]
1193 fn unix_parent(&self, relative: &Path) -> io::Result<(rustix::fd::OwnedFd, OsString)> {
1194 let (parent, name) = checked_parent(relative)?;
1195 Ok((self.unix_directory(&parent)?, name))
1196 }
1197
1198 #[cfg(windows)]
1199 fn windows_path(&self, relative: &Path) -> io::Result<(PathBuf, Vec<std::fs::File>)> {
1200 let components = checked_components(relative)?;
1201 let mut path = self.canonical_path.clone();
1202 let mut ancestors = Vec::new();
1203 for component in components.iter().take(components.len().saturating_sub(1)) {
1204 path.push(component);
1205 let directory = open_windows_nofollow(&path)?;
1206 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
1207 ancestors.push(directory);
1208 }
1209 if let Some(name) = components.last() {
1210 path.push(name);
1211 }
1212 Ok((path, ancestors))
1213 }
1214
1215 fn open_create_new(&self, relative: &Path) -> io::Result<std::fs::File> {
1216 #[cfg(all(
1217 unix,
1218 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1219 ))]
1220 {
1221 use rustix::fs::{openat, Mode, OFlags};
1222 let (directory, name) = self.unix_parent(relative)?;
1223 return openat(
1224 &directory,
1225 Path::new(&name),
1226 OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
1227 Mode::from_raw_mode(0o600),
1228 )
1229 .map(std::fs::File::from)
1230 .map_err(io::Error::from);
1231 }
1232
1233 #[cfg(windows)]
1234 {
1235 use std::os::windows::fs::OpenOptionsExt;
1236 use windows_sys::Win32::Storage::FileSystem::{
1237 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
1238 };
1239 let (path, _ancestors) = self.windows_path(relative)?;
1240 return std::fs::OpenOptions::new()
1241 .write(true)
1242 .create_new(true)
1243 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1244 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
1245 .open(path);
1246 }
1247
1248 #[allow(unreachable_code)]
1249 Err(io::Error::new(
1250 io::ErrorKind::Unsupported,
1251 "descriptor-relative durable files are unsupported on this platform",
1252 ))
1253 }
1254
1255 fn replace_file_with_after<F>(
1256 &self,
1257 source: &Path,
1258 destination: &Path,
1259 after_publish: F,
1260 ) -> io::Result<()>
1261 where
1262 F: FnOnce(),
1263 {
1264 #[cfg(all(
1265 unix,
1266 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1267 ))]
1268 {
1269 use rustix::fs::{fsync, renameat};
1270 let (source_parent, source_name) = self.unix_parent(source)?;
1271 let (destination_parent, destination_name) = self.unix_parent(destination)?;
1272 renameat(
1273 &source_parent,
1274 Path::new(&source_name),
1275 &destination_parent,
1276 Path::new(&destination_name),
1277 )
1278 .map_err(io::Error::from)?;
1279 after_publish();
1280 if self.should_sync_eagerly() {
1281 fsync(&destination_parent).map_err(io::Error::from)?;
1282 if source.parent() != destination.parent() {
1283 fsync(&source_parent).map_err(io::Error::from)?;
1284 }
1285 }
1286 return Ok(());
1287 }
1288
1289 #[cfg(windows)]
1290 {
1291 let (source, mut source_ancestors) = self.windows_path(source)?;
1292 let (destination, destination_ancestors) = self.windows_path(destination)?;
1293 source_ancestors.extend(destination_ancestors);
1294 return replace_with_after(&source, &destination, after_publish);
1295 }
1296
1297 #[allow(unreachable_code)]
1298 Err(io::Error::new(
1299 io::ErrorKind::Unsupported,
1300 "descriptor-relative durable files are unsupported on this platform",
1301 ))
1302 }
1303
1304 fn sync_relative_parent(&self, relative: &Path) -> io::Result<()> {
1305 if !self.should_sync_eagerly() {
1306 return Ok(());
1307 }
1308 #[cfg(all(
1309 unix,
1310 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1311 ))]
1312 {
1313 use rustix::fs::fsync;
1314 let (directory, _) = self.unix_parent(relative)?;
1315 return fsync(directory).map_err(io::Error::from);
1316 }
1317
1318 #[cfg(windows)]
1319 {
1320 let (path, _ancestors) = self.windows_path(relative)?;
1321 return sync_directory(path.parent().unwrap_or(&self.canonical_path));
1322 }
1323
1324 #[allow(unreachable_code)]
1325 Err(io::Error::new(
1326 io::ErrorKind::Unsupported,
1327 "descriptor-relative durable files are unsupported on this platform",
1328 ))
1329 }
1330}
1331
1332#[cfg(all(
1333 unix,
1334 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1335))]
1336fn open_unix_directory_path(path: &Path) -> io::Result<std::os::fd::OwnedFd> {
1337 use rustix::fs::{openat, Mode, OFlags, CWD};
1338 use std::path::Component;
1339
1340 let flags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::DIRECTORY;
1341 for component in path.components() {
1342 match component {
1343 Component::RootDir | Component::CurDir | Component::Normal(_) => {}
1344 Component::ParentDir | Component::Prefix(_) => {
1345 return Err(io::Error::new(
1346 io::ErrorKind::InvalidInput,
1347 "durable root path contains an unsafe component",
1348 ))
1349 }
1350 }
1351 }
1352 openat(CWD, path, flags, Mode::empty()).map_err(io::Error::from)
1357}
1358
1359#[cfg(all(
1360 unix,
1361 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1362))]
1363fn remove_unix_directory_contents(directory: &rustix::fd::OwnedFd) -> io::Result<()> {
1364 use rustix::fs::{fstat, fsync, openat, unlinkat, AtFlags, Dir, FileType, Mode, OFlags};
1365 use std::os::unix::ffi::OsStrExt;
1366
1367 let mut entries = Dir::read_from(directory).map_err(io::Error::from)?;
1368 let mut names = Vec::new();
1369 for entry in &mut entries {
1370 let entry = entry.map_err(io::Error::from)?;
1371 let bytes = entry.file_name().to_bytes();
1372 if bytes != b"." && bytes != b".." {
1373 names.push(OsStr::from_bytes(bytes).to_os_string());
1374 }
1375 }
1376 for name in names {
1377 let child = openat(
1378 directory,
1379 Path::new(&name),
1380 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
1381 Mode::empty(),
1382 )
1383 .map_err(io::Error::from)?;
1384 match FileType::from_raw_mode(fstat(&child).map_err(io::Error::from)?.st_mode) {
1385 FileType::Directory => {
1386 remove_unix_directory_contents(&child)?;
1387 unlinkat(directory, Path::new(&name), AtFlags::REMOVEDIR)
1388 .map_err(io::Error::from)?;
1389 }
1390 FileType::RegularFile => {
1391 unlinkat(directory, Path::new(&name), AtFlags::empty()).map_err(io::Error::from)?;
1392 }
1393 _ => {
1394 return Err(io::Error::new(
1395 io::ErrorKind::InvalidInput,
1396 "refuses non-regular durable entry",
1397 ))
1398 }
1399 }
1400 }
1401 fsync(directory).map_err(io::Error::from)
1402}
1403
1404#[cfg(windows)]
1405fn remove_windows_directory_contents(path: &Path, _directory: &std::fs::File) -> io::Result<()> {
1406 let child_paths = std::fs::read_dir(path)?
1407 .map(|entry| entry.map(|entry| entry.path()))
1408 .collect::<io::Result<Vec<_>>>()?;
1409 for child_path in child_paths {
1410 let child = open_windows_for_delete(&child_path)?;
1411 let metadata =
1412 ensure_windows_not_reparse(&child, &child_path).map_err(mongrel_error_to_io)?;
1413 if metadata.is_dir() {
1414 remove_windows_directory_contents(&child_path, &child)?;
1415 } else if !metadata.is_file() {
1416 return Err(io::Error::new(
1417 io::ErrorKind::InvalidInput,
1418 "refuses non-regular durable entry",
1419 ));
1420 }
1421 delete_windows_handle(child)?;
1422 }
1423 sync_directory(path)
1424}
1425
1426fn checked_components(path: &Path) -> io::Result<Vec<OsString>> {
1427 let components = checked_components_allow_empty(path)?;
1428 if components.is_empty() {
1429 return Err(io::Error::new(
1430 io::ErrorKind::InvalidInput,
1431 "durable relative path must not be empty",
1432 ));
1433 }
1434 Ok(components)
1435}
1436
1437fn checked_components_allow_empty(path: &Path) -> io::Result<Vec<OsString>> {
1438 let mut components = Vec::new();
1439 for component in path.components() {
1440 match component {
1441 std::path::Component::Normal(name) => components.push(name.to_os_string()),
1442 _ => {
1443 return Err(io::Error::new(
1444 io::ErrorKind::InvalidInput,
1445 format!("invalid durable relative path {}", path.display()),
1446 ))
1447 }
1448 }
1449 }
1450 Ok(components)
1451}
1452
1453fn checked_parent(path: &Path) -> io::Result<(PathBuf, OsString)> {
1454 let components = checked_components(path)?;
1455 let name = components.last().cloned().unwrap();
1456 let parent = components[..components.len() - 1]
1457 .iter()
1458 .collect::<PathBuf>();
1459 Ok((parent, name))
1460}
1461
1462#[cfg(windows)]
1463fn mongrel_error_to_io(error: crate::MongrelError) -> io::Error {
1464 io::Error::new(io::ErrorKind::InvalidInput, error.to_string())
1465}
1466
1467pub(crate) fn open_regular_nofollow(path: &Path) -> crate::Result<std::fs::File> {
1469 #[cfg(all(
1470 unix,
1471 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1472 ))]
1473 {
1474 use rustix::fs::{fstat, openat, FileType, Mode, OFlags, CWD};
1475
1476 let fd = openat(
1477 CWD,
1478 path,
1479 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
1480 Mode::empty(),
1481 )
1482 .map_err(io::Error::from)?;
1483 if FileType::from_raw_mode(fstat(&fd).map_err(io::Error::from)?.st_mode)
1484 != FileType::RegularFile
1485 {
1486 return Err(crate::MongrelError::InvalidArgument(format!(
1487 "refuses non-regular file {}",
1488 path.display()
1489 )));
1490 }
1491 Ok(std::fs::File::from(fd))
1492 }
1493
1494 #[cfg(windows)]
1495 {
1496 let file = open_windows_nofollow(path)?;
1497 ensure_windows_regular(&file, path)?;
1498 return Ok(file);
1499 }
1500
1501 #[cfg(not(any(
1502 windows,
1503 all(
1504 unix,
1505 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1506 )
1507 )))]
1508 {
1509 let _ = path;
1510 Err(crate::MongrelError::Other(
1511 "no-follow file opens are unsupported on this platform".into(),
1512 ))
1513 }
1514}
1515
1516pub(crate) fn walk_regular_files_nofollow<P, D, F>(
1520 root: &Path,
1521 mut include: P,
1522 mut on_directory: D,
1523 mut on_file: F,
1524) -> crate::Result<()>
1525where
1526 P: FnMut(&Path, bool) -> crate::Result<bool>,
1527 D: FnMut(&Path) -> crate::Result<()>,
1528 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
1529{
1530 #[cfg(all(
1531 unix,
1532 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1533 ))]
1534 {
1535 use rustix::fs::{openat, Mode, OFlags, CWD};
1536
1537 let directory = openat(
1538 CWD,
1539 root,
1540 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
1541 Mode::empty(),
1542 )
1543 .map_err(io::Error::from)?;
1544 walk_unix_directory(
1545 directory,
1546 Path::new(""),
1547 &mut include,
1548 &mut on_directory,
1549 &mut on_file,
1550 )
1551 }
1552
1553 #[cfg(windows)]
1554 {
1555 let directory = open_windows_nofollow(root)?;
1556 ensure_windows_directory(&directory, root)?;
1557 return walk_windows_directory(
1558 root,
1559 Path::new(""),
1560 directory,
1561 &mut include,
1562 &mut on_directory,
1563 &mut on_file,
1564 );
1565 }
1566
1567 #[cfg(not(any(
1568 windows,
1569 all(
1570 unix,
1571 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1572 )
1573 )))]
1574 {
1575 let _ = (root, &mut include, &mut on_directory, &mut on_file);
1576 Err(crate::MongrelError::Other(
1577 "no-follow directory traversal is unsupported on this platform".into(),
1578 ))
1579 }
1580}
1581
1582#[cfg(all(
1583 unix,
1584 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1585))]
1586fn walk_unix_directory<P, D, F>(
1587 directory: rustix::fd::OwnedFd,
1588 relative: &Path,
1589 include: &mut P,
1590 on_directory: &mut D,
1591 on_file: &mut F,
1592) -> crate::Result<()>
1593where
1594 P: FnMut(&Path, bool) -> crate::Result<bool>,
1595 D: FnMut(&Path) -> crate::Result<()>,
1596 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
1597{
1598 use rustix::fs::{fstat, openat, Dir, FileType, Mode, OFlags};
1599 use std::ffi::OsStr;
1600 use std::os::unix::ffi::OsStrExt;
1601
1602 let mut entries = Dir::read_from(&directory).map_err(io::Error::from)?;
1603 let mut names = Vec::new();
1604 for entry in &mut entries {
1605 let entry = entry.map_err(io::Error::from)?;
1606 let bytes = entry.file_name().to_bytes();
1607 if bytes != b"." && bytes != b".." {
1608 names.push(OsStr::from_bytes(bytes).to_os_string());
1609 }
1610 }
1611 names.sort();
1612
1613 for name in names {
1614 let child_relative = relative.join(&name);
1615 let child = openat(
1616 &directory,
1617 Path::new(&name),
1618 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
1619 Mode::empty(),
1620 )
1621 .map_err(io::Error::from)?;
1622 match FileType::from_raw_mode(fstat(&child).map_err(io::Error::from)?.st_mode) {
1623 FileType::Directory => {
1624 if include(&child_relative, true)? {
1625 on_directory(&child_relative)?;
1626 walk_unix_directory(child, &child_relative, include, on_directory, on_file)?;
1627 }
1628 }
1629 FileType::RegularFile => {
1630 if include(&child_relative, false)? {
1631 on_file(&child_relative, &mut std::fs::File::from(child))?;
1632 }
1633 }
1634 _ => {
1635 return Err(crate::MongrelError::InvalidArgument(format!(
1636 "refuses non-regular entry {}",
1637 child_relative.display()
1638 )));
1639 }
1640 }
1641 }
1642 Ok(())
1643}
1644
1645#[cfg(windows)]
1646fn open_windows_nofollow(path: &Path) -> io::Result<std::fs::File> {
1647 use std::os::windows::fs::OpenOptionsExt;
1648 use windows_sys::Win32::Storage::FileSystem::{
1649 FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
1650 };
1651
1652 let mut options = std::fs::OpenOptions::new();
1653 options
1654 .read(true)
1655 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1656 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT);
1657 options.open(path)
1658}
1659
1660#[cfg(windows)]
1661fn open_windows_for_delete(path: &Path) -> io::Result<std::fs::File> {
1662 use std::os::windows::fs::OpenOptionsExt;
1663 use windows_sys::Win32::Foundation::GENERIC_READ;
1664 use windows_sys::Win32::Storage::FileSystem::{
1665 DELETE, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ,
1666 FILE_SHARE_WRITE,
1667 };
1668
1669 let mut options = std::fs::OpenOptions::new();
1670 options
1671 .read(true)
1672 .access_mode(GENERIC_READ | DELETE)
1673 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1674 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT);
1675 options.open(path)
1676}
1677
1678#[cfg(windows)]
1679fn delete_windows_handle(file: std::fs::File) -> io::Result<()> {
1680 use std::os::windows::io::AsRawHandle;
1681 use windows_sys::Win32::Storage::FileSystem::{
1682 FileDispositionInfo, SetFileInformationByHandle, FILE_DISPOSITION_INFO,
1683 };
1684
1685 let disposition = FILE_DISPOSITION_INFO { DeleteFile: true };
1686 let deleted = unsafe {
1687 SetFileInformationByHandle(
1688 file.as_raw_handle(),
1689 FileDispositionInfo,
1690 std::ptr::from_ref(&disposition).cast(),
1691 std::mem::size_of::<FILE_DISPOSITION_INFO>() as u32,
1692 )
1693 };
1694 if deleted == 0 {
1695 return Err(io::Error::last_os_error());
1696 }
1697 drop(file);
1698 Ok(())
1699}
1700
1701#[cfg(windows)]
1702fn ensure_windows_not_reparse(
1703 file: &std::fs::File,
1704 path: &Path,
1705) -> crate::Result<std::fs::Metadata> {
1706 use std::os::windows::fs::MetadataExt;
1707 use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
1708
1709 let metadata = file.metadata()?;
1710 if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1711 return Err(crate::MongrelError::InvalidArgument(format!(
1712 "refuses reparse point {}",
1713 path.display()
1714 )));
1715 }
1716 Ok(metadata)
1717}
1718
1719#[cfg(windows)]
1720fn ensure_windows_regular(file: &std::fs::File, path: &Path) -> crate::Result<()> {
1721 if !ensure_windows_not_reparse(file, path)?.is_file() {
1722 return Err(crate::MongrelError::InvalidArgument(format!(
1723 "refuses non-regular file {}",
1724 path.display()
1725 )));
1726 }
1727 Ok(())
1728}
1729
1730#[cfg(windows)]
1731fn ensure_windows_directory(file: &std::fs::File, path: &Path) -> crate::Result<()> {
1732 if !ensure_windows_not_reparse(file, path)?.is_dir() {
1733 return Err(crate::MongrelError::InvalidArgument(format!(
1734 "refuses non-directory {}",
1735 path.display()
1736 )));
1737 }
1738 Ok(())
1739}
1740
1741#[cfg(windows)]
1742fn walk_windows_directory<P, D, F>(
1743 path: &Path,
1744 relative: &Path,
1745 _directory: std::fs::File,
1746 include: &mut P,
1747 on_directory: &mut D,
1748 on_file: &mut F,
1749) -> crate::Result<()>
1750where
1751 P: FnMut(&Path, bool) -> crate::Result<bool>,
1752 D: FnMut(&Path) -> crate::Result<()>,
1753 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
1754{
1755 let mut entries = std::fs::read_dir(path)?.collect::<io::Result<Vec<_>>>()?;
1756 entries.sort_by_key(std::fs::DirEntry::file_name);
1757 for entry in entries {
1758 let child_path = entry.path();
1759 let child_relative = relative.join(entry.file_name());
1760 let mut child = open_windows_nofollow(&child_path)?;
1761 let metadata = ensure_windows_not_reparse(&child, &child_path)?;
1762 if metadata.is_dir() {
1763 if include(&child_relative, true)? {
1764 on_directory(&child_relative)?;
1765 walk_windows_directory(
1766 &child_path,
1767 &child_relative,
1768 child,
1769 include,
1770 on_directory,
1771 on_file,
1772 )?;
1773 }
1774 } else if metadata.is_file() {
1775 if include(&child_relative, false)? {
1776 on_file(&child_relative, &mut child)?;
1777 }
1778 } else {
1779 return Err(crate::MongrelError::InvalidArgument(format!(
1780 "refuses non-regular entry {}",
1781 child_path.display()
1782 )));
1783 }
1784 }
1785 Ok(())
1786}
1787
1788fn parent_or_current(path: &Path) -> &Path {
1789 path.parent()
1790 .filter(|parent| !parent.as_os_str().is_empty())
1791 .unwrap_or_else(|| Path::new("."))
1792}
1793
1794pub(crate) fn create_directory_all(path: &Path) -> io::Result<()> {
1796 if path.is_dir() {
1797 return sync_parent(path);
1798 }
1799 if path.exists() {
1800 return Err(io::Error::new(
1801 io::ErrorKind::AlreadyExists,
1802 format!("{} exists and is not a directory", path.display()),
1803 ));
1804 }
1805 let parent = parent_or_current(path);
1806 if !parent.is_dir() {
1807 create_directory_all(parent)?;
1808 }
1809 create_directory(path)
1810}
1811
1812pub(crate) fn create_directory(path: &Path) -> io::Result<()> {
1815 if path.is_dir() {
1816 return sync_parent(path);
1817 }
1818 if path.exists() {
1819 return Err(io::Error::new(
1820 io::ErrorKind::AlreadyExists,
1821 format!("{} exists and is not a directory", path.display()),
1822 ));
1823 }
1824
1825 #[cfg(unix)]
1826 {
1827 match std::fs::create_dir(path) {
1828 Ok(()) => sync_parent(path),
1829 Err(error) if error.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => {
1830 sync_parent(path)
1831 }
1832 Err(error) => Err(error),
1833 }
1834 }
1835
1836 #[cfg(windows)]
1837 {
1838 let parent = parent_or_current(path);
1839 let stage = parent.join(format!(
1840 ".mongreldb-dir-{}-{}",
1841 std::process::id(),
1842 std::time::SystemTime::now()
1843 .duration_since(std::time::UNIX_EPOCH)
1844 .unwrap_or_default()
1845 .as_nanos()
1846 ));
1847 std::fs::create_dir(&stage)?;
1848 match rename(&stage, path) {
1849 Ok(()) => Ok(()),
1850 Err(_error) if path.is_dir() => {
1851 let _ = std::fs::remove_dir(&stage);
1852 Ok(())
1853 }
1854 Err(error) => {
1855 let _ = std::fs::remove_dir(&stage);
1856 Err(error)
1857 }
1858 }
1859 }
1860
1861 #[cfg(not(any(unix, windows)))]
1862 {
1863 let _ = path;
1864 Err(io::Error::new(
1865 io::ErrorKind::Unsupported,
1866 "durable directory creation is unsupported on this platform",
1867 ))
1868 }
1869}
1870
1871pub(crate) fn remove_directory_all(path: &Path) -> io::Result<()> {
1873 remove_directory_all_with_after(path, || {})
1874}
1875
1876fn remove_directory_all_with_after<F>(path: &Path, after_unlink: F) -> io::Result<()>
1877where
1878 F: FnOnce(),
1879{
1880 match std::fs::remove_dir_all(path) {
1881 Ok(()) => {
1882 after_unlink();
1883 sync_parent(path)
1884 }
1885 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1886 after_unlink();
1889 sync_parent(path)
1890 }
1891 Err(error) => Err(error),
1892 }
1893}
1894
1895pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
1898 write_atomic_with_after(path, bytes, || {})
1899}
1900
1901pub(crate) fn write_atomic_with_after<F>(
1904 path: &Path,
1905 bytes: &[u8],
1906 after_publish: F,
1907) -> io::Result<()>
1908where
1909 F: FnOnce(),
1910{
1911 let parent = parent_or_current(path);
1912 if !parent.is_dir() {
1913 create_directory_all(parent)?;
1914 }
1915 let file_name = path.file_name().ok_or_else(|| {
1916 io::Error::new(
1917 io::ErrorKind::InvalidInput,
1918 format!("{} has no file name", path.display()),
1919 )
1920 })?;
1921 let root = DurableRoot::open(parent)?;
1922 root.write_atomic_with_after(Path::new(file_name), bytes, after_publish)
1923}
1924
1925pub(crate) fn rename(source: &Path, destination: &Path) -> io::Result<()> {
1928 rename_with_after(source, destination, || {})
1929}
1930
1931pub(crate) fn rename_with_after<F>(
1934 source: &Path,
1935 destination: &Path,
1936 after_publish: F,
1937) -> io::Result<()>
1938where
1939 F: FnOnce(),
1940{
1941 #[cfg(all(
1942 unix,
1943 any(
1944 target_os = "linux",
1945 target_os = "android",
1946 target_vendor = "apple",
1947 target_os = "redox"
1948 )
1949 ))]
1950 {
1951 use rustix::fs::{renameat_with, RenameFlags, CWD};
1952
1953 renameat_with(CWD, source, CWD, destination, RenameFlags::NOREPLACE)
1954 .map_err(io::Error::from)?;
1955 after_publish();
1956 if source.parent() == destination.parent() {
1957 sync_parent(destination)?;
1958 } else {
1959 sync_parent(destination)?;
1962 sync_parent(source)?;
1963 }
1964 Ok(())
1965 }
1966
1967 #[cfg(all(
1968 unix,
1969 not(any(
1970 target_os = "linux",
1971 target_os = "android",
1972 target_vendor = "apple",
1973 target_os = "redox"
1974 ))
1975 ))]
1976 {
1977 let _ = (source, destination, after_publish);
1978 Err(io::Error::new(
1979 io::ErrorKind::Unsupported,
1980 "atomic no-replace rename is unsupported on this platform",
1981 ))
1982 }
1983
1984 #[cfg(windows)]
1985 {
1986 use std::os::windows::ffi::OsStrExt;
1987 use windows_sys::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_WRITE_THROUGH};
1988
1989 let source = source
1990 .as_os_str()
1991 .encode_wide()
1992 .chain(std::iter::once(0))
1993 .collect::<Vec<_>>();
1994 let destination = destination
1995 .as_os_str()
1996 .encode_wide()
1997 .chain(std::iter::once(0))
1998 .collect::<Vec<_>>();
1999 let result = unsafe {
2000 MoveFileExW(
2001 source.as_ptr(),
2002 destination.as_ptr(),
2003 MOVEFILE_WRITE_THROUGH,
2004 )
2005 };
2006 if result == 0 {
2007 return Err(io::Error::last_os_error());
2008 }
2009 after_publish();
2010 Ok(())
2011 }
2012
2013 #[cfg(not(any(unix, windows)))]
2014 {
2015 let _ = (source, destination, after_publish);
2016 Err(io::Error::new(
2017 io::ErrorKind::Unsupported,
2018 "durable rename is unsupported on this platform",
2019 ))
2020 }
2021}
2022
2023pub(crate) fn sync_directory(path: &Path) -> io::Result<()> {
2025 mongreldb_types::durability::sync_directory(path)
2026}
2027
2028fn sync_parent(path: &Path) -> io::Result<()> {
2029 sync_directory(parent_or_current(path))
2030}
2031
2032pub(crate) fn replace(source: &Path, destination: &Path) -> io::Result<()> {
2034 replace_with_after(source, destination, || {})
2035}
2036
2037pub(crate) fn replace_with_after<F>(
2040 source: &Path,
2041 destination: &Path,
2042 after_publish: F,
2043) -> io::Result<()>
2044where
2045 F: FnOnce(),
2046{
2047 #[cfg(unix)]
2048 {
2049 std::fs::rename(source, destination)?;
2050 after_publish();
2051 std::fs::File::open(destination.parent().unwrap_or_else(|| Path::new(".")))?.sync_all()
2052 }
2053
2054 #[cfg(windows)]
2055 {
2056 use std::os::windows::ffi::OsStrExt;
2057 use windows_sys::Win32::Storage::FileSystem::{
2058 MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
2059 };
2060
2061 let source = source
2062 .as_os_str()
2063 .encode_wide()
2064 .chain(std::iter::once(0))
2065 .collect::<Vec<_>>();
2066 let destination = destination
2067 .as_os_str()
2068 .encode_wide()
2069 .chain(std::iter::once(0))
2070 .collect::<Vec<_>>();
2071 let result = unsafe {
2072 MoveFileExW(
2073 source.as_ptr(),
2074 destination.as_ptr(),
2075 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
2076 )
2077 };
2078 if result == 0 {
2079 return Err(io::Error::last_os_error());
2080 }
2081 after_publish();
2082 Ok(())
2083 }
2084
2085 #[cfg(not(any(unix, windows)))]
2086 {
2087 let _ = (source, destination, after_publish);
2088 Err(io::Error::new(
2089 io::ErrorKind::Unsupported,
2090 "durable atomic replacement is unsupported on this platform",
2091 ))
2092 }
2093}
2094
2095#[cfg(test)]
2096mod common_tests {
2097 use super::*;
2098 use std::io::Read;
2099 use std::sync::{Arc, Barrier};
2100
2101 #[test]
2102 fn atomic_new_file_publishes_one_complete_winner() {
2103 let directory = tempfile::tempdir().unwrap();
2104 let root = Arc::new(DurableRoot::open(directory.path()).unwrap());
2105 let barrier = Arc::new(Barrier::new(2));
2106 let writers = (*b"ab").map(|byte| {
2107 let root = Arc::clone(&root);
2108 let barrier = Arc::clone(&barrier);
2109 std::thread::spawn(move || {
2110 barrier.wait();
2111 root.write_new_atomic("key", &[byte; 32])
2112 })
2113 });
2114 let results = writers.map(|writer| writer.join().unwrap());
2115 assert_eq!(results.iter().filter(|result| result.is_ok()).count(), 1);
2116 assert_eq!(
2117 results
2118 .iter()
2119 .filter(|result| {
2120 result
2121 .as_ref()
2122 .is_err_and(|error| error.kind() == io::ErrorKind::AlreadyExists)
2123 })
2124 .count(),
2125 1
2126 );
2127 let mut bytes = Vec::new();
2128 root.open_regular("key")
2129 .unwrap()
2130 .read_to_end(&mut bytes)
2131 .unwrap();
2132 assert!(bytes == [b'a'; 32] || bytes == [b'b'; 32]);
2133 assert_eq!(std::fs::read_dir(directory.path()).unwrap().count(), 1);
2134 }
2135}
2136
2137#[cfg(all(test, windows))]
2138mod windows_tests {
2139 use super::*;
2140
2141 #[test]
2142 fn durable_root_identity_is_stable() {
2143 let root = tempfile::tempdir().unwrap();
2144 let first = DurableRoot::open(root.path()).unwrap();
2145 let second = DurableRoot::open(root.path()).unwrap();
2146
2147 assert_eq!(
2148 first.file_identity().unwrap(),
2149 second.file_identity().unwrap()
2150 );
2151 }
2152
2153 #[test]
2154 fn descriptor_removal_deletes_validated_files_and_trees() {
2155 let root = tempfile::tempdir().unwrap();
2156 let durable = DurableRoot::open(root.path()).unwrap();
2157 durable.create_directory_all("stale/nested").unwrap();
2158 durable.write_new("stale/nested/chunk", b"stale").unwrap();
2159 durable.write_new("orphan", b"stale").unwrap();
2160
2161 durable.remove_file("orphan").unwrap();
2162 durable.remove_directory_all("stale").unwrap();
2163
2164 assert!(!root.path().join("orphan").exists());
2165 assert!(!root.path().join("stale").exists());
2166 }
2167}
2168
2169#[cfg(all(test, unix))]
2170mod tests {
2171 use super::*;
2172 use std::io::Read;
2173 use std::os::unix::fs::symlink;
2174 use std::sync::{Arc, Barrier};
2175
2176 #[test]
2177 fn no_follow_file_and_tree_reject_symlinks() {
2178 let root = tempfile::tempdir().unwrap();
2179 let outside = tempfile::NamedTempFile::new().unwrap();
2180 let link = root.path().join("escape");
2181 symlink(outside.path(), &link).unwrap();
2182
2183 assert!(open_regular_nofollow(&link).is_err());
2184 let result = walk_regular_files_nofollow(
2185 root.path(),
2186 |_, _| Ok(true),
2187 |_| Ok(()),
2188 |_, file| {
2189 let mut bytes = Vec::new();
2190 file.read_to_end(&mut bytes)?;
2191 Ok(())
2192 },
2193 );
2194 assert!(result.is_err());
2195 }
2196
2197 #[test]
2198 fn descriptor_root_rejects_intermediate_symlinks() {
2199 let root = tempfile::tempdir().unwrap();
2200 let outside = tempfile::tempdir().unwrap();
2201 std::fs::create_dir(root.path().join("state")).unwrap();
2202 std::fs::write(outside.path().join("receipt"), b"outside").unwrap();
2203 symlink(outside.path(), root.path().join("state").join("escape")).unwrap();
2204 let durable = DurableRoot::open(root.path()).unwrap();
2205
2206 assert!(durable.open_regular("state/escape/receipt").is_err());
2207 assert!(durable.write_new("state/escape/new", b"bad").is_err());
2208 assert!(!outside.path().join("new").exists());
2209 }
2210
2211 #[test]
2212 fn atomic_write_ignores_orphaned_fixed_temporary_name() {
2213 let root = tempfile::tempdir().unwrap();
2214 let destination = root.path().join("state");
2215 let orphan = root.path().join(".state.tmp");
2216 std::fs::write(&orphan, b"orphan").unwrap();
2217
2218 write_atomic(&destination, b"published").unwrap();
2219
2220 assert_eq!(std::fs::read(destination).unwrap(), b"published");
2221 assert_eq!(std::fs::read(orphan).unwrap(), b"orphan");
2222 }
2223
2224 #[test]
2225 fn descriptor_atomic_write_reports_visible_publication_before_return() {
2226 let root = tempfile::tempdir().unwrap();
2227 let durable = DurableRoot::open(root.path()).unwrap();
2228 let published = std::cell::Cell::new(false);
2229
2230 durable
2231 .write_atomic_with_after("state", b"published", || {
2232 assert_eq!(
2233 std::fs::read(root.path().join("state")).unwrap(),
2234 b"published"
2235 );
2236 published.set(true);
2237 })
2238 .unwrap();
2239
2240 assert!(published.get());
2241 }
2242
2243 #[test]
2244 fn parent_sync_failure_is_not_swallowed_after_publication() {
2245 let root = tempfile::tempdir().unwrap();
2246 let parent = root.path().join("parent");
2247 let moved = root.path().join("moved");
2248 std::fs::create_dir(&parent).unwrap();
2249 let source = parent.join("source");
2250 let destination = parent.join("destination");
2251 std::fs::write(&source, b"durable").unwrap();
2252
2253 let error = replace_with_after(&source, &destination, || {
2254 std::fs::rename(&parent, &moved).unwrap();
2255 })
2256 .unwrap_err();
2257
2258 assert_eq!(error.kind(), io::ErrorKind::NotFound);
2259 assert_eq!(
2260 std::fs::read(moved.join("destination")).unwrap(),
2261 b"durable"
2262 );
2263 }
2264
2265 #[test]
2266 fn cross_directory_rename_syncs_destination_before_source() {
2267 let root = tempfile::tempdir().unwrap();
2268 let source_parent = root.path().join("source-parent");
2269 let destination_parent = root.path().join("destination-parent");
2270 let moved_source_parent = root.path().join("moved-source-parent");
2271 std::fs::create_dir(&source_parent).unwrap();
2272 std::fs::create_dir(&destination_parent).unwrap();
2273 let source = source_parent.join("source");
2274 let destination = destination_parent.join("destination");
2275 std::fs::write(&source, b"durable").unwrap();
2276
2277 let error = rename_with_after(&source, &destination, || {
2278 std::fs::rename(&source_parent, &moved_source_parent).unwrap();
2279 })
2280 .unwrap_err();
2281
2282 assert_eq!(error.kind(), io::ErrorKind::NotFound);
2283 assert_eq!(std::fs::read(&destination).unwrap(), b"durable");
2284 }
2285
2286 #[test]
2287 fn no_replace_rename_never_clobbers_a_racing_destination() {
2288 for iteration in 0..128 {
2289 let root = tempfile::tempdir().unwrap();
2290 let source = root.path().join("source");
2291 let destination = root.path().join("destination");
2292 std::fs::write(&source, b"source").unwrap();
2293 let barrier = Arc::new(Barrier::new(3));
2294
2295 let create_barrier = Arc::clone(&barrier);
2296 let create_destination = destination.clone();
2297 let creator = std::thread::spawn(move || {
2298 create_barrier.wait();
2299 std::fs::OpenOptions::new()
2300 .write(true)
2301 .create_new(true)
2302 .open(create_destination)
2303 .map(|mut file| file.write_all(b"racer"))
2304 });
2305
2306 let rename_barrier = Arc::clone(&barrier);
2307 let rename_source = source.clone();
2308 let rename_destination = destination.clone();
2309 let renamer = std::thread::spawn(move || {
2310 rename_barrier.wait();
2311 rename(&rename_source, &rename_destination)
2312 });
2313
2314 barrier.wait();
2315 let create_result = creator.join().unwrap();
2316 let rename_result = renamer.join().unwrap();
2317 let destination_bytes = std::fs::read(&destination).unwrap();
2318
2319 match (create_result, rename_result) {
2320 (Ok(Ok(())), Err(error)) => {
2321 assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
2322 assert_eq!(destination_bytes, b"racer", "iteration {iteration}");
2323 assert_eq!(std::fs::read(&source).unwrap(), b"source");
2324 }
2325 (Err(error), Ok(())) => {
2326 assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
2327 assert_eq!(destination_bytes, b"source", "iteration {iteration}");
2328 assert!(!source.exists());
2329 }
2330 (create_result, rename_result) => panic!(
2331 "unexpected race results at iteration {iteration}: create={create_result:?}, rename={rename_result:?}"
2332 ),
2333 }
2334 }
2335 }
2336
2337 #[test]
2338 fn missing_directory_retry_still_reports_parent_sync_failure() {
2339 let root = tempfile::tempdir().unwrap();
2340 let parent = root.path().join("parent");
2341 let moved_parent = root.path().join("moved-parent");
2342 std::fs::create_dir(&parent).unwrap();
2343 let missing = parent.join("already-removed");
2344
2345 let error = remove_directory_all_with_after(&missing, || {
2346 std::fs::rename(&parent, &moved_parent).unwrap();
2347 })
2348 .unwrap_err();
2349
2350 assert_eq!(error.kind(), io::ErrorKind::NotFound);
2351 }
2352
2353 #[test]
2366 fn deferred_sync_shallow_makes_files_durable_and_re_enables_eager() {
2367 let root = tempfile::tempdir().unwrap();
2368 std::fs::create_dir_all(root.path()).unwrap();
2369
2370 let durable = DurableRoot::open_deferred(root.path()).unwrap();
2371 assert!(
2372 !durable.should_sync_eagerly(),
2373 "open_deferred must start in deferred mode"
2374 );
2375
2376 durable.write_new("schema", b"schema-bytes").unwrap();
2378 durable.write_atomic("manifest", b"manifest-bytes").unwrap();
2379 durable.create_directory_all_pinned("_wal").unwrap();
2380 durable.write_new("_wal/seg-0.wal", b"wbyts").unwrap();
2381
2382 durable
2384 .finalize_deferred_sync_shallow()
2385 .expect("shallow finalize must succeed");
2386 assert!(
2387 durable.should_sync_eagerly(),
2388 "finalize must re-enable eager fsync"
2389 );
2390
2391 assert_eq!(
2393 std::fs::read(root.path().join("schema")).unwrap(),
2394 b"schema-bytes"
2395 );
2396 assert_eq!(
2397 std::fs::read(root.path().join("manifest")).unwrap(),
2398 b"manifest-bytes"
2399 );
2400
2401 durable.write_new("post-finalize", b"ok").unwrap();
2406 assert_eq!(
2407 std::fs::read(root.path().join("post-finalize")).unwrap(),
2408 b"ok"
2409 );
2410 }
2411
2412 #[test]
2416 fn deferred_sync_recursive_handles_subdir_files() {
2417 let root = tempfile::tempdir().unwrap();
2418 std::fs::create_dir_all(root.path()).unwrap();
2419
2420 let durable = DurableRoot::open_deferred(root.path()).unwrap();
2421 durable.create_directory_all_pinned("_meta").unwrap();
2422 durable.write_new("_meta/salt", b"salt-bytes").unwrap();
2423 durable.write_new("manifest", b"mf").unwrap();
2424
2425 durable.finalize_deferred_sync().unwrap();
2426 assert!(durable.should_sync_eagerly());
2427
2428 assert_eq!(
2429 std::fs::read(root.path().join("_meta").join("salt")).unwrap(),
2430 b"salt-bytes"
2431 );
2432 }
2433
2434 #[test]
2436 fn finalize_on_eager_root_is_noop() {
2437 let root = tempfile::tempdir().unwrap();
2438 std::fs::create_dir_all(root.path()).unwrap();
2439 let durable = DurableRoot::open(root.path()).unwrap();
2440 assert!(durable.should_sync_eagerly());
2441 durable.finalize_deferred_sync_shallow().unwrap();
2442 assert!(durable.should_sync_eagerly());
2443 }
2444}