Skip to main content

dbmd_core/
fsx.rs

1//! `fsx` — the one atomic, durable file write for db.md's primary data.
2//!
3//! Every store-state file that holds **primary** data — content records
4//! ([`crate::parser::write_file`]), `log.md` and its archives ([`crate::log`]),
5//! and in-place link rewrites — is written through [`write_atomic`] or
6//! [`write_atomic_new`]:
7//!
8//! 1. write the bytes to a uniquely-named sibling temp file in the *same*
9//!    directory (`create_new`, so a predictable temp name can never be
10//!    clobbered — closing the temp-clobber race);
11//! 2. `fsync` the temp file;
12//! 3. either `rename` it over the destination ([`write_atomic`]) or hard-link it
13//!    into place with create-new semantics ([`write_atomic_new`]);
14//! 4. `fsync` the parent directory so the committed directory entry survives a
15//!    crash.
16//!
17//! These are the only primitives for durable writes — never `std::fs::write`,
18//! which is neither atomic nor crash-durable. Use [`write_atomic`] when replacing
19//! an existing file is intended; use [`write_atomic_new`] when the destination
20//! must not already exist.
21//!
22//! **Not for the index.** `index.md` / `index.jsonl` are *derived, rebuildable*
23//! artifacts on the O(changed) write-through path; they use their own
24//! atomic-but-not-`fsync`'d writer ([`crate::index`]'s `AtomicTemp`) on purpose
25//! — a crash-lost index write is recovered by `dbmd index rebuild`, so paying an
26//! `fsync` per catalog update on the hot loop would be cost without benefit.
27
28#[cfg(unix)]
29use std::collections::BTreeMap;
30use std::fs::File;
31#[cfg(test)]
32use std::fs::{self, OpenOptions};
33use std::io::{Read, Write};
34#[cfg(unix)]
35use std::path::Component;
36use std::path::{Path, PathBuf};
37use std::sync::atomic::{AtomicU64, Ordering};
38use std::time::{SystemTime, UNIX_EPOCH};
39
40/// Atomically and durably replace `path` with `bytes` (see the module docs for
41/// the write/fsync/rename/fsync sequence). The parent directory is created if
42/// missing. On *any* early return between temp-file creation and a successful
43/// rename — a `write_all`/`sync_all` failure (ENOSPC, EIO, quota) as well as a
44/// rename failure — the temp file is cleaned up rather than leaked, via the
45/// [`TempGuard`] `Drop` impl (mirroring `index.rs`'s `AtomicTemp`).
46pub fn write_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
47    #[cfg(unix)]
48    {
49        write_atomic_unix(path, bytes, false, true)
50    }
51    #[cfg(not(unix))]
52    {
53        let _ = (path, bytes);
54        Err(secure_filesystem_unsupported())
55    }
56}
57
58/// Atomically and durably create `path` with `bytes`, failing with
59/// [`std::io::ErrorKind::AlreadyExists`] if the destination already exists.
60///
61/// This follows the same temp-file + file-fsync + parent-fsync sequence as
62/// [`write_atomic`], but installs the temp file with `hard_link(temp, path)`
63/// instead of `rename(temp, path)`. Hard-link creation is resolved atomically by
64/// the OS and refuses an existing destination, so concurrent creators for the
65/// same path produce exactly one winner and `AlreadyExists` for the rest. The
66/// temporary link is removed after the destination link is established.
67pub fn write_atomic_new(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
68    #[cfg(unix)]
69    {
70        write_atomic_unix(path, bytes, true, true)
71    }
72    #[cfg(not(unix))]
73    {
74        let _ = (path, bytes);
75        Err(secure_filesystem_unsupported())
76    }
77}
78
79#[cfg(not(unix))]
80fn secure_filesystem_unsupported() -> std::io::Error {
81    std::io::Error::new(
82        std::io::ErrorKind::Unsupported,
83        "secure filesystem mutation requires handle-relative no-follow primitives on this platform",
84    )
85}
86
87/// Open one regular file exactly once through no-follow directory handles and
88/// read at most `max_bytes`. The size check and read operate on the same inode.
89pub fn read_bounded_nofollow(path: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
90    let file = open_regular_nofollow(path)?;
91    read_bounded_file(file, max_bytes)
92}
93
94fn read_bounded_file(file: File, max_bytes: u64) -> std::io::Result<Vec<u8>> {
95    let metadata = file.metadata()?;
96    if metadata.len() > max_bytes {
97        return Err(std::io::Error::new(
98            std::io::ErrorKind::InvalidData,
99            "file is not a bounded regular file",
100        ));
101    }
102    let mut bytes = Vec::with_capacity(metadata.len() as usize);
103    // `u64::MAX + 1` wraps in release builds and panics in debug builds. A
104    // caller is allowed to express "no smaller than the addressable stream" as
105    // `u64::MAX`; in that case there is no representable sentinel byte beyond
106    // the limit, so read to the saturated ceiling and rely on the descriptor
107    // metadata check above as the only possible over-limit signal.
108    file.take(max_bytes.saturating_add(1))
109        .read_to_end(&mut bytes)?;
110    if bytes.len() as u64 > max_bytes {
111        return Err(std::io::Error::new(
112            std::io::ErrorKind::InvalidData,
113            "file grew beyond the read limit",
114        ));
115    }
116    Ok(bytes)
117}
118
119/// A held store-directory capability for bounded sweep reads. Parent directory
120/// handles are cached by relative path, so a 10k-file scan pays one no-follow
121/// traversal per folder and one `openat` per file instead of reopening the
122/// entire ancestor chain for every record.
123#[cfg(unix)]
124#[derive(Debug)]
125pub(crate) struct BoundedDirReader {
126    root: File,
127    parents: BTreeMap<PathBuf, File>,
128}
129
130#[cfg(unix)]
131impl BoundedDirReader {
132    #[cfg_attr(not(test), allow(dead_code))]
133    pub(crate) fn new(root: &Path) -> std::io::Result<Self> {
134        let root = open_directory_nofollow(root)?;
135        Ok(Self {
136            root,
137            parents: BTreeMap::new(),
138        })
139    }
140
141    /// Start a bounded reader from an already-held directory capability. This
142    /// is the store-safe constructor: once `Store::open` succeeds, no later
143    /// operation re-resolves the user-supplied store pathname.
144    pub(crate) fn from_root(root: &File) -> std::io::Result<Self> {
145        Ok(Self {
146            root: root.try_clone()?,
147            parents: BTreeMap::new(),
148        })
149    }
150
151    pub(crate) fn read(&mut self, relative: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
152        read_bounded_file(self.open(relative)?, max_bytes)
153    }
154
155    pub(crate) fn open(&mut self, relative: &Path) -> std::io::Result<File> {
156        use std::os::fd::{AsRawFd as _, FromRawFd as _};
157
158        if relative.is_absolute()
159            || relative
160                .components()
161                .any(|component| matches!(component, Component::ParentDir | Component::Prefix(_)))
162        {
163            return Err(std::io::Error::new(
164                std::io::ErrorKind::PermissionDenied,
165                "bounded directory read requires a contained relative path",
166            ));
167        }
168        let leaf = relative.file_name().ok_or_else(|| {
169            std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
170        })?;
171        let parent = relative.parent().unwrap_or_else(|| Path::new(""));
172
173        if !self.parents.contains_key(parent) {
174            let mut cursor = self.root.try_clone()?;
175            for component in parent.components() {
176                let Component::Normal(name) = component else {
177                    continue;
178                };
179                let fd = unsafe {
180                    libc::openat(
181                        cursor.as_raw_fd(),
182                        c_name(name)?.as_ptr(),
183                        libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
184                    )
185                };
186                if fd < 0 {
187                    return Err(std::io::Error::last_os_error());
188                }
189                cursor = unsafe { File::from_raw_fd(fd) };
190                if directory_contains_exact_regular(&cursor, "DB.md".as_ref())? {
191                    return Err(std::io::Error::new(
192                        std::io::ErrorKind::PermissionDenied,
193                        "refusing to cross a nested db.md store boundary",
194                    ));
195                }
196            }
197            self.parents.insert(parent.to_path_buf(), cursor);
198        }
199
200        let parent_fd = self
201            .parents
202            .get(parent)
203            .expect("parent capability inserted above")
204            .as_raw_fd();
205        let fd = unsafe {
206            libc::openat(
207                parent_fd,
208                c_name(leaf)?.as_ptr(),
209                libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
210            )
211        };
212        if fd < 0 {
213            return Err(std::io::Error::last_os_error());
214        }
215        let file = unsafe { File::from_raw_fd(fd) };
216        if !file.metadata()?.is_file() {
217            return Err(std::io::Error::new(
218                std::io::ErrorKind::InvalidData,
219                "refusing to read a non-regular file",
220            ));
221        }
222        Ok(file)
223    }
224}
225
226/// Open one directory exactly once without following its leaf or any ancestor.
227/// Store roots retain this descriptor for their full lifetime so a rename or
228/// symlink swap of the original pathname cannot redirect later operations.
229#[cfg(unix)]
230pub(crate) fn open_directory_nofollow(path: &Path) -> std::io::Result<File> {
231    // Reuse the ancestor traversal by appending a synthetic leaf and retaining
232    // the returned parent. Unlike `path.file_name()`, this also handles the
233    // ordinary store spellings `.` and `/`.
234    let (directory, _) = open_parent_unix(&path.join(".dbmd-held-root-capability"), false)?;
235    Ok(directory)
236}
237
238#[cfg(not(unix))]
239pub(crate) fn open_directory_nofollow(_path: &Path) -> std::io::Result<File> {
240    Err(secure_filesystem_unsupported())
241}
242
243/// Test for an exact byte-for-byte regular-file basename inside a held
244/// directory. This is deliberately descriptor-relative: on a case-insensitive
245/// filesystem `openat(dir, "DB.md")` can open a lowercase `db.md`, while the
246/// db.md format requires the uppercase marker spelling.
247#[cfg(unix)]
248pub(crate) fn directory_contains_exact_regular(
249    directory: &File,
250    wanted: &std::ffi::OsStr,
251) -> std::io::Result<bool> {
252    use std::os::fd::AsRawFd as _;
253    use std::os::unix::ffi::OsStrExt as _;
254
255    let dot = c_name(std::ffi::OsStr::new("."))?;
256    let scan_fd = unsafe {
257        libc::openat(
258            directory.as_raw_fd(),
259            dot.as_ptr(),
260            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
261        )
262    };
263    if scan_fd < 0 {
264        return Err(std::io::Error::last_os_error());
265    }
266    let stream = unsafe { libc::fdopendir(scan_fd) };
267    if stream.is_null() {
268        let error = std::io::Error::last_os_error();
269        unsafe {
270            libc::close(scan_fd);
271        }
272        return Err(error);
273    }
274
275    let mut found = false;
276    loop {
277        let entry = unsafe { libc::readdir(stream) };
278        if entry.is_null() {
279            break;
280        }
281        let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) };
282        if name.to_bytes() != wanted.as_bytes() {
283            continue;
284        }
285        let c_wanted = c_name(wanted)?;
286        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
287        if unsafe {
288            libc::fstatat(
289                directory.as_raw_fd(),
290                c_wanted.as_ptr(),
291                &mut stat,
292                libc::AT_SYMLINK_NOFOLLOW,
293            )
294        } == 0
295            && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
296        {
297            found = true;
298        }
299        break;
300    }
301    if unsafe { libc::closedir(stream) } != 0 {
302        return Err(std::io::Error::last_os_error());
303    }
304    Ok(found)
305}
306
307#[cfg(not(unix))]
308pub(crate) fn directory_contains_exact_regular(
309    _directory: &File,
310    _wanted: &std::ffi::OsStr,
311) -> std::io::Result<bool> {
312    Err(secure_filesystem_unsupported())
313}
314
315/// Recursively enumerate regular files below a held directory capability.
316///
317/// Symlinks, hidden names, and nested db.md stores are never traversed. Paths
318/// are returned relative to `root`, including the caller-supplied `start`
319/// prefix. This is the sweep-side counterpart to [`BoundedDirReader`]: callers
320/// do not reopen the mutable store pathname merely to discover what to read.
321#[cfg(unix)]
322pub(crate) fn walk_regular_files_beneath(
323    root: &File,
324    start: &Path,
325) -> std::io::Result<Vec<PathBuf>> {
326    use std::os::fd::AsRawFd as _;
327    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
328
329    const MAX_WALK_ENTRIES: usize = 1_000_000;
330
331    fn open_dir_at(parent: &File, name: &std::ffi::OsStr) -> std::io::Result<File> {
332        use std::os::fd::{AsRawFd as _, FromRawFd as _};
333        let fd = unsafe {
334            libc::openat(
335                parent.as_raw_fd(),
336                c_name(name)?.as_ptr(),
337                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
338            )
339        };
340        if fd < 0 {
341            return Err(std::io::Error::last_os_error());
342        }
343        Ok(unsafe { File::from_raw_fd(fd) })
344    }
345
346    if start.is_absolute()
347        || start.components().any(|component| {
348            matches!(
349                component,
350                Component::ParentDir | Component::Prefix(_) | Component::RootDir
351            )
352        })
353    {
354        return Err(std::io::Error::new(
355            std::io::ErrorKind::PermissionDenied,
356            "store walk requires a contained relative path",
357        ));
358    }
359
360    let mut start_dir = root.try_clone()?;
361    for component in start.components() {
362        let Component::Normal(name) = component else {
363            continue;
364        };
365        start_dir = open_dir_at(&start_dir, name)?;
366        if directory_contains_exact_regular(&start_dir, "DB.md".as_ref())? {
367            return Ok(Vec::new());
368        }
369    }
370
371    let mut pending = vec![(start_dir, start.to_path_buf())];
372    let mut files = Vec::new();
373    let mut seen = 0usize;
374    while let Some((directory, relative_dir)) = pending.pop() {
375        let scan_fd = unsafe {
376            libc::openat(
377                directory.as_raw_fd(),
378                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
379                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
380            )
381        };
382        if scan_fd < 0 {
383            return Err(std::io::Error::last_os_error());
384        }
385        let stream = unsafe { libc::fdopendir(scan_fd) };
386        if stream.is_null() {
387            let error = std::io::Error::last_os_error();
388            unsafe {
389                libc::close(scan_fd);
390            }
391            return Err(error);
392        }
393
394        let mut names = Vec::new();
395        loop {
396            let entry = unsafe { libc::readdir(stream) };
397            if entry.is_null() {
398                break;
399            }
400            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
401            if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
402                continue;
403            }
404            names.push(std::ffi::OsString::from_vec(bytes.to_vec()));
405            seen = seen.saturating_add(1);
406            if seen > MAX_WALK_ENTRIES {
407                unsafe {
408                    libc::closedir(stream);
409                }
410                return Err(std::io::Error::new(
411                    std::io::ErrorKind::InvalidData,
412                    "store contains more than 1000000 visible entries",
413                ));
414            }
415        }
416        if unsafe { libc::closedir(stream) } != 0 {
417            return Err(std::io::Error::last_os_error());
418        }
419        names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
420
421        for name in names {
422            let c_name = c_name(&name)?;
423            let mut stat: libc::stat = unsafe { std::mem::zeroed() };
424            if unsafe {
425                libc::fstatat(
426                    directory.as_raw_fd(),
427                    c_name.as_ptr(),
428                    &mut stat,
429                    libc::AT_SYMLINK_NOFOLLOW,
430                )
431            } != 0
432            {
433                let error = std::io::Error::last_os_error();
434                if error.kind() == std::io::ErrorKind::NotFound {
435                    continue;
436                }
437                return Err(error);
438            }
439            let relative = relative_dir.join(&name);
440            match stat.st_mode & libc::S_IFMT {
441                libc::S_IFREG => files.push(relative),
442                libc::S_IFDIR => {
443                    let child = open_dir_at(&directory, &name)?;
444                    if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
445                        pending.push((child, relative));
446                    }
447                }
448                _ => {}
449            }
450        }
451    }
452    files.sort();
453    Ok(files)
454}
455
456/// Discover visible symlinks and nested-store roots without following either.
457#[cfg(unix)]
458pub(crate) fn ownership_boundaries_beneath(
459    root: &File,
460) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
461    use std::os::fd::{AsRawFd as _, FromRawFd as _};
462    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
463
464    let mut pending = vec![(root.try_clone()?, PathBuf::new())];
465    let mut symlinks = Vec::new();
466    let mut nested = Vec::new();
467    while let Some((directory, relative_dir)) = pending.pop() {
468        let scan_fd = unsafe {
469            libc::openat(
470                directory.as_raw_fd(),
471                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
472                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
473            )
474        };
475        if scan_fd < 0 {
476            return Err(std::io::Error::last_os_error());
477        }
478        let stream = unsafe { libc::fdopendir(scan_fd) };
479        if stream.is_null() {
480            let error = std::io::Error::last_os_error();
481            unsafe {
482                libc::close(scan_fd);
483            }
484            return Err(error);
485        }
486        let mut names = Vec::new();
487        loop {
488            let entry = unsafe { libc::readdir(stream) };
489            if entry.is_null() {
490                break;
491            }
492            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
493            if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
494                continue;
495            }
496            names.push(std::ffi::OsString::from_vec(bytes.to_vec()));
497        }
498        if unsafe { libc::closedir(stream) } != 0 {
499            return Err(std::io::Error::last_os_error());
500        }
501        names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
502        for name in names {
503            let mut stat: libc::stat = unsafe { std::mem::zeroed() };
504            if unsafe {
505                libc::fstatat(
506                    directory.as_raw_fd(),
507                    c_name(&name)?.as_ptr(),
508                    &mut stat,
509                    libc::AT_SYMLINK_NOFOLLOW,
510                )
511            } != 0
512            {
513                continue;
514            }
515            let relative = relative_dir.join(&name);
516            match stat.st_mode & libc::S_IFMT {
517                libc::S_IFLNK => symlinks.push(relative),
518                libc::S_IFDIR => {
519                    let fd = unsafe {
520                        libc::openat(
521                            directory.as_raw_fd(),
522                            c_name(&name)?.as_ptr(),
523                            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
524                        )
525                    };
526                    if fd < 0 {
527                        return Err(std::io::Error::last_os_error());
528                    }
529                    let child = unsafe { File::from_raw_fd(fd) };
530                    if directory_contains_exact_regular(&child, "DB.md".as_ref())? {
531                        nested.push(relative);
532                    } else {
533                        pending.push((child, relative));
534                    }
535                }
536                _ => {}
537            }
538        }
539    }
540    symlinks.sort();
541    nested.sort();
542    Ok((symlinks, nested))
543}
544
545#[cfg(not(unix))]
546pub(crate) fn ownership_boundaries_beneath(
547    _root: &File,
548) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
549    Err(secure_filesystem_unsupported())
550}
551
552#[cfg(unix)]
553pub(crate) fn regular_file_names_beneath(
554    root: &File,
555    directory: &Path,
556) -> std::io::Result<Vec<std::ffi::OsString>> {
557    use std::os::fd::AsRawFd as _;
558    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
559
560    let probe = directory.join(".dbmd-directory-list-probe");
561    let (directory, _) = open_parent_beneath(root, &probe, false)?;
562    let scan_fd = unsafe {
563        libc::openat(
564            directory.as_raw_fd(),
565            c_name(std::ffi::OsStr::new("."))?.as_ptr(),
566            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
567        )
568    };
569    if scan_fd < 0 {
570        return Err(std::io::Error::last_os_error());
571    }
572    let stream = unsafe { libc::fdopendir(scan_fd) };
573    if stream.is_null() {
574        let error = std::io::Error::last_os_error();
575        unsafe {
576            libc::close(scan_fd);
577        }
578        return Err(error);
579    }
580
581    let mut names = Vec::new();
582    loop {
583        let entry = unsafe { libc::readdir(stream) };
584        if entry.is_null() {
585            break;
586        }
587        let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
588        if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
589            continue;
590        }
591        let name = std::ffi::OsString::from_vec(bytes.to_vec());
592        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
593        if unsafe {
594            libc::fstatat(
595                directory.as_raw_fd(),
596                c_name(&name)?.as_ptr(),
597                &mut stat,
598                libc::AT_SYMLINK_NOFOLLOW,
599            )
600        } == 0
601            && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
602        {
603            names.push(name);
604        }
605    }
606    if unsafe { libc::closedir(stream) } != 0 {
607        return Err(std::io::Error::last_os_error());
608    }
609    names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
610    Ok(names)
611}
612
613#[cfg(not(unix))]
614pub(crate) fn regular_file_names_beneath(
615    _root: &File,
616    _directory: &Path,
617) -> std::io::Result<Vec<std::ffi::OsString>> {
618    Err(secure_filesystem_unsupported())
619}
620
621#[cfg(not(unix))]
622pub(crate) fn walk_regular_files_beneath(
623    _root: &File,
624    _start: &Path,
625) -> std::io::Result<Vec<PathBuf>> {
626    Err(secure_filesystem_unsupported())
627}
628
629#[cfg(not(unix))]
630pub(crate) struct BoundedDirReader;
631
632#[cfg(not(unix))]
633impl BoundedDirReader {
634    pub(crate) fn new(_root: &Path) -> std::io::Result<Self> {
635        Err(secure_filesystem_unsupported())
636    }
637
638    pub(crate) fn read(&mut self, _relative: &Path, _max_bytes: u64) -> std::io::Result<Vec<u8>> {
639        Err(secure_filesystem_unsupported())
640    }
641
642    pub(crate) fn open(&mut self, _relative: &Path) -> std::io::Result<File> {
643        Err(secure_filesystem_unsupported())
644    }
645}
646
647/// Open one regular file through held no-follow parent descriptors. The caller
648/// may safely perform metadata checks and reads on the returned inode without a
649/// pathname reopen in between.
650pub fn open_regular_nofollow(path: &Path) -> std::io::Result<File> {
651    #[cfg(unix)]
652    {
653        use std::os::fd::{AsRawFd as _, FromRawFd as _};
654        let (directory, leaf) = open_parent_unix(path, false)?;
655        let fd = unsafe {
656            libc::openat(
657                directory.as_raw_fd(),
658                leaf.as_ptr(),
659                libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
660            )
661        };
662        if fd < 0 {
663            return Err(std::io::Error::last_os_error());
664        }
665        let file = unsafe { File::from_raw_fd(fd) };
666        let metadata = file.metadata()?;
667        if !metadata.is_file() {
668            return Err(std::io::Error::new(
669                std::io::ErrorKind::InvalidData,
670                "path is not a regular file",
671            ));
672        }
673        Ok(file)
674    }
675    #[cfg(not(unix))]
676    {
677        let _ = path;
678        Err(secure_filesystem_unsupported())
679    }
680}
681
682/// Rename a single entry without re-resolving either parent through mutable
683/// pathnames. Existing symlink ancestors are refused.
684#[cfg(unix)]
685pub fn rename_nofollow(old: &Path, new: &Path) -> std::io::Result<()> {
686    use std::os::fd::AsRawFd as _;
687    let (old_parent, old_leaf) = open_parent_unix(old, false)?;
688    let (new_parent, new_leaf) = open_parent_unix(new, true)?;
689    let mut source_stat: libc::stat = unsafe { std::mem::zeroed() };
690    if unsafe {
691        libc::fstatat(
692            old_parent.as_raw_fd(),
693            old_leaf.as_ptr(),
694            &mut source_stat,
695            libc::AT_SYMLINK_NOFOLLOW,
696        )
697    } != 0
698    {
699        return Err(std::io::Error::last_os_error());
700    }
701    if (source_stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
702        return Err(std::io::Error::new(
703            std::io::ErrorKind::PermissionDenied,
704            "refusing to rename a non-regular file",
705        ));
706    }
707    renameat_noreplace(
708        old_parent.as_raw_fd(),
709        &old_leaf,
710        new_parent.as_raw_fd(),
711        &new_leaf,
712    )?;
713    old_parent.sync_all()?;
714    new_parent.sync_all()?;
715    Ok(())
716}
717
718#[cfg(not(unix))]
719pub fn rename_nofollow(_old: &Path, _new: &Path) -> std::io::Result<()> {
720    Err(secure_filesystem_unsupported())
721}
722
723#[cfg(any(target_os = "linux", target_os = "android"))]
724fn renameat_noreplace(
725    old_dir: std::os::fd::RawFd,
726    old: &std::ffi::CStr,
727    new_dir: std::os::fd::RawFd,
728    new: &std::ffi::CStr,
729) -> std::io::Result<()> {
730    let result = unsafe {
731        libc::syscall(
732            libc::SYS_renameat2,
733            old_dir,
734            old.as_ptr(),
735            new_dir,
736            new.as_ptr(),
737            libc::RENAME_NOREPLACE,
738        )
739    };
740    if result != 0 {
741        return Err(std::io::Error::last_os_error());
742    }
743    Ok(())
744}
745
746#[cfg(target_os = "macos")]
747fn renameat_noreplace(
748    old_dir: std::os::fd::RawFd,
749    old: &std::ffi::CStr,
750    new_dir: std::os::fd::RawFd,
751    new: &std::ffi::CStr,
752) -> std::io::Result<()> {
753    if unsafe {
754        libc::renameatx_np(
755            old_dir,
756            old.as_ptr(),
757            new_dir,
758            new.as_ptr(),
759            libc::RENAME_EXCL,
760        )
761    } != 0
762    {
763        return Err(std::io::Error::last_os_error());
764    }
765    Ok(())
766}
767
768#[cfg(all(
769    unix,
770    not(any(target_os = "linux", target_os = "android", target_os = "macos"))
771))]
772fn renameat_noreplace(
773    _old_dir: std::os::fd::RawFd,
774    _old: &std::ffi::CStr,
775    _new_dir: std::os::fd::RawFd,
776    _new: &std::ffi::CStr,
777) -> std::io::Result<()> {
778    Err(std::io::Error::new(
779        std::io::ErrorKind::Unsupported,
780        "atomic no-replace rename is unsupported on this Unix platform",
781    ))
782}
783
784#[cfg(unix)]
785fn c_name(value: &std::ffi::OsStr) -> std::io::Result<std::ffi::CString> {
786    use std::os::unix::ffi::OsStrExt as _;
787    std::ffi::CString::new(value.as_bytes())
788        .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "NUL in path"))
789}
790
791#[cfg(unix)]
792fn open_parent_unix(path: &Path, create: bool) -> std::io::Result<(File, std::ffi::CString)> {
793    use std::os::fd::{AsRawFd as _, FromRawFd as _};
794    use std::path::Component;
795
796    #[cfg(target_os = "macos")]
797    let path = [("/var", "/private/var"), ("/tmp", "/private/tmp")]
798        .into_iter()
799        .find_map(|(alias, real)| {
800            path.strip_prefix(alias)
801                .ok()
802                .map(|rest| Path::new(real).join(rest))
803        })
804        .unwrap_or_else(|| path.to_path_buf());
805    #[cfg(not(target_os = "macos"))]
806    let path = path.to_path_buf();
807
808    let leaf = path.file_name().ok_or_else(|| {
809        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
810    })?;
811    let parent = path.parent().unwrap_or_else(|| Path::new("."));
812    let mut directory = if path.is_absolute() {
813        File::open("/")?
814    } else {
815        File::open(".")?
816    };
817    for component in parent.components() {
818        let name = match component {
819            Component::RootDir | Component::CurDir => continue,
820            Component::ParentDir => std::ffi::OsStr::new(".."),
821            Component::Normal(name) => name,
822            Component::Prefix(_) => {
823                return Err(std::io::Error::new(
824                    std::io::ErrorKind::Unsupported,
825                    "Windows path prefix is unsupported",
826                ))
827            }
828        };
829        let name = c_name(name)?;
830        if create {
831            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
832            if made != 0 {
833                let error = std::io::Error::last_os_error();
834                if error.raw_os_error() != Some(libc::EEXIST) {
835                    return Err(error);
836                }
837            }
838        }
839        let fd = unsafe {
840            libc::openat(
841                directory.as_raw_fd(),
842                name.as_ptr(),
843                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
844            )
845        };
846        if fd < 0 {
847            return Err(std::io::Error::last_os_error());
848        }
849        directory = unsafe { File::from_raw_fd(fd) };
850    }
851    Ok((directory, c_name(leaf)?))
852}
853
854#[cfg(unix)]
855fn open_parent_beneath(
856    root: &File,
857    relative: &Path,
858    create: bool,
859) -> std::io::Result<(File, std::ffi::CString)> {
860    use std::os::fd::{AsRawFd as _, FromRawFd as _};
861
862    if relative.is_absolute()
863        || relative.components().any(|component| {
864            matches!(
865                component,
866                Component::ParentDir | Component::Prefix(_) | Component::RootDir
867            )
868        })
869    {
870        return Err(std::io::Error::new(
871            std::io::ErrorKind::PermissionDenied,
872            "store capability requires a contained relative path",
873        ));
874    }
875    let leaf = relative.file_name().ok_or_else(|| {
876        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
877    })?;
878    let parent = relative.parent().unwrap_or_else(|| Path::new(""));
879    if !parent.as_os_str().is_empty() && leaf == std::ffi::OsStr::new("DB.md") {
880        return Err(std::io::Error::new(
881            std::io::ErrorKind::PermissionDenied,
882            "refusing to create a nested store marker",
883        ));
884    }
885
886    let mut directory = root.try_clone()?;
887    for component in parent.components() {
888        let Component::Normal(name) = component else {
889            continue;
890        };
891        let name = c_name(name)?;
892        if create {
893            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
894            if made != 0 {
895                let error = std::io::Error::last_os_error();
896                if error.raw_os_error() != Some(libc::EEXIST) {
897                    return Err(error);
898                }
899            }
900        }
901        let fd = unsafe {
902            libc::openat(
903                directory.as_raw_fd(),
904                name.as_ptr(),
905                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
906            )
907        };
908        if fd < 0 {
909            return Err(std::io::Error::last_os_error());
910        }
911        directory = unsafe { File::from_raw_fd(fd) };
912        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
913            return Err(std::io::Error::new(
914                std::io::ErrorKind::PermissionDenied,
915                "path crosses a nested db.md store boundary",
916            ));
917        }
918    }
919    Ok((directory, c_name(leaf)?))
920}
921
922#[cfg(unix)]
923pub(crate) fn write_atomic_beneath(
924    root: &File,
925    relative: &Path,
926    bytes: &[u8],
927    create_new: bool,
928    durable: bool,
929) -> std::io::Result<()> {
930    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
931    write_atomic_at(directory, leaf, bytes, create_new, durable)
932}
933
934/// Atomically replace a rebuildable file beneath a held root without forcing
935/// the bytes or directory entry to stable storage.
936#[cfg(unix)]
937pub(crate) fn write_atomic_nondurable_beneath(
938    root: &File,
939    relative: &Path,
940    bytes: &[u8],
941) -> std::io::Result<()> {
942    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
943    write_atomic_at(directory, leaf, bytes, false, false)
944}
945
946#[cfg(not(unix))]
947pub(crate) fn write_atomic_nondurable_beneath(
948    _root: &File,
949    _relative: &Path,
950    _bytes: &[u8],
951) -> std::io::Result<()> {
952    Err(secure_filesystem_unsupported())
953}
954
955/// Open or create a regular advisory-lock file beneath a held root, then take
956/// an exclusive `flock` on the exact inode. The parent traversal and leaf open
957/// are no-follow, so replacing the store's original pathname cannot redirect
958/// the lock into another tree.
959#[cfg(unix)]
960pub(crate) fn lock_exclusive_beneath(root: &File, relative: &Path) -> std::io::Result<File> {
961    use std::os::fd::{AsRawFd as _, FromRawFd as _};
962
963    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
964    // Darwin can transiently report ENOENT when two creators race on the same
965    // O_CREAT+O_NOFOLLOW leaf. Retry that lookup race (and EINTR) against the
966    // same held parent; every successful contender still opens the one shared
967    // inode and serializes on `flock`.
968    let mut retries = 0_u8;
969    let fd = loop {
970        let fd = unsafe {
971            libc::openat(
972                directory.as_raw_fd(),
973                leaf.as_ptr(),
974                libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
975                0o600,
976            )
977        };
978        if fd >= 0 {
979            break fd;
980        }
981        let error = std::io::Error::last_os_error();
982        if error.kind() == std::io::ErrorKind::Interrupted
983            || (error.kind() == std::io::ErrorKind::NotFound && retries < 8)
984        {
985            retries = retries.saturating_add(1);
986            std::thread::yield_now();
987            continue;
988        }
989        return Err(error);
990    };
991    let file = unsafe { File::from_raw_fd(fd) };
992    if !file.metadata()?.is_file() {
993        return Err(std::io::Error::new(
994            std::io::ErrorKind::PermissionDenied,
995            "refusing to lock a non-regular file",
996        ));
997    }
998    loop {
999        if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } == 0 {
1000            break;
1001        }
1002        let error = std::io::Error::last_os_error();
1003        if error.kind() != std::io::ErrorKind::Interrupted {
1004            return Err(error);
1005        }
1006    }
1007    Ok(file)
1008}
1009
1010#[cfg(not(unix))]
1011pub(crate) fn lock_exclusive_beneath(_root: &File, _relative: &Path) -> std::io::Result<File> {
1012    Err(secure_filesystem_unsupported())
1013}
1014
1015/// Open a directory beneath a held root without following any component.
1016#[cfg(unix)]
1017pub(crate) fn open_directory_beneath(
1018    root: &File,
1019    relative: &Path,
1020    create: bool,
1021) -> std::io::Result<File> {
1022    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1023
1024    if relative.is_absolute()
1025        || relative.components().any(|component| {
1026            matches!(
1027                component,
1028                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1029            )
1030        })
1031    {
1032        return Err(std::io::Error::new(
1033            std::io::ErrorKind::PermissionDenied,
1034            "store capability requires a contained relative directory",
1035        ));
1036    }
1037    let mut directory = root.try_clone()?;
1038    for component in relative.components() {
1039        let Component::Normal(name) = component else {
1040            continue;
1041        };
1042        let name = c_name(name)?;
1043        if create {
1044            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
1045            if made != 0 {
1046                let error = std::io::Error::last_os_error();
1047                if error.raw_os_error() != Some(libc::EEXIST) {
1048                    return Err(error);
1049                }
1050            }
1051        }
1052        let fd = unsafe {
1053            libc::openat(
1054                directory.as_raw_fd(),
1055                name.as_ptr(),
1056                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1057            )
1058        };
1059        if fd < 0 {
1060            return Err(std::io::Error::last_os_error());
1061        }
1062        directory = unsafe { File::from_raw_fd(fd) };
1063        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
1064            return Err(std::io::Error::new(
1065                std::io::ErrorKind::PermissionDenied,
1066                "path crosses a nested db.md store boundary",
1067            ));
1068        }
1069    }
1070    Ok(directory)
1071}
1072
1073#[cfg(not(unix))]
1074pub(crate) fn open_directory_beneath(
1075    _root: &File,
1076    _relative: &Path,
1077    _create: bool,
1078) -> std::io::Result<File> {
1079    Err(secure_filesystem_unsupported())
1080}
1081
1082pub(crate) fn directory_exists_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
1083    match open_directory_beneath(root, relative, false) {
1084        Ok(_) => Ok(true),
1085        Err(error)
1086            if matches!(
1087                error.kind(),
1088                std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1089            ) =>
1090        {
1091            Ok(false)
1092        }
1093        Err(error) => Err(error),
1094    }
1095}
1096
1097/// Confirm that every component of a relative regular-file path has the exact
1098/// byte spelling present on disk. This keeps validation platform-independent
1099/// on case-insensitive filesystems without canonicalizing the mutable root
1100/// pathname.
1101#[cfg(unix)]
1102pub(crate) fn path_case_matches_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
1103    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1104    use std::os::unix::ffi::OsStrExt as _;
1105
1106    if relative.is_absolute()
1107        || relative.components().any(|component| {
1108            matches!(
1109                component,
1110                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1111            )
1112        })
1113    {
1114        return Ok(false);
1115    }
1116    let components: Vec<_> = relative
1117        .components()
1118        .filter_map(|component| match component {
1119            Component::Normal(name) => Some(name),
1120            _ => None,
1121        })
1122        .collect();
1123    let mut directory = root.try_clone()?;
1124    for (index, name) in components.iter().enumerate() {
1125        let scan_fd = unsafe {
1126            libc::openat(
1127                directory.as_raw_fd(),
1128                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
1129                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1130            )
1131        };
1132        if scan_fd < 0 {
1133            return Err(std::io::Error::last_os_error());
1134        }
1135        let stream = unsafe { libc::fdopendir(scan_fd) };
1136        if stream.is_null() {
1137            let error = std::io::Error::last_os_error();
1138            unsafe {
1139                libc::close(scan_fd);
1140            }
1141            return Err(error);
1142        }
1143        let mut exact = false;
1144        loop {
1145            let entry = unsafe { libc::readdir(stream) };
1146            if entry.is_null() {
1147                break;
1148            }
1149            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1150            if bytes == name.as_bytes() {
1151                exact = true;
1152                break;
1153            }
1154        }
1155        if unsafe { libc::closedir(stream) } != 0 {
1156            return Err(std::io::Error::last_os_error());
1157        }
1158        if !exact {
1159            return Ok(false);
1160        }
1161        if index + 1 == components.len() {
1162            return directory_contains_exact_regular(&directory, name);
1163        }
1164        let fd = unsafe {
1165            libc::openat(
1166                directory.as_raw_fd(),
1167                c_name(name)?.as_ptr(),
1168                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1169            )
1170        };
1171        if fd < 0 {
1172            return Ok(false);
1173        }
1174        directory = unsafe { File::from_raw_fd(fd) };
1175        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
1176            return Ok(false);
1177        }
1178    }
1179    Ok(false)
1180}
1181
1182#[cfg(not(unix))]
1183pub(crate) fn path_case_matches_beneath(_root: &File, _relative: &Path) -> std::io::Result<bool> {
1184    Err(secure_filesystem_unsupported())
1185}
1186
1187/// Immediate no-follow child directories beneath a held root.
1188#[cfg(unix)]
1189pub(crate) fn directory_names_beneath(
1190    root: &File,
1191    relative: &Path,
1192) -> std::io::Result<Vec<std::ffi::OsString>> {
1193    use std::os::fd::AsRawFd as _;
1194    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
1195
1196    let directory = open_directory_beneath(root, relative, false)?;
1197    let scan_fd = unsafe {
1198        libc::openat(
1199            directory.as_raw_fd(),
1200            c_name(std::ffi::OsStr::new("."))?.as_ptr(),
1201            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1202        )
1203    };
1204    if scan_fd < 0 {
1205        return Err(std::io::Error::last_os_error());
1206    }
1207    let stream = unsafe { libc::fdopendir(scan_fd) };
1208    if stream.is_null() {
1209        let error = std::io::Error::last_os_error();
1210        unsafe {
1211            libc::close(scan_fd);
1212        }
1213        return Err(error);
1214    }
1215    let mut names = Vec::new();
1216    loop {
1217        let entry = unsafe { libc::readdir(stream) };
1218        if entry.is_null() {
1219            break;
1220        }
1221        let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1222        if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
1223            continue;
1224        }
1225        let name = std::ffi::OsString::from_vec(bytes.to_vec());
1226        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1227        if unsafe {
1228            libc::fstatat(
1229                directory.as_raw_fd(),
1230                c_name(&name)?.as_ptr(),
1231                &mut stat,
1232                libc::AT_SYMLINK_NOFOLLOW,
1233            )
1234        } == 0
1235            && (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR
1236        {
1237            let child = open_directory_beneath(root, &relative.join(&name), false)?;
1238            if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
1239                names.push(name);
1240            }
1241        }
1242    }
1243    if unsafe { libc::closedir(stream) } != 0 {
1244        return Err(std::io::Error::last_os_error());
1245    }
1246    names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1247    Ok(names)
1248}
1249
1250#[cfg(not(unix))]
1251pub(crate) fn directory_names_beneath(
1252    _root: &File,
1253    _relative: &Path,
1254) -> std::io::Result<Vec<std::ffi::OsString>> {
1255    Err(secure_filesystem_unsupported())
1256}
1257
1258#[cfg(not(unix))]
1259pub(crate) fn write_atomic_beneath(
1260    _root: &File,
1261    _relative: &Path,
1262    _bytes: &[u8],
1263    _create_new: bool,
1264    _durable: bool,
1265) -> std::io::Result<()> {
1266    Err(secure_filesystem_unsupported())
1267}
1268
1269#[cfg(unix)]
1270pub(crate) fn rename_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
1271    use std::os::fd::AsRawFd as _;
1272
1273    let (old_parent, old_leaf) = open_parent_beneath(root, old, false)?;
1274    let (new_parent, new_leaf) = open_parent_beneath(root, new, true)?;
1275    let mut source_stat: libc::stat = unsafe { std::mem::zeroed() };
1276    if unsafe {
1277        libc::fstatat(
1278            old_parent.as_raw_fd(),
1279            old_leaf.as_ptr(),
1280            &mut source_stat,
1281            libc::AT_SYMLINK_NOFOLLOW,
1282        )
1283    } != 0
1284    {
1285        return Err(std::io::Error::last_os_error());
1286    }
1287    if (source_stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
1288        return Err(std::io::Error::new(
1289            std::io::ErrorKind::PermissionDenied,
1290            "refusing to rename a non-regular file",
1291        ));
1292    }
1293    renameat_noreplace(
1294        old_parent.as_raw_fd(),
1295        &old_leaf,
1296        new_parent.as_raw_fd(),
1297        &new_leaf,
1298    )?;
1299    old_parent.sync_all()?;
1300    new_parent.sync_all()?;
1301    Ok(())
1302}
1303
1304#[cfg(not(unix))]
1305pub(crate) fn rename_beneath(_root: &File, _old: &Path, _new: &Path) -> std::io::Result<()> {
1306    Err(secure_filesystem_unsupported())
1307}
1308
1309#[cfg(unix)]
1310pub(crate) fn remove_file_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
1311    use std::os::fd::AsRawFd as _;
1312
1313    let (parent, leaf) = open_parent_beneath(root, relative, false)?;
1314    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1315    if unsafe {
1316        libc::fstatat(
1317            parent.as_raw_fd(),
1318            leaf.as_ptr(),
1319            &mut stat,
1320            libc::AT_SYMLINK_NOFOLLOW,
1321        )
1322    } != 0
1323    {
1324        return Err(std::io::Error::last_os_error());
1325    }
1326    if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
1327        return Err(std::io::Error::new(
1328            std::io::ErrorKind::PermissionDenied,
1329            "refusing to remove a non-regular file",
1330        ));
1331    }
1332    if unsafe { libc::unlinkat(parent.as_raw_fd(), leaf.as_ptr(), 0) } != 0 {
1333        return Err(std::io::Error::last_os_error());
1334    }
1335    parent.sync_all()
1336}
1337
1338#[cfg(not(unix))]
1339pub(crate) fn remove_file_beneath(_root: &File, _relative: &Path) -> std::io::Result<()> {
1340    Err(secure_filesystem_unsupported())
1341}
1342
1343#[cfg(unix)]
1344fn write_atomic_unix(
1345    path: &Path,
1346    bytes: &[u8],
1347    create_new: bool,
1348    durable: bool,
1349) -> std::io::Result<()> {
1350    let (directory, leaf) = open_parent_unix(path, true)?;
1351    write_atomic_at(directory, leaf, bytes, create_new, durable)
1352}
1353
1354#[cfg(unix)]
1355fn write_atomic_at(
1356    directory: File,
1357    leaf: std::ffi::CString,
1358    bytes: &[u8],
1359    create_new: bool,
1360    durable: bool,
1361) -> std::io::Result<()> {
1362    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1363
1364    static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
1365    let pid = std::process::id();
1366    let nanos = SystemTime::now()
1367        .duration_since(UNIX_EPOCH)
1368        .map(|duration| duration.as_nanos())
1369        .unwrap_or(0);
1370    let mut allocated = None;
1371    for _ in 0..128 {
1372        let name = std::ffi::OsString::from(format!(
1373            ".dbmd.tmp.{pid}.{nanos}.{}",
1374            TMP_SEQ.fetch_add(1, Ordering::Relaxed)
1375        ));
1376        let name = c_name(&name)?;
1377        let fd = unsafe {
1378            libc::openat(
1379                directory.as_raw_fd(),
1380                name.as_ptr(),
1381                libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1382                0o666,
1383            )
1384        };
1385        if fd >= 0 {
1386            allocated = Some((name, unsafe { File::from_raw_fd(fd) }));
1387            break;
1388        }
1389        let error = std::io::Error::last_os_error();
1390        if error.kind() != std::io::ErrorKind::AlreadyExists {
1391            return Err(error);
1392        }
1393    }
1394    let (temp, mut file) = allocated.ok_or_else(|| {
1395        std::io::Error::new(
1396            std::io::ErrorKind::AlreadyExists,
1397            "could not allocate secure temporary file",
1398        )
1399    })?;
1400    let cleanup =
1401        |name: &std::ffi::CStr| unsafe { libc::unlinkat(directory.as_raw_fd(), name.as_ptr(), 0) };
1402    let write_result = file.write_all(bytes).and_then(|_| {
1403        if durable {
1404            file.sync_all()
1405        } else {
1406            file.flush()
1407        }
1408    });
1409    if let Err(error) = write_result {
1410        let _ = cleanup(&temp);
1411        return Err(error);
1412    }
1413
1414    // Preserve an existing regular destination's exact Unix mode. A symlink is
1415    // never dereferenced; chmod failure aborts rather than silently widening.
1416    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1417    let destination_stat = unsafe {
1418        libc::fstatat(
1419            directory.as_raw_fd(),
1420            leaf.as_ptr(),
1421            &mut stat,
1422            libc::AT_SYMLINK_NOFOLLOW,
1423        )
1424    };
1425    if destination_stat == 0 {
1426        if (stat.st_mode & libc::S_IFMT) == libc::S_IFLNK {
1427            let _ = cleanup(&temp);
1428            return Err(std::io::Error::new(
1429                std::io::ErrorKind::PermissionDenied,
1430                "refusing a symlink destination",
1431            ));
1432        }
1433        if unsafe { libc::fchmod(file.as_raw_fd(), stat.st_mode & 0o7777) } != 0 {
1434            let error = std::io::Error::last_os_error();
1435            let _ = cleanup(&temp);
1436            return Err(error);
1437        }
1438    } else {
1439        let error = std::io::Error::last_os_error();
1440        if error.kind() != std::io::ErrorKind::NotFound {
1441            let _ = cleanup(&temp);
1442            return Err(error);
1443        }
1444    }
1445    drop(file);
1446
1447    let installed = if create_new {
1448        unsafe {
1449            libc::linkat(
1450                directory.as_raw_fd(),
1451                temp.as_ptr(),
1452                directory.as_raw_fd(),
1453                leaf.as_ptr(),
1454                0,
1455            )
1456        }
1457    } else {
1458        unsafe {
1459            libc::renameat(
1460                directory.as_raw_fd(),
1461                temp.as_ptr(),
1462                directory.as_raw_fd(),
1463                leaf.as_ptr(),
1464            )
1465        }
1466    };
1467    if installed != 0 {
1468        let error = std::io::Error::last_os_error();
1469        let _ = cleanup(&temp);
1470        return Err(error);
1471    }
1472    if create_new {
1473        let _ = cleanup(&temp);
1474    }
1475    if durable {
1476        directory.sync_all()?;
1477    }
1478    Ok(())
1479}
1480
1481/// Drop-based cleanup for the hidden temp file `write_atomic` creates. While
1482/// armed, dropping the guard removes `path`. [`TempGuard::disarm`] is called
1483/// only after a successful rename, or after a successful temp-link cleanup in
1484/// [`write_atomic_new`], so the final destination is never touched.
1485#[cfg(test)]
1486struct TempGuard {
1487    path: PathBuf,
1488    armed: bool,
1489}
1490
1491#[cfg(test)]
1492impl TempGuard {
1493    /// Stop cleaning up `path` on drop — used once the temp has been renamed
1494    /// into place and is no longer a stray temp file.
1495    fn disarm(&mut self) {
1496        self.armed = false;
1497    }
1498}
1499
1500#[cfg(test)]
1501impl Drop for TempGuard {
1502    fn drop(&mut self) {
1503        // Best-effort cleanup if an error path bailed out before the rename.
1504        if self.armed {
1505            let _ = fs::remove_file(&self.path);
1506        }
1507    }
1508}
1509
1510/// Create a uniquely-named temp file in `dir` with `create_new` (never clobbers
1511/// a predictable name), retrying on the vanishingly-rare collision. The name is
1512/// hidden (`.`-prefixed) and tagged with pid + nanos + a process-wide counter so
1513/// concurrent writers in the same directory never pick the same path. Returns the
1514/// open handle plus an armed [`TempGuard`] so any early return cleans up the temp.
1515#[cfg(test)]
1516fn create_temp_file(dir: &Path, file_name: &str) -> std::io::Result<(File, TempGuard)> {
1517    static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
1518    let pid = std::process::id();
1519    let nanos = SystemTime::now()
1520        .duration_since(UNIX_EPOCH)
1521        .map(|d| d.as_nanos())
1522        .unwrap_or(0);
1523
1524    for _ in 0..128 {
1525        let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);
1526        let tmp = dir.join(format!(".{file_name}.tmp.{pid}.{nanos}.{seq}"));
1527        match OpenOptions::new().write(true).create_new(true).open(&tmp) {
1528            Ok(file) => {
1529                return Ok((
1530                    file,
1531                    TempGuard {
1532                        path: tmp,
1533                        armed: true,
1534                    },
1535                ))
1536            }
1537            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
1538            Err(e) => return Err(e),
1539        }
1540    }
1541
1542    Err(std::io::Error::new(
1543        std::io::ErrorKind::AlreadyExists,
1544        "could not allocate a unique dbmd temp file",
1545    ))
1546}
1547
1548#[cfg(test)]
1549mod tests {
1550    use super::*;
1551    use tempfile::TempDir;
1552
1553    #[test]
1554    fn write_atomic_creates_then_replaces_durably() {
1555        let tmp = TempDir::new().unwrap();
1556        let target = tmp.path().join("sub").join("file.txt"); // parent missing
1557
1558        write_atomic(&target, b"first").unwrap();
1559        assert_eq!(std::fs::read(&target).unwrap(), b"first");
1560
1561        // Replace in place — content swaps, no temp files left behind.
1562        write_atomic(&target, b"second").unwrap();
1563        assert_eq!(std::fs::read(&target).unwrap(), b"second");
1564
1565        let leftovers: Vec<_> = std::fs::read_dir(target.parent().unwrap())
1566            .unwrap()
1567            .filter_map(|e| e.ok())
1568            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
1569            .collect();
1570        assert!(leftovers.is_empty(), "no temp files may be left behind");
1571    }
1572
1573    #[test]
1574    fn write_atomic_is_byte_exact_including_empty() {
1575        let tmp = TempDir::new().unwrap();
1576        let target = tmp.path().join("empty.txt");
1577        write_atomic(&target, b"").unwrap();
1578        assert_eq!(std::fs::read(&target).unwrap(), b"");
1579    }
1580
1581    #[test]
1582    fn write_atomic_new_creates_but_refuses_existing() {
1583        let tmp = TempDir::new().unwrap();
1584        let target = tmp.path().join("sub").join("file.txt");
1585
1586        write_atomic_new(&target, b"first").unwrap();
1587        assert_eq!(std::fs::read(&target).unwrap(), b"first");
1588
1589        let err = write_atomic_new(&target, b"second").unwrap_err();
1590        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
1591        assert_eq!(
1592            std::fs::read(&target).unwrap(),
1593            b"first",
1594            "create-new failure must leave the existing destination untouched"
1595        );
1596
1597        assert_no_temp_files(target.parent().unwrap());
1598    }
1599
1600    #[test]
1601    fn write_atomic_new_allows_only_one_concurrent_creator() {
1602        use std::sync::{Arc, Barrier};
1603
1604        for round in 0..40 {
1605            let tmp = TempDir::new().unwrap();
1606            let target = tmp.path().join("file.txt");
1607            let barrier = Arc::new(Barrier::new(8));
1608
1609            let handles: Vec<_> = (0..8)
1610                .map(|i| {
1611                    let target = target.clone();
1612                    let barrier = Arc::clone(&barrier);
1613                    std::thread::spawn(move || {
1614                        let payload = format!("payload-{i}");
1615                        barrier.wait();
1616                        let result = write_atomic_new(&target, payload.as_bytes())
1617                            .map(|_| ())
1618                            .map_err(|e| e.kind());
1619                        (payload, result)
1620                    })
1621                })
1622                .collect();
1623
1624            let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
1625            let winners: Vec<_> = results
1626                .iter()
1627                .filter_map(|(payload, result)| result.is_ok().then_some(payload))
1628                .collect();
1629            let already_exists = results
1630                .iter()
1631                .filter(|(_, result)| {
1632                    matches!(result, Err(kind) if *kind == std::io::ErrorKind::AlreadyExists)
1633                })
1634                .count();
1635
1636            assert_eq!(
1637                winners.len(),
1638                1,
1639                "round {round}: exactly one creator may win, got {results:?}"
1640            );
1641            assert_eq!(
1642                already_exists, 7,
1643                "round {round}: every losing creator must get AlreadyExists, got {results:?}"
1644            );
1645
1646            let written = std::fs::read_to_string(&target).unwrap();
1647            assert_eq!(
1648                written, *winners[0],
1649                "round {round}: destination must contain the winner's payload"
1650            );
1651            assert_no_temp_files(tmp.path());
1652        }
1653    }
1654
1655    /// Regression for finding #22: an early return between temp-file creation and
1656    /// a successful rename (e.g. `write_all`/`sync_all` failing under ENOSPC/EIO)
1657    /// must NOT leave the hidden temp file orphaned in the data directory.
1658    ///
1659    /// Pre-fix, `create_temp_file` handed back a bare `PathBuf` with no `Drop`
1660    /// cleanup, so dropping it without a rename — exactly what `?` does on a
1661    /// write/sync failure — left the temp on disk. This reconstructs that path by
1662    /// dropping the guard without renaming and asserting the temp is gone.
1663    #[test]
1664    fn regression_armed_guard_removes_temp_on_early_drop() {
1665        let dir = TempDir::new().unwrap();
1666        let (file, guard) = create_temp_file(dir.path(), "file.txt").unwrap();
1667        let tmp_path = guard.path.clone();
1668        assert!(
1669            tmp_path.exists(),
1670            "temp file should exist after create_temp_file"
1671        );
1672
1673        // Simulate a write/sync failure bailing out before the rename: the file
1674        // handle and the (still-armed) guard go out of scope without a rename.
1675        drop(file);
1676        drop(guard);
1677
1678        assert!(
1679            !tmp_path.exists(),
1680            "armed guard must remove the orphaned temp file on early drop"
1681        );
1682        // No stray `.tmp.` files left in the directory.
1683        let leftovers: Vec<_> = std::fs::read_dir(dir.path())
1684            .unwrap()
1685            .filter_map(|e| e.ok())
1686            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
1687            .collect();
1688        assert!(leftovers.is_empty(), "no temp files may be left behind");
1689    }
1690
1691    /// Once disarmed (after a successful rename) the guard must NOT delete the
1692    /// path it was tracking — otherwise it would clobber the renamed destination.
1693    #[test]
1694    fn regression_disarmed_guard_leaves_file_intact() {
1695        let dir = TempDir::new().unwrap();
1696        let (file, mut guard) = create_temp_file(dir.path(), "kept.txt").unwrap();
1697        drop(file);
1698        let kept = guard.path.clone();
1699
1700        guard.disarm();
1701        drop(guard);
1702
1703        assert!(
1704            kept.exists(),
1705            "disarmed guard must leave the renamed destination untouched"
1706        );
1707    }
1708
1709    fn assert_no_temp_files(dir: &Path) {
1710        let leftovers: Vec<_> = std::fs::read_dir(dir)
1711            .unwrap()
1712            .filter_map(|e| e.ok())
1713            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
1714            .collect();
1715        assert!(leftovers.is_empty(), "no temp files may be left behind");
1716    }
1717
1718    /// Regression: rewriting an existing file via `write_atomic` must PRESERVE
1719    /// its permission bits. Pre-fix the temp file's default mode (0644) replaced
1720    /// a deliberately-restricted destination (0600) on every rewrite — a quiet
1721    /// permission-widening on user data. A first create still uses the default
1722    /// mode (there is no destination mode to copy).
1723    #[cfg(unix)]
1724    #[test]
1725    fn write_atomic_preserves_existing_destination_permissions() {
1726        use std::os::unix::fs::PermissionsExt;
1727
1728        let tmp = TempDir::new().unwrap();
1729        let target = tmp.path().join("private.md");
1730
1731        // Create, then restrict to 0600.
1732        write_atomic(&target, b"secret v1").unwrap();
1733        std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
1734        let before = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
1735        assert_eq!(before, 0o600, "fixture must start at 0600");
1736
1737        // Rewrite in place: the 0600 mode must survive (not reset to 0644).
1738        write_atomic(&target, b"secret v2").unwrap();
1739        let after = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
1740        assert_eq!(
1741            after, 0o600,
1742            "write_atomic must preserve the destination's 0600 mode, got {after:o}"
1743        );
1744        assert_eq!(std::fs::read(&target).unwrap(), b"secret v2");
1745    }
1746
1747    /// Exploit regression for the containment/write TOCTOU: a caller may have
1748    /// validated `store/records/safe.md`, then an attacker replaces `records`
1749    /// with a symlink to an external directory before the atomic writer opens
1750    /// it. Every ancestor is opened with `openat(O_DIRECTORY|O_NOFOLLOW)`, so
1751    /// the write is refused and the outside victim is byte-identical.
1752    #[cfg(unix)]
1753    #[test]
1754    fn write_atomic_refuses_symlinked_ancestor_without_touching_external_file() {
1755        use std::os::unix::fs::symlink;
1756
1757        let sandbox = TempDir::new().unwrap();
1758        let store = sandbox.path().join("store");
1759        let external = sandbox.path().join("external");
1760        std::fs::create_dir_all(store.join("records")).unwrap();
1761        std::fs::create_dir_all(&external).unwrap();
1762        let victim = external.join("safe.md");
1763        std::fs::write(&victim, b"external secret").unwrap();
1764
1765        std::fs::remove_dir(store.join("records")).unwrap();
1766        symlink(&external, store.join("records")).unwrap();
1767
1768        let error = write_atomic(&store.join("records/safe.md"), b"attacker output")
1769            .expect_err("a symlinked ancestor must fail closed");
1770        assert!(
1771            matches!(
1772                error.raw_os_error(),
1773                Some(code) if code == libc::ELOOP || code == libc::ENOTDIR
1774            ),
1775            "expected no-follow refusal, got {error:?}"
1776        );
1777        assert_eq!(std::fs::read(&victim).unwrap(), b"external secret");
1778    }
1779
1780    /// A leaf swap is equally unsafe for reads: after containment validation an
1781    /// attacker can replace the selected record with a symlink to a secret.
1782    /// `read_bounded_nofollow` opens the leaf once with `O_NOFOLLOW`, then sizes
1783    /// and reads that same descriptor, so no external bytes are returned.
1784    #[cfg(unix)]
1785    #[test]
1786    fn bounded_read_refuses_symlink_leaf() {
1787        use std::os::unix::fs::symlink;
1788
1789        let sandbox = TempDir::new().unwrap();
1790        let external = sandbox.path().join("secret");
1791        std::fs::write(&external, b"do not exfiltrate").unwrap();
1792        let selected = sandbox.path().join("selected.md");
1793        symlink(&external, &selected).unwrap();
1794
1795        let error = read_bounded_nofollow(&selected, 1024)
1796            .expect_err("the no-follow reader must reject a symlink leaf");
1797        assert_eq!(error.raw_os_error(), Some(libc::ELOOP));
1798    }
1799
1800    /// If the file grows after its descriptor metadata was read, the bounded
1801    /// descriptor read still enforces the actual byte ceiling (`take(max+1)`),
1802    /// rather than trusting the stale size.
1803    #[test]
1804    fn bounded_read_rejects_content_over_limit() {
1805        let sandbox = TempDir::new().unwrap();
1806        let selected = sandbox.path().join("selected.md");
1807        std::fs::write(&selected, b"12345").unwrap();
1808        let error = read_bounded_nofollow(&selected, 4)
1809            .expect_err("actual content above the cap must be refused");
1810        assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
1811    }
1812
1813    #[cfg(unix)]
1814    #[test]
1815    fn held_directory_reader_survives_ancestor_swap_without_disclosure() {
1816        use std::os::unix::fs::symlink;
1817
1818        let sandbox = TempDir::new().unwrap();
1819        let store = sandbox.path().join("store");
1820        let contacts = store.join("records/contacts");
1821        std::fs::create_dir_all(&contacts).unwrap();
1822        std::fs::write(contacts.join("selected.md"), b"owned").unwrap();
1823        let outside = sandbox.path().join("outside");
1824        std::fs::create_dir_all(&outside).unwrap();
1825        std::fs::write(outside.join("selected.md"), b"secret").unwrap();
1826
1827        let mut reader = BoundedDirReader::new(&store).unwrap();
1828        let relative = Path::new("records/contacts/selected.md");
1829        assert_eq!(reader.read(relative, 1024).unwrap(), b"owned");
1830
1831        let detached = store.join("records/contacts-detached");
1832        std::fs::rename(&contacts, &detached).unwrap();
1833        symlink(&outside, &contacts).unwrap();
1834
1835        assert_eq!(
1836            reader.read(relative, 1024).unwrap(),
1837            b"owned",
1838            "the cached directory capability must not reopen the swapped pathname"
1839        );
1840    }
1841
1842    #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
1843    #[test]
1844    fn rename_nofollow_is_atomic_no_replace() {
1845        let sandbox = TempDir::new().unwrap();
1846        let source = sandbox.path().join("source.md");
1847        let destination = sandbox.path().join("destination.md");
1848        std::fs::write(&source, b"source").unwrap();
1849        std::fs::write(&destination, b"existing").unwrap();
1850
1851        let error = rename_nofollow(&source, &destination)
1852            .expect_err("a destination created after preflight must not be clobbered");
1853        assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
1854        assert_eq!(std::fs::read(&source).unwrap(), b"source");
1855        assert_eq!(std::fs::read(&destination).unwrap(), b"existing");
1856    }
1857}