Skip to main content

mongreldb_core/
durable_file.rs

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