Skip to main content

mongreldb_core/
durable_file.rs

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