Skip to main content

mongreldb_core/
durable_file.rs

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