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