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