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