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(any(unix, windows))]
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(windows)]
52    {
53        windows_fs::atomic_write_absolute(path, bytes, false, true)
54    }
55    #[cfg(not(any(unix, windows)))]
56    {
57        let _ = (path, bytes);
58        Err(secure_filesystem_unsupported())
59    }
60}
61
62/// Atomically and durably create `path` with `bytes`, failing with
63/// [`std::io::ErrorKind::AlreadyExists`] if the destination already exists.
64///
65/// This follows the same temp-file + file-fsync + parent-fsync sequence as
66/// [`write_atomic`], but installs the temp file with `hard_link(temp, path)`
67/// instead of `rename(temp, path)`. Hard-link creation is resolved atomically by
68/// the OS and refuses an existing destination, so concurrent creators for the
69/// same path produce exactly one winner and `AlreadyExists` for the rest. The
70/// temporary link is removed after the destination link is established.
71pub fn write_atomic_new(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
72    #[cfg(unix)]
73    {
74        write_atomic_unix(path, bytes, true, true)
75    }
76    #[cfg(windows)]
77    {
78        windows_fs::atomic_write_absolute(path, bytes, true, true)
79    }
80    #[cfg(not(any(unix, windows)))]
81    {
82        let _ = (path, bytes);
83        Err(secure_filesystem_unsupported())
84    }
85}
86
87#[cfg(not(any(unix, windows)))]
88fn secure_filesystem_unsupported() -> std::io::Error {
89    std::io::Error::new(
90        std::io::ErrorKind::Unsupported,
91        "secure filesystem mutation requires handle-relative no-follow primitives on this platform",
92    )
93}
94
95#[cfg(windows)]
96mod windows_fs {
97    use super::*;
98    use std::ffi::{OsStr, OsString};
99    use std::os::windows::ffi::{OsStrExt as _, OsStringExt as _};
100    use std::os::windows::io::{AsRawHandle as _, FromRawHandle as _};
101    use std::path::Component;
102    use windows_sys::Win32::Foundation::{
103        CloseHandle, GENERIC_READ, GENERIC_WRITE, HANDLE, INVALID_HANDLE_VALUE,
104    };
105    use windows_sys::Win32::Storage::FileSystem::{
106        CreateDirectoryW, CreateFileW, GetFileInformationByHandle, GetFinalPathNameByHandleW,
107        LockFileEx, MoveFileExW, RemoveDirectoryW, SetFileAttributesW, BY_HANDLE_FILE_INFORMATION,
108        CREATE_NEW, FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_NORMAL, FILE_ATTRIBUTE_READONLY,
109        FILE_ATTRIBUTE_REPARSE_POINT, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
110        FILE_NAME_NORMALIZED, FILE_READ_ATTRIBUTES, FILE_SHARE_READ, FILE_SHARE_WRITE,
111        FILE_WRITE_ATTRIBUTES, LOCKFILE_EXCLUSIVE_LOCK, MOVEFILE_REPLACE_EXISTING,
112        MOVEFILE_WRITE_THROUGH, OPEN_ALWAYS, OPEN_EXISTING, VOLUME_NAME_DOS,
113    };
114    use windows_sys::Win32::System::IO::OVERLAPPED;
115
116    fn wide(path: &Path) -> Vec<u16> {
117        path.as_os_str()
118            .encode_wide()
119            .chain(std::iter::once(0))
120            .collect()
121    }
122
123    fn open_raw(
124        path: &Path,
125        access: u32,
126        directory: bool,
127        creation: u32,
128    ) -> std::io::Result<HANDLE> {
129        let mut flags = FILE_FLAG_OPEN_REPARSE_POINT;
130        if directory {
131            flags |= FILE_FLAG_BACKUP_SEMANTICS;
132        }
133        let path = wide(path);
134        let handle = unsafe {
135            CreateFileW(
136                path.as_ptr(),
137                access,
138                // Omitting FILE_SHARE_DELETE is the capability boundary: a
139                // checked component cannot be renamed away while held.
140                FILE_SHARE_READ | FILE_SHARE_WRITE,
141                std::ptr::null(),
142                creation,
143                flags,
144                std::ptr::null_mut(),
145            )
146        };
147        if handle == INVALID_HANDLE_VALUE {
148            Err(std::io::Error::last_os_error())
149        } else {
150            Ok(handle)
151        }
152    }
153
154    fn attributes(handle: HANDLE) -> std::io::Result<u32> {
155        let mut info = unsafe { std::mem::zeroed::<BY_HANDLE_FILE_INFORMATION>() };
156        if unsafe { GetFileInformationByHandle(handle, &mut info) } == 0 {
157            Err(std::io::Error::last_os_error())
158        } else {
159            Ok(info.dwFileAttributes)
160        }
161    }
162
163    fn file_from_handle(handle: HANDLE) -> File {
164        unsafe { File::from_raw_handle(handle as _) }
165    }
166
167    fn checked_file(handle: HANDLE, directory: bool) -> std::io::Result<File> {
168        let attrs = match attributes(handle) {
169            Ok(attrs) => attrs,
170            Err(error) => {
171                unsafe { CloseHandle(handle) };
172                return Err(error);
173            }
174        };
175        let wrong_kind = if directory {
176            attrs & FILE_ATTRIBUTE_DIRECTORY == 0
177        } else {
178            attrs & FILE_ATTRIBUTE_DIRECTORY != 0
179        };
180        if attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0 || wrong_kind {
181            unsafe { CloseHandle(handle) };
182            return Err(std::io::Error::new(
183                std::io::ErrorKind::PermissionDenied,
184                "path component is a reparse point or has the wrong type",
185            ));
186        }
187        Ok(file_from_handle(handle))
188    }
189
190    pub(super) fn open_directory(path: &Path) -> std::io::Result<File> {
191        let resolved = std::path::absolute(path)?;
192        let guards = hold_directory_chain(&resolved, false)?;
193        guards.into_iter().last().ok_or_else(|| {
194            std::io::Error::new(
195                std::io::ErrorKind::InvalidInput,
196                "directory path has no root",
197            )
198        })
199    }
200
201    pub(super) fn open_or_create_directory(path: &Path) -> std::io::Result<File> {
202        let resolved = std::path::absolute(path)?;
203        let guards = hold_directory_chain(&resolved, true)?;
204        guards.into_iter().last().ok_or_else(|| {
205            std::io::Error::new(
206                std::io::ErrorKind::InvalidInput,
207                "directory path has no root",
208            )
209        })
210    }
211
212    pub(super) fn directory_path(directory: &File) -> std::io::Result<PathBuf> {
213        let handle = directory.as_raw_handle() as HANDLE;
214        let flags = FILE_NAME_NORMALIZED | VOLUME_NAME_DOS;
215        let needed = unsafe { GetFinalPathNameByHandleW(handle, std::ptr::null_mut(), 0, flags) };
216        if needed == 0 {
217            return Err(std::io::Error::last_os_error());
218        }
219        let mut buffer = vec![0_u16; needed as usize + 1];
220        let written = unsafe {
221            GetFinalPathNameByHandleW(handle, buffer.as_mut_ptr(), buffer.len() as u32, flags)
222        };
223        if written == 0 || written as usize >= buffer.len() {
224            return Err(std::io::Error::last_os_error());
225        }
226        buffer.truncate(written as usize);
227        Ok(PathBuf::from(OsString::from_wide(&buffer)))
228    }
229
230    fn hold_directory_chain(path: &Path, create: bool) -> std::io::Result<Vec<File>> {
231        let resolved = std::path::absolute(path)?;
232        let mut current = PathBuf::new();
233        let mut held = Vec::new();
234        for component in resolved.components() {
235            current.push(component.as_os_str());
236            if !matches!(component, Component::RootDir | Component::Normal(_)) {
237                continue;
238            }
239            let opened = open_raw(&current, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)
240                .and_then(|handle| checked_file(handle, true));
241            match opened {
242                Ok(directory) => held.push(directory),
243                Err(error) if create && error.kind() == std::io::ErrorKind::NotFound => {
244                    let path = wide(&current);
245                    if unsafe { CreateDirectoryW(path.as_ptr(), std::ptr::null()) } == 0 {
246                        let create_error = std::io::Error::last_os_error();
247                        if create_error.kind() != std::io::ErrorKind::AlreadyExists {
248                            return Err(create_error);
249                        }
250                    }
251                    held.push(checked_file(
252                        open_raw(&current, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
253                        true,
254                    )?);
255                }
256                Err(error) => return Err(error),
257            }
258        }
259        Ok(held)
260    }
261
262    fn relative_parts(relative: &Path) -> std::io::Result<Vec<OsString>> {
263        if relative.is_absolute()
264            || relative.components().any(|component| {
265                matches!(
266                    component,
267                    Component::ParentDir | Component::Prefix(_) | Component::RootDir
268                )
269            })
270        {
271            return Err(std::io::Error::new(
272                std::io::ErrorKind::PermissionDenied,
273                "store capability requires a contained relative path",
274            ));
275        }
276        let parts = relative
277            .components()
278            .filter_map(|component| match component {
279                Component::Normal(name) => Some(name.to_os_string()),
280                _ => None,
281            })
282            .collect::<Vec<_>>();
283        if parts.is_empty() {
284            return Err(std::io::Error::new(
285                std::io::ErrorKind::InvalidInput,
286                "path has no component",
287            ));
288        }
289        Ok(parts)
290    }
291
292    fn held_parent(
293        root: &File,
294        relative: &Path,
295        create: bool,
296    ) -> std::io::Result<(Vec<File>, PathBuf, OsString)> {
297        let parts = relative_parts(relative)?;
298        let mut current = directory_path(root)?;
299        let mut held = vec![root.try_clone()?];
300        for component in &parts[..parts.len() - 1] {
301            current.push(component);
302            let opened = open_raw(&current, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)
303                .and_then(|handle| checked_file(handle, true));
304            match opened {
305                Ok(directory) => held.push(directory),
306                Err(error) if create && error.kind() == std::io::ErrorKind::NotFound => {
307                    let path = wide(&current);
308                    if unsafe { CreateDirectoryW(path.as_ptr(), std::ptr::null()) } == 0 {
309                        let create_error = std::io::Error::last_os_error();
310                        if create_error.kind() != std::io::ErrorKind::AlreadyExists {
311                            return Err(create_error);
312                        }
313                    }
314                    held.push(checked_file(
315                        open_raw(&current, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
316                        true,
317                    )?);
318                }
319                Err(error) => return Err(error),
320            }
321            if contains_exact_regular_path(&current, OsStr::new("DB.md"))? {
322                return Err(std::io::Error::new(
323                    std::io::ErrorKind::PermissionDenied,
324                    "path crosses a nested db.md store boundary",
325                ));
326            }
327        }
328        Ok((held, current, parts.last().unwrap().clone()))
329    }
330
331    fn contains_exact_regular_path(path: &Path, wanted: &OsStr) -> std::io::Result<bool> {
332        for entry in std::fs::read_dir(path)? {
333            let entry = entry?;
334            if entry.file_name() != wanted {
335                continue;
336            }
337            return match open_raw(&entry.path(), FILE_READ_ATTRIBUTES, false, OPEN_EXISTING) {
338                Ok(handle) => checked_file(handle, false).map(|_| true),
339                Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(false),
340                Err(error) => Err(error),
341            };
342        }
343        Ok(false)
344    }
345
346    pub(super) fn contains_exact_regular(
347        directory: &File,
348        wanted: &OsStr,
349    ) -> std::io::Result<bool> {
350        contains_exact_regular_path(&directory_path(directory)?, wanted)
351    }
352
353    pub(super) fn open_regular(root: &File, relative: &Path) -> std::io::Result<File> {
354        let (_held, parent, leaf) = held_parent(root, relative, false)?;
355        checked_file(
356            open_raw(&parent.join(leaf), GENERIC_READ, false, OPEN_EXISTING)?,
357            false,
358        )
359    }
360
361    pub(super) fn open_regular_absolute(path: &Path) -> std::io::Result<File> {
362        let resolved = std::path::absolute(path)?;
363        let parent = resolved.parent().ok_or_else(|| {
364            std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
365        })?;
366        let _held = hold_directory_chain(parent, false)?;
367        checked_file(
368            open_raw(&resolved, GENERIC_READ, false, OPEN_EXISTING)?,
369            false,
370        )
371    }
372
373    fn atomic_write_target(
374        parent: &Path,
375        leaf: &OsStr,
376        bytes: &[u8],
377        create_new: bool,
378        durable: bool,
379    ) -> std::io::Result<()> {
380        let target = parent.join(leaf);
381        let prior_attrs = match open_raw(&target, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING) {
382            Ok(handle) => {
383                let attrs = attributes(handle)?;
384                unsafe { CloseHandle(handle) };
385                if attrs & (FILE_ATTRIBUTE_REPARSE_POINT | FILE_ATTRIBUTE_DIRECTORY) != 0 {
386                    return Err(std::io::Error::new(
387                        std::io::ErrorKind::PermissionDenied,
388                        "destination is a reparse point or directory",
389                    ));
390                }
391                if create_new {
392                    return Err(std::io::Error::new(
393                        std::io::ErrorKind::AlreadyExists,
394                        "destination already exists",
395                    ));
396                }
397                Some(attrs)
398            }
399            Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,
400            Err(error) => return Err(error),
401        };
402        static TEMP_SEQ: AtomicU64 = AtomicU64::new(0);
403        let nonce = TEMP_SEQ.fetch_add(1, Ordering::Relaxed);
404        let temp = parent.join(format!(
405            ".dbmd-tmp-{}-{}-{}",
406            std::process::id(),
407            SystemTime::now()
408                .duration_since(UNIX_EPOCH)
409                .unwrap_or_default()
410                .as_nanos(),
411            nonce
412        ));
413        let handle = open_raw(&temp, GENERIC_WRITE, false, CREATE_NEW)?;
414        let mut file = file_from_handle(handle);
415        if let Err(error) =
416            file.write_all(bytes)
417                .and_then(|_| if durable { file.sync_all() } else { Ok(()) })
418        {
419            drop(file);
420            let _ = std::fs::remove_file(&temp);
421            return Err(error);
422        }
423        drop(file);
424        let temp_wide = wide(&temp);
425        let target_wide = wide(&target);
426        let was_readonly = prior_attrs.is_some_and(|attrs| attrs & FILE_ATTRIBUTE_READONLY != 0);
427        if was_readonly {
428            let attrs = prior_attrs.expect("readonly destination has attributes")
429                & !FILE_ATTRIBUTE_READONLY;
430            let writable_attrs = if attrs == 0 {
431                FILE_ATTRIBUTE_NORMAL
432            } else {
433                attrs
434            };
435            if unsafe { SetFileAttributesW(target_wide.as_ptr(), writable_attrs) } == 0 {
436                let error = std::io::Error::last_os_error();
437                let _ = std::fs::remove_file(&temp);
438                return Err(error);
439            }
440        }
441        let flags = if create_new {
442            MOVEFILE_WRITE_THROUGH
443        } else {
444            MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH
445        };
446        if unsafe { MoveFileExW(temp_wide.as_ptr(), target_wide.as_ptr(), flags) } == 0 {
447            let error = std::io::Error::last_os_error();
448            if let Some(attrs) = prior_attrs {
449                let _ = unsafe { SetFileAttributesW(target_wide.as_ptr(), attrs) };
450            }
451            let _ = std::fs::remove_file(&temp);
452            return Err(error);
453        }
454        if was_readonly
455            && unsafe { SetFileAttributesW(target_wide.as_ptr(), FILE_ATTRIBUTE_READONLY) } == 0
456        {
457            return Err(std::io::Error::last_os_error());
458        }
459        Ok(())
460    }
461
462    pub(super) fn atomic_write_absolute(
463        path: &Path,
464        bytes: &[u8],
465        create_new: bool,
466        durable: bool,
467    ) -> std::io::Result<()> {
468        let resolved = std::path::absolute(path)?;
469        let parent = resolved.parent().ok_or_else(|| {
470            std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
471        })?;
472        let _held = hold_directory_chain(parent, true)?;
473        atomic_write_target(
474            parent,
475            resolved.file_name().unwrap(),
476            bytes,
477            create_new,
478            durable,
479        )
480    }
481
482    pub(super) fn atomic_write_beneath(
483        root: &File,
484        relative: &Path,
485        bytes: &[u8],
486        create_new: bool,
487        durable: bool,
488    ) -> std::io::Result<()> {
489        let (_held, parent, leaf) = held_parent(root, relative, true)?;
490        atomic_write_target(&parent, &leaf, bytes, create_new, durable)
491    }
492
493    pub(super) fn open_directory_beneath(
494        root: &File,
495        relative: &Path,
496        create: bool,
497    ) -> std::io::Result<File> {
498        if relative.as_os_str().is_empty() {
499            return root.try_clone();
500        }
501        let parts = relative_parts(relative)?;
502        let mut current = directory_path(root)?;
503        let mut held = vec![root.try_clone()?];
504        for component in parts {
505            current.push(component);
506            let opened = open_raw(&current, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)
507                .and_then(|handle| checked_file(handle, true));
508            match opened {
509                Ok(directory) => held.push(directory),
510                Err(error) if create && error.kind() == std::io::ErrorKind::NotFound => {
511                    let path = wide(&current);
512                    if unsafe { CreateDirectoryW(path.as_ptr(), std::ptr::null()) } == 0 {
513                        let create_error = std::io::Error::last_os_error();
514                        if create_error.kind() != std::io::ErrorKind::AlreadyExists {
515                            return Err(create_error);
516                        }
517                    }
518                    held.push(checked_file(
519                        open_raw(&current, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
520                        true,
521                    )?);
522                }
523                Err(error) => return Err(error),
524            }
525            if contains_exact_regular_path(&current, OsStr::new("DB.md"))? {
526                return Err(std::io::Error::new(
527                    std::io::ErrorKind::PermissionDenied,
528                    "path crosses a nested db.md store boundary",
529                ));
530            }
531        }
532        held.into_iter().last().ok_or_else(|| {
533            std::io::Error::new(std::io::ErrorKind::InvalidInput, "directory path is empty")
534        })
535    }
536
537    pub(super) fn lock_beneath(root: &File, relative: &Path) -> std::io::Result<File> {
538        let (_held, parent, leaf) = held_parent(root, relative, true)?;
539        let handle = open_raw(
540            &parent.join(leaf),
541            GENERIC_READ | GENERIC_WRITE,
542            false,
543            OPEN_ALWAYS,
544        )?;
545        let file = checked_file(handle, false)?;
546        let mut overlapped = unsafe { std::mem::zeroed::<OVERLAPPED>() };
547        if unsafe {
548            LockFileEx(
549                file.as_raw_handle() as HANDLE,
550                LOCKFILE_EXCLUSIVE_LOCK,
551                0,
552                u32::MAX,
553                u32::MAX,
554                &mut overlapped,
555            )
556        } == 0
557        {
558            return Err(std::io::Error::last_os_error());
559        }
560        Ok(file)
561    }
562    pub(super) fn rename_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
563        let (_old_held, old_parent, old_leaf) = held_parent(root, old, false)?;
564        let (_new_held, new_parent, new_leaf) = held_parent(root, new, true)?;
565        let old_path = old_parent.join(old_leaf);
566        let new_path = new_parent.join(new_leaf);
567        let source = checked_file(
568            open_raw(&old_path, FILE_READ_ATTRIBUTES, false, OPEN_EXISTING)?,
569            false,
570        )?;
571        drop(source);
572        let old_wide = wide(&old_path);
573        let new_wide = wide(&new_path);
574        if unsafe { MoveFileExW(old_wide.as_ptr(), new_wide.as_ptr(), MOVEFILE_WRITE_THROUGH) } == 0
575        {
576            return Err(std::io::Error::last_os_error());
577        }
578        Ok(())
579    }
580
581    pub(super) fn rename_directory_beneath(
582        root: &File,
583        old: &Path,
584        new: &Path,
585    ) -> std::io::Result<()> {
586        let (_old_held, old_parent, old_leaf) = held_parent(root, old, false)?;
587        let (_new_held, new_parent, new_leaf) = held_parent(root, new, true)?;
588        let old_path = old_parent.join(old_leaf);
589        let new_path = new_parent.join(new_leaf);
590        let source = checked_file(
591            open_raw(&old_path, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
592            true,
593        )?;
594        drop(source);
595        let old_wide = wide(&old_path);
596        let new_wide = wide(&new_path);
597        if unsafe { MoveFileExW(old_wide.as_ptr(), new_wide.as_ptr(), MOVEFILE_WRITE_THROUGH) } == 0
598        {
599            return Err(std::io::Error::last_os_error());
600        }
601        Ok(())
602    }
603
604    pub(super) fn remove_file_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
605        let (_held, parent, leaf) = held_parent(root, relative, false)?;
606        let path = parent.join(leaf);
607        let source = checked_file(
608            open_raw(
609                &path,
610                FILE_READ_ATTRIBUTES | FILE_WRITE_ATTRIBUTES,
611                false,
612                OPEN_EXISTING,
613            )?,
614            false,
615        )?;
616        drop(source);
617        let tombstone = parent.join(format!(
618            ".dbmd-remove-{}-{}",
619            std::process::id(),
620            TEMP_COUNTER.fetch_add(1, Ordering::Relaxed)
621        ));
622        let path_wide = wide(&path);
623        let tombstone_wide = wide(&tombstone);
624        if unsafe {
625            MoveFileExW(
626                path_wide.as_ptr(),
627                tombstone_wide.as_ptr(),
628                MOVEFILE_WRITE_THROUGH,
629            )
630        } == 0
631        {
632            return Err(std::io::Error::last_os_error());
633        }
634        std::fs::remove_file(tombstone)
635    }
636
637    static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0);
638
639    pub(super) fn remove_tree_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
640        let (_held, parent, leaf) = held_parent(root, relative, false)?;
641        fn remove(path: &Path) -> std::io::Result<()> {
642            let guard = checked_file(
643                open_raw(path, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
644                true,
645            )?;
646            for entry in std::fs::read_dir(path)? {
647                let entry = entry?;
648                let child = entry.path();
649                let handle = open_raw(&child, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?;
650                let attrs = attributes(handle)?;
651                unsafe { CloseHandle(handle) };
652                if attrs & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
653                    if attrs & FILE_ATTRIBUTE_DIRECTORY != 0 {
654                        let child_wide = wide(&child);
655                        if unsafe { RemoveDirectoryW(child_wide.as_ptr()) } == 0 {
656                            return Err(std::io::Error::last_os_error());
657                        }
658                    } else {
659                        std::fs::remove_file(child)?;
660                    }
661                } else if attrs & FILE_ATTRIBUTE_DIRECTORY != 0 {
662                    remove(&child)?;
663                } else {
664                    std::fs::remove_file(child)?;
665                }
666            }
667            drop(guard);
668            let wide = wide(path);
669            if unsafe { RemoveDirectoryW(wide.as_ptr()) } == 0 {
670                return Err(std::io::Error::last_os_error());
671            }
672            Ok(())
673        }
674        remove(&parent.join(leaf))
675    }
676
677    pub(super) fn entry_attributes(path: &Path) -> std::io::Result<u32> {
678        let handle = open_raw(path, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?;
679        let result = attributes(handle);
680        unsafe { CloseHandle(handle) };
681        result
682    }
683}
684
685/// Open one regular file exactly once through no-follow directory handles and
686/// read at most `max_bytes`. The size check and read operate on the same inode.
687pub fn read_bounded_nofollow(path: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
688    let file = open_regular_nofollow(path)?;
689    read_bounded_file(file, max_bytes)
690}
691
692fn read_bounded_file(file: File, max_bytes: u64) -> std::io::Result<Vec<u8>> {
693    let metadata = file.metadata()?;
694    if metadata.len() > max_bytes {
695        return Err(std::io::Error::new(
696            std::io::ErrorKind::InvalidData,
697            "file is not a bounded regular file",
698        ));
699    }
700    let mut bytes = Vec::with_capacity(metadata.len() as usize);
701    // `u64::MAX + 1` wraps in release builds and panics in debug builds. A
702    // caller is allowed to express "no smaller than the addressable stream" as
703    // `u64::MAX`; in that case there is no representable sentinel byte beyond
704    // the limit, so read to the saturated ceiling and rely on the descriptor
705    // metadata check above as the only possible over-limit signal.
706    file.take(max_bytes.saturating_add(1))
707        .read_to_end(&mut bytes)?;
708    if bytes.len() as u64 > max_bytes {
709        return Err(std::io::Error::new(
710            std::io::ErrorKind::InvalidData,
711            "file grew beyond the read limit",
712        ));
713    }
714    Ok(bytes)
715}
716
717/// A held store-directory capability for bounded sweep reads. Parent directory
718/// handles are cached by relative path, so a 10k-file scan pays one no-follow
719/// traversal per folder and one `openat` per file instead of reopening the
720/// entire ancestor chain for every record.
721#[cfg(unix)]
722#[derive(Debug)]
723pub(crate) struct BoundedDirReader {
724    root: File,
725    parents: BTreeMap<PathBuf, File>,
726}
727
728#[cfg(unix)]
729impl BoundedDirReader {
730    #[cfg_attr(not(test), allow(dead_code))]
731    pub(crate) fn new(root: &Path) -> std::io::Result<Self> {
732        let root = open_directory_nofollow(root)?;
733        Ok(Self {
734            root,
735            parents: BTreeMap::new(),
736        })
737    }
738
739    /// Start a bounded reader from an already-held directory capability. This
740    /// is the store-safe constructor: once `Store::open` succeeds, no later
741    /// operation re-resolves the user-supplied store pathname.
742    pub(crate) fn from_root(root: &File) -> std::io::Result<Self> {
743        Ok(Self {
744            root: root.try_clone()?,
745            parents: BTreeMap::new(),
746        })
747    }
748
749    pub(crate) fn read(&mut self, relative: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
750        read_bounded_file(self.open(relative)?, max_bytes)
751    }
752
753    pub(crate) fn open(&mut self, relative: &Path) -> std::io::Result<File> {
754        use std::os::fd::{AsRawFd as _, FromRawFd as _};
755
756        if relative.is_absolute()
757            || relative
758                .components()
759                .any(|component| matches!(component, Component::ParentDir | Component::Prefix(_)))
760        {
761            return Err(std::io::Error::new(
762                std::io::ErrorKind::PermissionDenied,
763                "bounded directory read requires a contained relative path",
764            ));
765        }
766        let leaf = relative.file_name().ok_or_else(|| {
767            std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
768        })?;
769        let parent = relative.parent().unwrap_or_else(|| Path::new(""));
770
771        if !self.parents.contains_key(parent) {
772            let mut cursor = self.root.try_clone()?;
773            for component in parent.components() {
774                let Component::Normal(name) = component else {
775                    continue;
776                };
777                let fd = unsafe {
778                    libc::openat(
779                        cursor.as_raw_fd(),
780                        c_name(name)?.as_ptr(),
781                        libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
782                    )
783                };
784                if fd < 0 {
785                    return Err(std::io::Error::last_os_error());
786                }
787                cursor = unsafe { File::from_raw_fd(fd) };
788                if directory_contains_exact_regular(&cursor, "DB.md".as_ref())? {
789                    return Err(std::io::Error::new(
790                        std::io::ErrorKind::PermissionDenied,
791                        "refusing to cross a nested db.md store boundary",
792                    ));
793                }
794            }
795            self.parents.insert(parent.to_path_buf(), cursor);
796        }
797
798        let parent_fd = self
799            .parents
800            .get(parent)
801            .expect("parent capability inserted above")
802            .as_raw_fd();
803        let fd = unsafe {
804            libc::openat(
805                parent_fd,
806                c_name(leaf)?.as_ptr(),
807                libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
808            )
809        };
810        if fd < 0 {
811            return Err(std::io::Error::last_os_error());
812        }
813        let file = unsafe { File::from_raw_fd(fd) };
814        if !file.metadata()?.is_file() {
815            return Err(std::io::Error::new(
816                std::io::ErrorKind::InvalidData,
817                "refusing to read a non-regular file",
818            ));
819        }
820        Ok(file)
821    }
822}
823
824/// Open one directory exactly once without following its leaf or any ancestor.
825/// Store roots retain this descriptor for their full lifetime so a rename or
826/// symlink swap of the original pathname cannot redirect later operations.
827#[cfg(unix)]
828pub(crate) fn open_directory_nofollow(path: &Path) -> std::io::Result<File> {
829    // Reuse the ancestor traversal by appending a synthetic leaf and retaining
830    // the returned parent. Unlike `path.file_name()`, this also handles the
831    // ordinary store spellings `.` and `/`.
832    let (directory, _) = open_parent_unix(&path.join(".dbmd-held-root-capability"), false)?;
833    Ok(directory)
834}
835
836#[cfg(windows)]
837pub(crate) fn open_directory_nofollow(path: &Path) -> std::io::Result<File> {
838    windows_fs::open_directory(path)
839}
840
841/// Open a Windows directory path through no-follow capabilities, creating
842/// missing components without ever accepting a reparse-point component.
843#[cfg(windows)]
844pub(crate) fn open_or_create_directory_nofollow(path: &Path) -> std::io::Result<File> {
845    windows_fs::open_or_create_directory(path)
846}
847
848#[cfg(not(any(unix, windows)))]
849pub(crate) fn open_directory_nofollow(_path: &Path) -> std::io::Result<File> {
850    Err(secure_filesystem_unsupported())
851}
852
853/// Test for an exact byte-for-byte regular-file basename inside a held
854/// directory. This is deliberately descriptor-relative: on a case-insensitive
855/// filesystem `openat(dir, "DB.md")` can open a lowercase `db.md`, while the
856/// db.md format requires the uppercase marker spelling.
857#[cfg(unix)]
858pub(crate) fn directory_contains_exact_regular(
859    directory: &File,
860    wanted: &std::ffi::OsStr,
861) -> std::io::Result<bool> {
862    use std::os::fd::AsRawFd as _;
863    use std::os::unix::ffi::OsStrExt as _;
864
865    let dot = c_name(std::ffi::OsStr::new("."))?;
866    let scan_fd = unsafe {
867        libc::openat(
868            directory.as_raw_fd(),
869            dot.as_ptr(),
870            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
871        )
872    };
873    if scan_fd < 0 {
874        return Err(std::io::Error::last_os_error());
875    }
876    let stream = unsafe { libc::fdopendir(scan_fd) };
877    if stream.is_null() {
878        let error = std::io::Error::last_os_error();
879        unsafe {
880            libc::close(scan_fd);
881        }
882        return Err(error);
883    }
884
885    let mut found = false;
886    loop {
887        let entry = unsafe { libc::readdir(stream) };
888        if entry.is_null() {
889            break;
890        }
891        let name = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) };
892        if name.to_bytes() != wanted.as_bytes() {
893            continue;
894        }
895        let c_wanted = c_name(wanted)?;
896        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
897        if unsafe {
898            libc::fstatat(
899                directory.as_raw_fd(),
900                c_wanted.as_ptr(),
901                &mut stat,
902                libc::AT_SYMLINK_NOFOLLOW,
903            )
904        } == 0
905            && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
906        {
907            found = true;
908        }
909        break;
910    }
911    if unsafe { libc::closedir(stream) } != 0 {
912        return Err(std::io::Error::last_os_error());
913    }
914    Ok(found)
915}
916
917#[cfg(windows)]
918pub(crate) fn directory_contains_exact_regular(
919    directory: &File,
920    wanted: &std::ffi::OsStr,
921) -> std::io::Result<bool> {
922    windows_fs::contains_exact_regular(directory, wanted)
923}
924
925#[cfg(not(any(unix, windows)))]
926pub(crate) fn directory_contains_exact_regular(
927    _directory: &File,
928    _wanted: &std::ffi::OsStr,
929) -> std::io::Result<bool> {
930    Err(secure_filesystem_unsupported())
931}
932
933/// Recursively enumerate regular files below a held directory capability.
934///
935/// Symlinks, hidden names, and nested db.md stores are never traversed. Paths
936/// are returned relative to `root`, including the caller-supplied `start`
937/// prefix. This is the sweep-side counterpart to [`BoundedDirReader`]: callers
938/// do not reopen the mutable store pathname merely to discover what to read.
939#[cfg(unix)]
940pub(crate) fn walk_regular_files_beneath(
941    root: &File,
942    start: &Path,
943) -> std::io::Result<Vec<PathBuf>> {
944    use std::os::fd::AsRawFd as _;
945    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
946
947    const MAX_WALK_ENTRIES: usize = 1_000_000;
948
949    fn open_dir_at(parent: &File, name: &std::ffi::OsStr) -> std::io::Result<File> {
950        use std::os::fd::{AsRawFd as _, FromRawFd as _};
951        let fd = unsafe {
952            libc::openat(
953                parent.as_raw_fd(),
954                c_name(name)?.as_ptr(),
955                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
956            )
957        };
958        if fd < 0 {
959            return Err(std::io::Error::last_os_error());
960        }
961        Ok(unsafe { File::from_raw_fd(fd) })
962    }
963
964    if start.is_absolute()
965        || start.components().any(|component| {
966            matches!(
967                component,
968                Component::ParentDir | Component::Prefix(_) | Component::RootDir
969            )
970        })
971    {
972        return Err(std::io::Error::new(
973            std::io::ErrorKind::PermissionDenied,
974            "store walk requires a contained relative path",
975        ));
976    }
977
978    let mut start_dir = root.try_clone()?;
979    for component in start.components() {
980        let Component::Normal(name) = component else {
981            continue;
982        };
983        start_dir = open_dir_at(&start_dir, name)?;
984        if directory_contains_exact_regular(&start_dir, "DB.md".as_ref())? {
985            return Ok(Vec::new());
986        }
987    }
988
989    let mut pending = vec![(start_dir, start.to_path_buf())];
990    let mut files = Vec::new();
991    let mut seen = 0usize;
992    while let Some((directory, relative_dir)) = pending.pop() {
993        let scan_fd = unsafe {
994            libc::openat(
995                directory.as_raw_fd(),
996                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
997                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
998            )
999        };
1000        if scan_fd < 0 {
1001            return Err(std::io::Error::last_os_error());
1002        }
1003        let stream = unsafe { libc::fdopendir(scan_fd) };
1004        if stream.is_null() {
1005            let error = std::io::Error::last_os_error();
1006            unsafe {
1007                libc::close(scan_fd);
1008            }
1009            return Err(error);
1010        }
1011
1012        let mut names = Vec::new();
1013        loop {
1014            let entry = unsafe { libc::readdir(stream) };
1015            if entry.is_null() {
1016                break;
1017            }
1018            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1019            if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
1020                continue;
1021            }
1022            names.push(std::ffi::OsString::from_vec(bytes.to_vec()));
1023            seen = seen.saturating_add(1);
1024            if seen > MAX_WALK_ENTRIES {
1025                unsafe {
1026                    libc::closedir(stream);
1027                }
1028                return Err(std::io::Error::new(
1029                    std::io::ErrorKind::InvalidData,
1030                    "store contains more than 1000000 visible entries",
1031                ));
1032            }
1033        }
1034        if unsafe { libc::closedir(stream) } != 0 {
1035            return Err(std::io::Error::last_os_error());
1036        }
1037        names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1038
1039        for name in names {
1040            let c_name = c_name(&name)?;
1041            let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1042            if unsafe {
1043                libc::fstatat(
1044                    directory.as_raw_fd(),
1045                    c_name.as_ptr(),
1046                    &mut stat,
1047                    libc::AT_SYMLINK_NOFOLLOW,
1048                )
1049            } != 0
1050            {
1051                let error = std::io::Error::last_os_error();
1052                if error.kind() == std::io::ErrorKind::NotFound {
1053                    continue;
1054                }
1055                return Err(error);
1056            }
1057            let relative = relative_dir.join(&name);
1058            match stat.st_mode & libc::S_IFMT {
1059                libc::S_IFREG => files.push(relative),
1060                libc::S_IFDIR => {
1061                    let child = open_dir_at(&directory, &name)?;
1062                    if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
1063                        pending.push((child, relative));
1064                    }
1065                }
1066                _ => {}
1067            }
1068        }
1069    }
1070    files.sort();
1071    Ok(files)
1072}
1073
1074/// Discover visible symlinks and nested-store roots without following either.
1075#[cfg(unix)]
1076pub(crate) fn ownership_boundaries_beneath(
1077    root: &File,
1078) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
1079    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1080    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
1081
1082    let mut pending = vec![(root.try_clone()?, PathBuf::new())];
1083    let mut symlinks = Vec::new();
1084    let mut nested = Vec::new();
1085    while let Some((directory, relative_dir)) = pending.pop() {
1086        let scan_fd = unsafe {
1087            libc::openat(
1088                directory.as_raw_fd(),
1089                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
1090                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1091            )
1092        };
1093        if scan_fd < 0 {
1094            return Err(std::io::Error::last_os_error());
1095        }
1096        let stream = unsafe { libc::fdopendir(scan_fd) };
1097        if stream.is_null() {
1098            let error = std::io::Error::last_os_error();
1099            unsafe {
1100                libc::close(scan_fd);
1101            }
1102            return Err(error);
1103        }
1104        let mut names = Vec::new();
1105        loop {
1106            let entry = unsafe { libc::readdir(stream) };
1107            if entry.is_null() {
1108                break;
1109            }
1110            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1111            if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
1112                continue;
1113            }
1114            names.push(std::ffi::OsString::from_vec(bytes.to_vec()));
1115        }
1116        if unsafe { libc::closedir(stream) } != 0 {
1117            return Err(std::io::Error::last_os_error());
1118        }
1119        names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1120        for name in names {
1121            let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1122            if unsafe {
1123                libc::fstatat(
1124                    directory.as_raw_fd(),
1125                    c_name(&name)?.as_ptr(),
1126                    &mut stat,
1127                    libc::AT_SYMLINK_NOFOLLOW,
1128                )
1129            } != 0
1130            {
1131                continue;
1132            }
1133            let relative = relative_dir.join(&name);
1134            match stat.st_mode & libc::S_IFMT {
1135                libc::S_IFLNK => symlinks.push(relative),
1136                libc::S_IFDIR => {
1137                    let fd = unsafe {
1138                        libc::openat(
1139                            directory.as_raw_fd(),
1140                            c_name(&name)?.as_ptr(),
1141                            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1142                        )
1143                    };
1144                    if fd < 0 {
1145                        return Err(std::io::Error::last_os_error());
1146                    }
1147                    let child = unsafe { File::from_raw_fd(fd) };
1148                    if directory_contains_exact_regular(&child, "DB.md".as_ref())? {
1149                        nested.push(relative);
1150                    } else {
1151                        pending.push((child, relative));
1152                    }
1153                }
1154                _ => {}
1155            }
1156        }
1157    }
1158    symlinks.sort();
1159    nested.sort();
1160    Ok((symlinks, nested))
1161}
1162
1163#[cfg(windows)]
1164pub(crate) fn walk_regular_files_beneath(
1165    root: &File,
1166    start: &Path,
1167) -> std::io::Result<Vec<PathBuf>> {
1168    const MAX_WALK_ENTRIES: usize = 1_000_000;
1169    let start_dir = windows_fs::open_directory_beneath(root, start, false)?;
1170    let mut pending = vec![(start_dir, start.to_path_buf())];
1171    let mut files = Vec::new();
1172    let mut seen = 0usize;
1173    while let Some((directory, relative)) = pending.pop() {
1174        let path = windows_fs::directory_path(&directory)?;
1175        let mut entries = std::fs::read_dir(&path)?.collect::<Result<Vec<_>, _>>()?;
1176        entries.sort_by_key(|entry| entry.file_name());
1177        for entry in entries {
1178            let name = entry.file_name();
1179            if name.to_string_lossy().starts_with('.') {
1180                continue;
1181            }
1182            seen += 1;
1183            if seen > MAX_WALK_ENTRIES {
1184                return Err(std::io::Error::new(
1185                    std::io::ErrorKind::InvalidData,
1186                    "store contains more than 1000000 visible entries",
1187                ));
1188            }
1189            let attrs = windows_fs::entry_attributes(&entry.path())?;
1190            let child_relative = relative.join(&name);
1191            if attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1192                continue;
1193            }
1194            if attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY != 0 {
1195                let child = windows_fs::open_directory(&entry.path())?;
1196                if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
1197                    pending.push((child, child_relative));
1198                }
1199            } else {
1200                let _ = windows_fs::open_regular(root, &child_relative)?;
1201                files.push(child_relative);
1202            }
1203        }
1204    }
1205    files.sort();
1206    Ok(files)
1207}
1208
1209#[cfg(windows)]
1210pub(crate) fn ownership_boundaries_beneath(
1211    root: &File,
1212) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
1213    let mut pending = vec![(root.try_clone()?, PathBuf::new())];
1214    let mut reparse = Vec::new();
1215    let mut nested = Vec::new();
1216    while let Some((directory, relative)) = pending.pop() {
1217        let path = windows_fs::directory_path(&directory)?;
1218        for entry in std::fs::read_dir(&path)? {
1219            let entry = entry?;
1220            let name = entry.file_name();
1221            if name.to_string_lossy().starts_with('.') {
1222                continue;
1223            }
1224            let child_relative = relative.join(&name);
1225            let attrs = windows_fs::entry_attributes(&entry.path())?;
1226            if attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1227                reparse.push(child_relative);
1228            } else if attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY != 0
1229            {
1230                let child = windows_fs::open_directory(&entry.path())?;
1231                if directory_contains_exact_regular(&child, "DB.md".as_ref())? {
1232                    nested.push(child_relative);
1233                } else {
1234                    pending.push((child, child_relative));
1235                }
1236            }
1237        }
1238    }
1239    reparse.sort();
1240    nested.sort();
1241    Ok((reparse, nested))
1242}
1243
1244#[cfg(not(any(unix, windows)))]
1245pub(crate) fn ownership_boundaries_beneath(
1246    _root: &File,
1247) -> std::io::Result<(Vec<PathBuf>, Vec<PathBuf>)> {
1248    Err(secure_filesystem_unsupported())
1249}
1250
1251#[cfg(unix)]
1252pub(crate) fn regular_file_names_beneath(
1253    root: &File,
1254    directory: &Path,
1255) -> std::io::Result<Vec<std::ffi::OsString>> {
1256    use std::os::fd::AsRawFd as _;
1257    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
1258
1259    let probe = directory.join(".dbmd-directory-list-probe");
1260    let (directory, _) = open_parent_beneath(root, &probe, false)?;
1261    let scan_fd = unsafe {
1262        libc::openat(
1263            directory.as_raw_fd(),
1264            c_name(std::ffi::OsStr::new("."))?.as_ptr(),
1265            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1266        )
1267    };
1268    if scan_fd < 0 {
1269        return Err(std::io::Error::last_os_error());
1270    }
1271    let stream = unsafe { libc::fdopendir(scan_fd) };
1272    if stream.is_null() {
1273        let error = std::io::Error::last_os_error();
1274        unsafe {
1275            libc::close(scan_fd);
1276        }
1277        return Err(error);
1278    }
1279
1280    let mut names = Vec::new();
1281    loop {
1282        let entry = unsafe { libc::readdir(stream) };
1283        if entry.is_null() {
1284            break;
1285        }
1286        let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1287        if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
1288            continue;
1289        }
1290        let name = std::ffi::OsString::from_vec(bytes.to_vec());
1291        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
1292        if unsafe {
1293            libc::fstatat(
1294                directory.as_raw_fd(),
1295                c_name(&name)?.as_ptr(),
1296                &mut stat,
1297                libc::AT_SYMLINK_NOFOLLOW,
1298            )
1299        } == 0
1300            && (stat.st_mode & libc::S_IFMT) == libc::S_IFREG
1301        {
1302            names.push(name);
1303        }
1304    }
1305    if unsafe { libc::closedir(stream) } != 0 {
1306        return Err(std::io::Error::last_os_error());
1307    }
1308    names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
1309    Ok(names)
1310}
1311
1312#[cfg(not(any(unix, windows)))]
1313pub(crate) fn regular_file_names_beneath(
1314    _root: &File,
1315    _directory: &Path,
1316) -> std::io::Result<Vec<std::ffi::OsString>> {
1317    Err(secure_filesystem_unsupported())
1318}
1319
1320#[cfg(windows)]
1321pub(crate) fn regular_file_names_beneath(
1322    root: &File,
1323    directory: &Path,
1324) -> std::io::Result<Vec<std::ffi::OsString>> {
1325    let held = windows_fs::open_directory_beneath(root, directory, false)?;
1326    let path = windows_fs::directory_path(&held)?;
1327    let mut names = Vec::new();
1328    for entry in std::fs::read_dir(path)? {
1329        let entry = entry?;
1330        let name = entry.file_name();
1331        if name.to_string_lossy().starts_with('.') {
1332            continue;
1333        }
1334        let attrs = windows_fs::entry_attributes(&entry.path())?;
1335        if attrs
1336            & (windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT
1337                | windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY)
1338            == 0
1339        {
1340            names.push(name);
1341        }
1342    }
1343    names.sort();
1344    Ok(names)
1345}
1346
1347#[cfg(not(any(unix, windows)))]
1348pub(crate) fn walk_regular_files_beneath(
1349    _root: &File,
1350    _start: &Path,
1351) -> std::io::Result<Vec<PathBuf>> {
1352    Err(secure_filesystem_unsupported())
1353}
1354
1355#[cfg(windows)]
1356#[derive(Debug)]
1357pub(crate) struct BoundedDirReader {
1358    root: File,
1359}
1360
1361#[cfg(windows)]
1362impl BoundedDirReader {
1363    #[allow(dead_code)]
1364    pub(crate) fn new(root: &Path) -> std::io::Result<Self> {
1365        Ok(Self {
1366            root: open_directory_nofollow(root)?,
1367        })
1368    }
1369
1370    pub(crate) fn from_root(root: &File) -> std::io::Result<Self> {
1371        Ok(Self {
1372            root: root.try_clone()?,
1373        })
1374    }
1375
1376    pub(crate) fn read(&mut self, relative: &Path, max_bytes: u64) -> std::io::Result<Vec<u8>> {
1377        read_bounded_file(self.open(relative)?, max_bytes)
1378    }
1379
1380    pub(crate) fn open(&mut self, relative: &Path) -> std::io::Result<File> {
1381        windows_fs::open_regular(&self.root, relative)
1382    }
1383}
1384
1385#[cfg(not(any(unix, windows)))]
1386pub(crate) struct BoundedDirReader;
1387
1388#[cfg(not(any(unix, windows)))]
1389impl BoundedDirReader {
1390    pub(crate) fn new(_root: &Path) -> std::io::Result<Self> {
1391        Err(secure_filesystem_unsupported())
1392    }
1393
1394    pub(crate) fn read(&mut self, _relative: &Path, _max_bytes: u64) -> std::io::Result<Vec<u8>> {
1395        Err(secure_filesystem_unsupported())
1396    }
1397
1398    pub(crate) fn open(&mut self, _relative: &Path) -> std::io::Result<File> {
1399        Err(secure_filesystem_unsupported())
1400    }
1401}
1402
1403/// Open one regular file through held no-follow parent descriptors. The caller
1404/// may safely perform metadata checks and reads on the returned inode without a
1405/// pathname reopen in between.
1406pub fn open_regular_nofollow(path: &Path) -> std::io::Result<File> {
1407    #[cfg(unix)]
1408    {
1409        use std::os::fd::{AsRawFd as _, FromRawFd as _};
1410        let (directory, leaf) = open_parent_unix(path, false)?;
1411        let fd = unsafe {
1412            libc::openat(
1413                directory.as_raw_fd(),
1414                leaf.as_ptr(),
1415                libc::O_RDONLY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1416            )
1417        };
1418        if fd < 0 {
1419            return Err(std::io::Error::last_os_error());
1420        }
1421        let file = unsafe { File::from_raw_fd(fd) };
1422        let metadata = file.metadata()?;
1423        if !metadata.is_file() {
1424            return Err(std::io::Error::new(
1425                std::io::ErrorKind::InvalidData,
1426                "path is not a regular file",
1427            ));
1428        }
1429        Ok(file)
1430    }
1431    #[cfg(windows)]
1432    {
1433        windows_fs::open_regular_absolute(path)
1434    }
1435    #[cfg(not(any(unix, windows)))]
1436    {
1437        let _ = path;
1438        Err(secure_filesystem_unsupported())
1439    }
1440}
1441
1442/// Rename a single entry without re-resolving either parent through mutable
1443/// pathnames. Existing symlink ancestors are refused.
1444#[cfg(unix)]
1445pub fn rename_nofollow(old: &Path, new: &Path) -> std::io::Result<()> {
1446    use std::os::fd::AsRawFd as _;
1447    let (old_parent, old_leaf) = open_parent_unix(old, false)?;
1448    let (new_parent, new_leaf) = open_parent_unix(new, true)?;
1449    let mut source_stat: libc::stat = unsafe { std::mem::zeroed() };
1450    if unsafe {
1451        libc::fstatat(
1452            old_parent.as_raw_fd(),
1453            old_leaf.as_ptr(),
1454            &mut source_stat,
1455            libc::AT_SYMLINK_NOFOLLOW,
1456        )
1457    } != 0
1458    {
1459        return Err(std::io::Error::last_os_error());
1460    }
1461    if (source_stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
1462        return Err(std::io::Error::new(
1463            std::io::ErrorKind::PermissionDenied,
1464            "refusing to rename a non-regular file",
1465        ));
1466    }
1467    renameat_noreplace(
1468        old_parent.as_raw_fd(),
1469        &old_leaf,
1470        new_parent.as_raw_fd(),
1471        &new_leaf,
1472    )?;
1473    old_parent.sync_all()?;
1474    new_parent.sync_all()?;
1475    Ok(())
1476}
1477
1478#[cfg(not(any(unix, windows)))]
1479pub fn rename_nofollow(_old: &Path, _new: &Path) -> std::io::Result<()> {
1480    Err(secure_filesystem_unsupported())
1481}
1482
1483#[cfg(windows)]
1484pub fn rename_nofollow(old: &Path, new: &Path) -> std::io::Result<()> {
1485    let parent = std::path::absolute(old)?
1486        .parent()
1487        .ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent"))?
1488        .to_path_buf();
1489    let root = windows_fs::open_directory(&parent)?;
1490    let old_name = old.file_name().ok_or_else(|| {
1491        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
1492    })?;
1493    let new_absolute = std::path::absolute(new)?;
1494    let new_parent = new_absolute.parent().ok_or_else(|| {
1495        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no parent")
1496    })?;
1497    if std::path::absolute(&parent)? != std::path::absolute(new_parent)? {
1498        return Err(std::io::Error::new(
1499            std::io::ErrorKind::Unsupported,
1500            "secure Windows no-replace rename currently requires one parent directory",
1501        ));
1502    }
1503    windows_fs::rename_beneath(
1504        &root,
1505        Path::new(old_name),
1506        Path::new(new.file_name().unwrap()),
1507    )
1508}
1509
1510#[cfg(any(target_os = "linux", target_os = "android"))]
1511fn renameat_noreplace(
1512    old_dir: std::os::fd::RawFd,
1513    old: &std::ffi::CStr,
1514    new_dir: std::os::fd::RawFd,
1515    new: &std::ffi::CStr,
1516) -> std::io::Result<()> {
1517    let result = unsafe {
1518        libc::syscall(
1519            libc::SYS_renameat2,
1520            old_dir,
1521            old.as_ptr(),
1522            new_dir,
1523            new.as_ptr(),
1524            libc::RENAME_NOREPLACE,
1525        )
1526    };
1527    if result != 0 {
1528        return Err(std::io::Error::last_os_error());
1529    }
1530    Ok(())
1531}
1532
1533#[cfg(target_os = "macos")]
1534fn renameat_noreplace(
1535    old_dir: std::os::fd::RawFd,
1536    old: &std::ffi::CStr,
1537    new_dir: std::os::fd::RawFd,
1538    new: &std::ffi::CStr,
1539) -> std::io::Result<()> {
1540    if unsafe {
1541        libc::renameatx_np(
1542            old_dir,
1543            old.as_ptr(),
1544            new_dir,
1545            new.as_ptr(),
1546            libc::RENAME_EXCL,
1547        )
1548    } != 0
1549    {
1550        return Err(std::io::Error::last_os_error());
1551    }
1552    Ok(())
1553}
1554
1555#[cfg(all(
1556    unix,
1557    not(any(target_os = "linux", target_os = "android", target_os = "macos"))
1558))]
1559fn renameat_noreplace(
1560    _old_dir: std::os::fd::RawFd,
1561    _old: &std::ffi::CStr,
1562    _new_dir: std::os::fd::RawFd,
1563    _new: &std::ffi::CStr,
1564) -> std::io::Result<()> {
1565    Err(std::io::Error::new(
1566        std::io::ErrorKind::Unsupported,
1567        "atomic no-replace rename is unsupported on this Unix platform",
1568    ))
1569}
1570
1571#[cfg(unix)]
1572fn c_name(value: &std::ffi::OsStr) -> std::io::Result<std::ffi::CString> {
1573    use std::os::unix::ffi::OsStrExt as _;
1574    std::ffi::CString::new(value.as_bytes())
1575        .map_err(|_| std::io::Error::new(std::io::ErrorKind::InvalidInput, "NUL in path"))
1576}
1577
1578#[cfg(unix)]
1579fn open_parent_unix(path: &Path, create: bool) -> std::io::Result<(File, std::ffi::CString)> {
1580    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1581    use std::path::Component;
1582
1583    #[cfg(target_os = "macos")]
1584    let path = [("/var", "/private/var"), ("/tmp", "/private/tmp")]
1585        .into_iter()
1586        .find_map(|(alias, real)| {
1587            path.strip_prefix(alias)
1588                .ok()
1589                .map(|rest| Path::new(real).join(rest))
1590        })
1591        .unwrap_or_else(|| path.to_path_buf());
1592    #[cfg(not(target_os = "macos"))]
1593    let path = path.to_path_buf();
1594
1595    let leaf = path.file_name().ok_or_else(|| {
1596        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
1597    })?;
1598    let parent = path.parent().unwrap_or_else(|| Path::new("."));
1599    let mut directory = if path.is_absolute() {
1600        File::open("/")?
1601    } else {
1602        File::open(".")?
1603    };
1604    for component in parent.components() {
1605        let name = match component {
1606            Component::RootDir | Component::CurDir => continue,
1607            Component::ParentDir => std::ffi::OsStr::new(".."),
1608            Component::Normal(name) => name,
1609            Component::Prefix(_) => {
1610                return Err(std::io::Error::new(
1611                    std::io::ErrorKind::Unsupported,
1612                    "Windows path prefix is unsupported",
1613                ))
1614            }
1615        };
1616        let name = c_name(name)?;
1617        if create {
1618            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
1619            if made != 0 {
1620                let error = std::io::Error::last_os_error();
1621                if error.raw_os_error() != Some(libc::EEXIST) {
1622                    return Err(error);
1623                }
1624            }
1625        }
1626        let fd = unsafe {
1627            libc::openat(
1628                directory.as_raw_fd(),
1629                name.as_ptr(),
1630                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1631            )
1632        };
1633        if fd < 0 {
1634            return Err(std::io::Error::last_os_error());
1635        }
1636        directory = unsafe { File::from_raw_fd(fd) };
1637    }
1638    Ok((directory, c_name(leaf)?))
1639}
1640
1641#[cfg(unix)]
1642fn open_parent_beneath(
1643    root: &File,
1644    relative: &Path,
1645    create: bool,
1646) -> std::io::Result<(File, std::ffi::CString)> {
1647    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1648
1649    if relative.is_absolute()
1650        || relative.components().any(|component| {
1651            matches!(
1652                component,
1653                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1654            )
1655        })
1656    {
1657        return Err(std::io::Error::new(
1658            std::io::ErrorKind::PermissionDenied,
1659            "store capability requires a contained relative path",
1660        ));
1661    }
1662    let leaf = relative.file_name().ok_or_else(|| {
1663        std::io::Error::new(std::io::ErrorKind::InvalidInput, "path has no file name")
1664    })?;
1665    let parent = relative.parent().unwrap_or_else(|| Path::new(""));
1666    if !parent.as_os_str().is_empty() && leaf == std::ffi::OsStr::new("DB.md") {
1667        return Err(std::io::Error::new(
1668            std::io::ErrorKind::PermissionDenied,
1669            "refusing to create a nested store marker",
1670        ));
1671    }
1672
1673    let mut directory = root.try_clone()?;
1674    for component in parent.components() {
1675        let Component::Normal(name) = component else {
1676            continue;
1677        };
1678        let name = c_name(name)?;
1679        if create {
1680            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
1681            if made != 0 {
1682                let error = std::io::Error::last_os_error();
1683                if error.raw_os_error() != Some(libc::EEXIST) {
1684                    return Err(error);
1685                }
1686            }
1687        }
1688        let fd = unsafe {
1689            libc::openat(
1690                directory.as_raw_fd(),
1691                name.as_ptr(),
1692                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1693            )
1694        };
1695        if fd < 0 {
1696            return Err(std::io::Error::last_os_error());
1697        }
1698        directory = unsafe { File::from_raw_fd(fd) };
1699        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
1700            return Err(std::io::Error::new(
1701                std::io::ErrorKind::PermissionDenied,
1702                "path crosses a nested db.md store boundary",
1703            ));
1704        }
1705    }
1706    Ok((directory, c_name(leaf)?))
1707}
1708
1709#[cfg(unix)]
1710pub(crate) fn write_atomic_beneath(
1711    root: &File,
1712    relative: &Path,
1713    bytes: &[u8],
1714    create_new: bool,
1715    durable: bool,
1716) -> std::io::Result<()> {
1717    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
1718    write_atomic_at(directory, leaf, bytes, create_new, durable, None)
1719}
1720
1721/// Atomically install private control/recovery material beneath a held root.
1722/// Unix creates the temporary inode at 0600 and forces the installed file back
1723/// to 0600 even if an interrupted/manual operation widened the destination.
1724#[cfg(unix)]
1725pub(crate) fn write_private_atomic_beneath(
1726    root: &File,
1727    relative: &Path,
1728    bytes: &[u8],
1729    create_new: bool,
1730) -> std::io::Result<()> {
1731    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
1732    write_atomic_at(directory, leaf, bytes, create_new, true, Some(0o600))
1733}
1734
1735#[cfg(windows)]
1736pub(crate) fn write_atomic_beneath(
1737    root: &File,
1738    relative: &Path,
1739    bytes: &[u8],
1740    create_new: bool,
1741    durable: bool,
1742) -> std::io::Result<()> {
1743    windows_fs::atomic_write_beneath(root, relative, bytes, create_new, durable)
1744}
1745
1746#[cfg(windows)]
1747pub(crate) fn write_private_atomic_beneath(
1748    root: &File,
1749    relative: &Path,
1750    bytes: &[u8],
1751    create_new: bool,
1752) -> std::io::Result<()> {
1753    // Windows has no POSIX mode; the secure no-follow writer inherits the
1754    // current user's ACL from the private checkout directory.
1755    windows_fs::atomic_write_beneath(root, relative, bytes, create_new, true)
1756}
1757
1758/// Atomically replace a rebuildable file beneath a held root without forcing
1759/// the bytes or directory entry to stable storage.
1760#[cfg(unix)]
1761pub(crate) fn write_atomic_nondurable_beneath(
1762    root: &File,
1763    relative: &Path,
1764    bytes: &[u8],
1765) -> std::io::Result<()> {
1766    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
1767    write_atomic_at(directory, leaf, bytes, false, false, None)
1768}
1769
1770#[cfg(windows)]
1771pub(crate) fn write_atomic_nondurable_beneath(
1772    root: &File,
1773    relative: &Path,
1774    bytes: &[u8],
1775) -> std::io::Result<()> {
1776    windows_fs::atomic_write_beneath(root, relative, bytes, false, false)
1777}
1778
1779#[cfg(not(any(unix, windows)))]
1780pub(crate) fn write_atomic_nondurable_beneath(
1781    _root: &File,
1782    _relative: &Path,
1783    _bytes: &[u8],
1784) -> std::io::Result<()> {
1785    Err(secure_filesystem_unsupported())
1786}
1787
1788/// Open or create a regular advisory-lock file beneath a held root, then take
1789/// an exclusive `flock` on the exact inode. The parent traversal and leaf open
1790/// are no-follow, so replacing the store's original pathname cannot redirect
1791/// the lock into another tree.
1792#[cfg(unix)]
1793pub(crate) fn lock_exclusive_beneath(root: &File, relative: &Path) -> std::io::Result<File> {
1794    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1795
1796    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
1797    // Darwin can transiently report ENOENT when two creators race on the same
1798    // O_CREAT+O_NOFOLLOW leaf. Retry that lookup race (and EINTR) against the
1799    // same held parent; every successful contender still opens the one shared
1800    // inode and serializes on `flock`.
1801    let mut retries = 0_u8;
1802    let fd = loop {
1803        let fd = unsafe {
1804            libc::openat(
1805                directory.as_raw_fd(),
1806                leaf.as_ptr(),
1807                libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1808                0o600,
1809            )
1810        };
1811        if fd >= 0 {
1812            break fd;
1813        }
1814        let error = std::io::Error::last_os_error();
1815        if error.kind() == std::io::ErrorKind::Interrupted
1816            || (error.kind() == std::io::ErrorKind::NotFound && retries < 8)
1817        {
1818            retries = retries.saturating_add(1);
1819            std::thread::yield_now();
1820            continue;
1821        }
1822        return Err(error);
1823    };
1824    let file = unsafe { File::from_raw_fd(fd) };
1825    if !file.metadata()?.is_file() {
1826        return Err(std::io::Error::new(
1827            std::io::ErrorKind::PermissionDenied,
1828            "refusing to lock a non-regular file",
1829        ));
1830    }
1831    loop {
1832        if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } == 0 {
1833            break;
1834        }
1835        let error = std::io::Error::last_os_error();
1836        if error.kind() != std::io::ErrorKind::Interrupted {
1837            return Err(error);
1838        }
1839    }
1840    Ok(file)
1841}
1842
1843#[cfg(windows)]
1844pub(crate) fn lock_exclusive_beneath(root: &File, relative: &Path) -> std::io::Result<File> {
1845    windows_fs::lock_beneath(root, relative)
1846}
1847
1848#[cfg(not(any(unix, windows)))]
1849pub(crate) fn lock_exclusive_beneath(_root: &File, _relative: &Path) -> std::io::Result<File> {
1850    Err(secure_filesystem_unsupported())
1851}
1852
1853/// Open a directory beneath a held root without following any component.
1854#[cfg(unix)]
1855pub(crate) fn open_directory_beneath(
1856    root: &File,
1857    relative: &Path,
1858    create: bool,
1859) -> std::io::Result<File> {
1860    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1861
1862    if relative.is_absolute()
1863        || relative.components().any(|component| {
1864            matches!(
1865                component,
1866                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1867            )
1868        })
1869    {
1870        return Err(std::io::Error::new(
1871            std::io::ErrorKind::PermissionDenied,
1872            "store capability requires a contained relative directory",
1873        ));
1874    }
1875    let mut directory = root.try_clone()?;
1876    for component in relative.components() {
1877        let Component::Normal(name) = component else {
1878            continue;
1879        };
1880        let name = c_name(name)?;
1881        if create {
1882            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
1883            if made != 0 {
1884                let error = std::io::Error::last_os_error();
1885                if error.raw_os_error() != Some(libc::EEXIST) {
1886                    return Err(error);
1887                }
1888            }
1889        }
1890        let fd = unsafe {
1891            libc::openat(
1892                directory.as_raw_fd(),
1893                name.as_ptr(),
1894                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1895            )
1896        };
1897        if fd < 0 {
1898            return Err(std::io::Error::last_os_error());
1899        }
1900        directory = unsafe { File::from_raw_fd(fd) };
1901        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
1902            return Err(std::io::Error::new(
1903                std::io::ErrorKind::PermissionDenied,
1904                "path crosses a nested db.md store boundary",
1905            ));
1906        }
1907    }
1908    Ok(directory)
1909}
1910
1911#[cfg(windows)]
1912pub(crate) fn open_directory_beneath(
1913    root: &File,
1914    relative: &Path,
1915    create: bool,
1916) -> std::io::Result<File> {
1917    windows_fs::open_directory_beneath(root, relative, create)
1918}
1919
1920#[cfg(not(any(unix, windows)))]
1921pub(crate) fn open_directory_beneath(
1922    _root: &File,
1923    _relative: &Path,
1924    _create: bool,
1925) -> std::io::Result<File> {
1926    Err(secure_filesystem_unsupported())
1927}
1928
1929pub(crate) fn directory_exists_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
1930    match open_directory_beneath(root, relative, false) {
1931        Ok(_) => Ok(true),
1932        Err(error)
1933            if matches!(
1934                error.kind(),
1935                std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1936            ) =>
1937        {
1938            Ok(false)
1939        }
1940        Err(error) => Err(error),
1941    }
1942}
1943
1944/// Confirm that every component of a relative regular-file path has the exact
1945/// byte spelling present on disk. This keeps validation platform-independent
1946/// on case-insensitive filesystems without canonicalizing the mutable root
1947/// pathname.
1948#[cfg(unix)]
1949pub(crate) fn path_case_matches_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
1950    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1951    use std::os::unix::ffi::OsStrExt as _;
1952
1953    if relative.is_absolute()
1954        || relative.components().any(|component| {
1955            matches!(
1956                component,
1957                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1958            )
1959        })
1960    {
1961        return Ok(false);
1962    }
1963    let components: Vec<_> = relative
1964        .components()
1965        .filter_map(|component| match component {
1966            Component::Normal(name) => Some(name),
1967            _ => None,
1968        })
1969        .collect();
1970    let mut directory = root.try_clone()?;
1971    for (index, name) in components.iter().enumerate() {
1972        let scan_fd = unsafe {
1973            libc::openat(
1974                directory.as_raw_fd(),
1975                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
1976                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1977            )
1978        };
1979        if scan_fd < 0 {
1980            return Err(std::io::Error::last_os_error());
1981        }
1982        let stream = unsafe { libc::fdopendir(scan_fd) };
1983        if stream.is_null() {
1984            let error = std::io::Error::last_os_error();
1985            unsafe {
1986                libc::close(scan_fd);
1987            }
1988            return Err(error);
1989        }
1990        let mut exact = false;
1991        loop {
1992            let entry = unsafe { libc::readdir(stream) };
1993            if entry.is_null() {
1994                break;
1995            }
1996            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1997            if bytes == name.as_bytes() {
1998                exact = true;
1999                break;
2000            }
2001        }
2002        if unsafe { libc::closedir(stream) } != 0 {
2003            return Err(std::io::Error::last_os_error());
2004        }
2005        if !exact {
2006            return Ok(false);
2007        }
2008        if index + 1 == components.len() {
2009            return directory_contains_exact_regular(&directory, name);
2010        }
2011        let fd = unsafe {
2012            libc::openat(
2013                directory.as_raw_fd(),
2014                c_name(name)?.as_ptr(),
2015                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2016            )
2017        };
2018        if fd < 0 {
2019            return Ok(false);
2020        }
2021        directory = unsafe { File::from_raw_fd(fd) };
2022        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
2023            return Ok(false);
2024        }
2025    }
2026    Ok(false)
2027}
2028
2029#[cfg(windows)]
2030pub(crate) fn path_case_matches_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
2031    if relative.is_absolute() {
2032        return Ok(false);
2033    }
2034    let components = relative
2035        .components()
2036        .filter_map(|component| match component {
2037            Component::Normal(name) => Some(name.to_os_string()),
2038            _ => None,
2039        })
2040        .collect::<Vec<_>>();
2041    let mut directory = root.try_clone()?;
2042    for (index, name) in components.iter().enumerate() {
2043        let path = windows_fs::directory_path(&directory)?;
2044        let exact = std::fs::read_dir(&path)?
2045            .collect::<Result<Vec<_>, _>>()?
2046            .into_iter()
2047            .find(|entry| entry.file_name() == *name);
2048        let Some(entry) = exact else { return Ok(false) };
2049        if index + 1 == components.len() {
2050            let attrs = windows_fs::entry_attributes(&entry.path())?;
2051            return Ok(attrs
2052                & (windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT
2053                    | windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY)
2054                == 0);
2055        }
2056        directory = windows_fs::open_directory(&entry.path())?;
2057        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
2058            return Ok(false);
2059        }
2060    }
2061    Ok(false)
2062}
2063
2064#[cfg(not(any(unix, windows)))]
2065pub(crate) fn path_case_matches_beneath(_root: &File, _relative: &Path) -> std::io::Result<bool> {
2066    Err(secure_filesystem_unsupported())
2067}
2068
2069/// Immediate no-follow child directories beneath a held root.
2070#[cfg(unix)]
2071pub(crate) fn directory_names_beneath(
2072    root: &File,
2073    relative: &Path,
2074) -> std::io::Result<Vec<std::ffi::OsString>> {
2075    use std::os::fd::AsRawFd as _;
2076    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
2077
2078    let directory = open_directory_beneath(root, relative, false)?;
2079    let scan_fd = unsafe {
2080        libc::openat(
2081            directory.as_raw_fd(),
2082            c_name(std::ffi::OsStr::new("."))?.as_ptr(),
2083            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2084        )
2085    };
2086    if scan_fd < 0 {
2087        return Err(std::io::Error::last_os_error());
2088    }
2089    let stream = unsafe { libc::fdopendir(scan_fd) };
2090    if stream.is_null() {
2091        let error = std::io::Error::last_os_error();
2092        unsafe {
2093            libc::close(scan_fd);
2094        }
2095        return Err(error);
2096    }
2097    let mut names = Vec::new();
2098    loop {
2099        let entry = unsafe { libc::readdir(stream) };
2100        if entry.is_null() {
2101            break;
2102        }
2103        let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
2104        if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
2105            continue;
2106        }
2107        let name = std::ffi::OsString::from_vec(bytes.to_vec());
2108        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2109        if unsafe {
2110            libc::fstatat(
2111                directory.as_raw_fd(),
2112                c_name(&name)?.as_ptr(),
2113                &mut stat,
2114                libc::AT_SYMLINK_NOFOLLOW,
2115            )
2116        } == 0
2117            && (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR
2118        {
2119            let child = open_directory_beneath(root, &relative.join(&name), false)?;
2120            if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
2121                names.push(name);
2122            }
2123        }
2124    }
2125    if unsafe { libc::closedir(stream) } != 0 {
2126        return Err(std::io::Error::last_os_error());
2127    }
2128    names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
2129    Ok(names)
2130}
2131
2132#[cfg(windows)]
2133pub(crate) fn directory_names_beneath(
2134    root: &File,
2135    relative: &Path,
2136) -> std::io::Result<Vec<std::ffi::OsString>> {
2137    let directory = windows_fs::open_directory_beneath(root, relative, false)?;
2138    let path = windows_fs::directory_path(&directory)?;
2139    let mut names = Vec::new();
2140    for entry in std::fs::read_dir(path)? {
2141        let entry = entry?;
2142        let name = entry.file_name();
2143        if name.to_string_lossy().starts_with('.') {
2144            continue;
2145        }
2146        let attrs = windows_fs::entry_attributes(&entry.path())?;
2147        if attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT == 0
2148            && attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY != 0
2149        {
2150            let child = windows_fs::open_directory(&entry.path())?;
2151            if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
2152                names.push(name);
2153            }
2154        }
2155    }
2156    names.sort();
2157    Ok(names)
2158}
2159
2160#[cfg(not(any(unix, windows)))]
2161pub(crate) fn directory_names_beneath(
2162    _root: &File,
2163    _relative: &Path,
2164) -> std::io::Result<Vec<std::ffi::OsString>> {
2165    Err(secure_filesystem_unsupported())
2166}
2167
2168#[cfg(not(any(unix, windows)))]
2169pub(crate) fn write_atomic_beneath(
2170    _root: &File,
2171    _relative: &Path,
2172    _bytes: &[u8],
2173    _create_new: bool,
2174    _durable: bool,
2175) -> std::io::Result<()> {
2176    Err(secure_filesystem_unsupported())
2177}
2178
2179#[cfg(not(any(unix, windows)))]
2180pub(crate) fn write_private_atomic_beneath(
2181    _root: &File,
2182    _relative: &Path,
2183    _bytes: &[u8],
2184    _create_new: bool,
2185) -> std::io::Result<()> {
2186    Err(secure_filesystem_unsupported())
2187}
2188
2189#[cfg(unix)]
2190pub(crate) fn rename_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
2191    use std::os::fd::AsRawFd as _;
2192
2193    let (old_parent, old_leaf) = open_parent_beneath(root, old, false)?;
2194    let (new_parent, new_leaf) = open_parent_beneath(root, new, true)?;
2195    let mut source_stat: libc::stat = unsafe { std::mem::zeroed() };
2196    if unsafe {
2197        libc::fstatat(
2198            old_parent.as_raw_fd(),
2199            old_leaf.as_ptr(),
2200            &mut source_stat,
2201            libc::AT_SYMLINK_NOFOLLOW,
2202        )
2203    } != 0
2204    {
2205        return Err(std::io::Error::last_os_error());
2206    }
2207    if (source_stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
2208        return Err(std::io::Error::new(
2209            std::io::ErrorKind::PermissionDenied,
2210            "refusing to rename a non-regular file",
2211        ));
2212    }
2213    renameat_noreplace(
2214        old_parent.as_raw_fd(),
2215        &old_leaf,
2216        new_parent.as_raw_fd(),
2217        &new_leaf,
2218    )?;
2219    old_parent.sync_all()?;
2220    new_parent.sync_all()?;
2221    Ok(())
2222}
2223
2224#[cfg(windows)]
2225pub(crate) fn rename_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
2226    windows_fs::rename_beneath(root, old, new)
2227}
2228
2229#[cfg(windows)]
2230pub(crate) fn rename_directory_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
2231    windows_fs::rename_directory_beneath(root, old, new)
2232}
2233
2234#[cfg(not(any(unix, windows)))]
2235pub(crate) fn rename_beneath(_root: &File, _old: &Path, _new: &Path) -> std::io::Result<()> {
2236    Err(secure_filesystem_unsupported())
2237}
2238
2239#[cfg(unix)]
2240pub(crate) fn remove_file_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2241    use std::os::fd::AsRawFd as _;
2242
2243    let (parent, leaf) = open_parent_beneath(root, relative, false)?;
2244    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2245    if unsafe {
2246        libc::fstatat(
2247            parent.as_raw_fd(),
2248            leaf.as_ptr(),
2249            &mut stat,
2250            libc::AT_SYMLINK_NOFOLLOW,
2251        )
2252    } != 0
2253    {
2254        return Err(std::io::Error::last_os_error());
2255    }
2256    if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
2257        return Err(std::io::Error::new(
2258            std::io::ErrorKind::PermissionDenied,
2259            "refusing to remove a non-regular file",
2260        ));
2261    }
2262    if unsafe { libc::unlinkat(parent.as_raw_fd(), leaf.as_ptr(), 0) } != 0 {
2263        return Err(std::io::Error::last_os_error());
2264    }
2265    parent.sync_all()
2266}
2267
2268#[cfg(windows)]
2269pub(crate) fn remove_file_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2270    windows_fs::remove_file_beneath(root, relative)
2271}
2272
2273/// Remove one private subtree beneath a held store root without following any
2274/// symlink. This is reserved for disposable control state such as completed
2275/// conflict bundles; riding store data uses explicit file operations instead.
2276#[cfg(unix)]
2277pub(crate) fn remove_tree_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2278    fn remove_at(parent: &File, name: &std::ffi::CStr) -> std::io::Result<()> {
2279        use std::os::fd::{AsRawFd as _, FromRawFd as _};
2280        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2281        if unsafe {
2282            libc::fstatat(
2283                parent.as_raw_fd(),
2284                name.as_ptr(),
2285                &mut stat,
2286                libc::AT_SYMLINK_NOFOLLOW,
2287            )
2288        } != 0
2289        {
2290            return Err(std::io::Error::last_os_error());
2291        }
2292        if (stat.st_mode & libc::S_IFMT) != libc::S_IFDIR {
2293            return Err(std::io::Error::new(
2294                std::io::ErrorKind::PermissionDenied,
2295                "private cleanup target is not a directory",
2296            ));
2297        }
2298        let fd = unsafe {
2299            libc::openat(
2300                parent.as_raw_fd(),
2301                name.as_ptr(),
2302                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2303            )
2304        };
2305        if fd < 0 {
2306            return Err(std::io::Error::last_os_error());
2307        }
2308        let directory = unsafe { File::from_raw_fd(fd) };
2309        let scan_fd = unsafe { libc::dup(directory.as_raw_fd()) };
2310        if scan_fd < 0 {
2311            return Err(std::io::Error::last_os_error());
2312        }
2313        let stream = unsafe { libc::fdopendir(scan_fd) };
2314        if stream.is_null() {
2315            unsafe { libc::close(scan_fd) };
2316            return Err(std::io::Error::last_os_error());
2317        }
2318        let mut names = Vec::new();
2319        loop {
2320            let entry = unsafe { libc::readdir(stream) };
2321            if entry.is_null() {
2322                break;
2323            }
2324            let raw = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) };
2325            if !matches!(raw.to_bytes(), b"." | b"..") {
2326                names.push(raw.to_owned());
2327            }
2328        }
2329        if unsafe { libc::closedir(stream) } != 0 {
2330            return Err(std::io::Error::last_os_error());
2331        }
2332        for child in names {
2333            let mut child_stat: libc::stat = unsafe { std::mem::zeroed() };
2334            if unsafe {
2335                libc::fstatat(
2336                    directory.as_raw_fd(),
2337                    child.as_ptr(),
2338                    &mut child_stat,
2339                    libc::AT_SYMLINK_NOFOLLOW,
2340                )
2341            } != 0
2342            {
2343                return Err(std::io::Error::last_os_error());
2344            }
2345            if (child_stat.st_mode & libc::S_IFMT) == libc::S_IFDIR {
2346                remove_at(&directory, &child)?;
2347            } else if unsafe { libc::unlinkat(directory.as_raw_fd(), child.as_ptr(), 0) } != 0 {
2348                return Err(std::io::Error::last_os_error());
2349            }
2350        }
2351        drop(directory);
2352        if unsafe { libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), libc::AT_REMOVEDIR) } != 0 {
2353            return Err(std::io::Error::last_os_error());
2354        }
2355        parent.sync_all()
2356    }
2357
2358    let (parent, leaf) = open_parent_beneath(root, relative, false)?;
2359    remove_at(&parent, &leaf)
2360}
2361
2362#[cfg(windows)]
2363pub(crate) fn remove_tree_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2364    windows_fs::remove_tree_beneath(root, relative)
2365}
2366
2367#[cfg(not(any(unix, windows)))]
2368pub(crate) fn remove_file_beneath(_root: &File, _relative: &Path) -> std::io::Result<()> {
2369    Err(secure_filesystem_unsupported())
2370}
2371
2372#[cfg(not(any(unix, windows)))]
2373pub(crate) fn remove_tree_beneath(_root: &File, _relative: &Path) -> std::io::Result<()> {
2374    Err(secure_filesystem_unsupported())
2375}
2376
2377#[cfg(unix)]
2378fn write_atomic_unix(
2379    path: &Path,
2380    bytes: &[u8],
2381    create_new: bool,
2382    durable: bool,
2383) -> std::io::Result<()> {
2384    let (directory, leaf) = open_parent_unix(path, true)?;
2385    write_atomic_at(directory, leaf, bytes, create_new, durable, None)
2386}
2387
2388#[cfg(unix)]
2389fn write_atomic_at(
2390    directory: File,
2391    leaf: std::ffi::CString,
2392    bytes: &[u8],
2393    create_new: bool,
2394    durable: bool,
2395    exact_mode: Option<libc::mode_t>,
2396) -> std::io::Result<()> {
2397    use std::os::fd::{AsRawFd as _, FromRawFd as _};
2398
2399    static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
2400    let pid = std::process::id();
2401    let nanos = SystemTime::now()
2402        .duration_since(UNIX_EPOCH)
2403        .map(|duration| duration.as_nanos())
2404        .unwrap_or(0);
2405    let mut allocated = None;
2406    for _ in 0..128 {
2407        let name = std::ffi::OsString::from(format!(
2408            ".dbmd.tmp.{pid}.{nanos}.{}",
2409            TMP_SEQ.fetch_add(1, Ordering::Relaxed)
2410        ));
2411        let name = c_name(&name)?;
2412        let fd = unsafe {
2413            libc::openat(
2414                directory.as_raw_fd(),
2415                name.as_ptr(),
2416                libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2417                exact_mode.unwrap_or(0o666) as libc::c_uint,
2418            )
2419        };
2420        if fd >= 0 {
2421            allocated = Some((name, unsafe { File::from_raw_fd(fd) }));
2422            break;
2423        }
2424        let error = std::io::Error::last_os_error();
2425        if error.kind() != std::io::ErrorKind::AlreadyExists {
2426            return Err(error);
2427        }
2428    }
2429    let (temp, mut file) = allocated.ok_or_else(|| {
2430        std::io::Error::new(
2431            std::io::ErrorKind::AlreadyExists,
2432            "could not allocate secure temporary file",
2433        )
2434    })?;
2435    let cleanup =
2436        |name: &std::ffi::CStr| unsafe { libc::unlinkat(directory.as_raw_fd(), name.as_ptr(), 0) };
2437    let write_result = file.write_all(bytes).and_then(|_| {
2438        if durable {
2439            file.sync_all()
2440        } else {
2441            file.flush()
2442        }
2443    });
2444    if let Err(error) = write_result {
2445        let _ = cleanup(&temp);
2446        return Err(error);
2447    }
2448
2449    // Preserve an existing regular destination's exact Unix mode. A symlink is
2450    // never dereferenced; chmod failure aborts rather than silently widening.
2451    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2452    let destination_stat = unsafe {
2453        libc::fstatat(
2454            directory.as_raw_fd(),
2455            leaf.as_ptr(),
2456            &mut stat,
2457            libc::AT_SYMLINK_NOFOLLOW,
2458        )
2459    };
2460    if destination_stat == 0 {
2461        if (stat.st_mode & libc::S_IFMT) == libc::S_IFLNK {
2462            let _ = cleanup(&temp);
2463            return Err(std::io::Error::new(
2464                std::io::ErrorKind::PermissionDenied,
2465                "refusing a symlink destination",
2466            ));
2467        }
2468        let mode = exact_mode.unwrap_or(stat.st_mode & 0o7777);
2469        if unsafe { libc::fchmod(file.as_raw_fd(), mode) } != 0 {
2470            let error = std::io::Error::last_os_error();
2471            let _ = cleanup(&temp);
2472            return Err(error);
2473        }
2474    } else {
2475        let error = std::io::Error::last_os_error();
2476        if error.kind() != std::io::ErrorKind::NotFound {
2477            let _ = cleanup(&temp);
2478            return Err(error);
2479        }
2480        if let Some(mode) = exact_mode {
2481            if unsafe { libc::fchmod(file.as_raw_fd(), mode) } != 0 {
2482                let error = std::io::Error::last_os_error();
2483                let _ = cleanup(&temp);
2484                return Err(error);
2485            }
2486        }
2487    }
2488    drop(file);
2489
2490    let installed = if create_new {
2491        unsafe {
2492            libc::linkat(
2493                directory.as_raw_fd(),
2494                temp.as_ptr(),
2495                directory.as_raw_fd(),
2496                leaf.as_ptr(),
2497                0,
2498            )
2499        }
2500    } else {
2501        unsafe {
2502            libc::renameat(
2503                directory.as_raw_fd(),
2504                temp.as_ptr(),
2505                directory.as_raw_fd(),
2506                leaf.as_ptr(),
2507            )
2508        }
2509    };
2510    if installed != 0 {
2511        let error = std::io::Error::last_os_error();
2512        let _ = cleanup(&temp);
2513        return Err(error);
2514    }
2515    if create_new {
2516        let _ = cleanup(&temp);
2517    }
2518    if durable {
2519        directory.sync_all()?;
2520    }
2521    Ok(())
2522}
2523
2524/// Drop-based cleanup for the hidden temp file `write_atomic` creates. While
2525/// armed, dropping the guard removes `path`. [`TempGuard::disarm`] is called
2526/// only after a successful rename, or after a successful temp-link cleanup in
2527/// [`write_atomic_new`], so the final destination is never touched.
2528#[cfg(test)]
2529struct TempGuard {
2530    path: PathBuf,
2531    armed: bool,
2532}
2533
2534#[cfg(test)]
2535impl TempGuard {
2536    /// Stop cleaning up `path` on drop — used once the temp has been renamed
2537    /// into place and is no longer a stray temp file.
2538    fn disarm(&mut self) {
2539        self.armed = false;
2540    }
2541}
2542
2543#[cfg(test)]
2544impl Drop for TempGuard {
2545    fn drop(&mut self) {
2546        // Best-effort cleanup if an error path bailed out before the rename.
2547        if self.armed {
2548            let _ = fs::remove_file(&self.path);
2549        }
2550    }
2551}
2552
2553/// Create a uniquely-named temp file in `dir` with `create_new` (never clobbers
2554/// a predictable name), retrying on the vanishingly-rare collision. The name is
2555/// hidden (`.`-prefixed) and tagged with pid + nanos + a process-wide counter so
2556/// concurrent writers in the same directory never pick the same path. Returns the
2557/// open handle plus an armed [`TempGuard`] so any early return cleans up the temp.
2558#[cfg(test)]
2559fn create_temp_file(dir: &Path, file_name: &str) -> std::io::Result<(File, TempGuard)> {
2560    static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
2561    let pid = std::process::id();
2562    let nanos = SystemTime::now()
2563        .duration_since(UNIX_EPOCH)
2564        .map(|d| d.as_nanos())
2565        .unwrap_or(0);
2566
2567    for _ in 0..128 {
2568        let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);
2569        let tmp = dir.join(format!(".{file_name}.tmp.{pid}.{nanos}.{seq}"));
2570        match OpenOptions::new().write(true).create_new(true).open(&tmp) {
2571            Ok(file) => {
2572                return Ok((
2573                    file,
2574                    TempGuard {
2575                        path: tmp,
2576                        armed: true,
2577                    },
2578                ))
2579            }
2580            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
2581            Err(e) => return Err(e),
2582        }
2583    }
2584
2585    Err(std::io::Error::new(
2586        std::io::ErrorKind::AlreadyExists,
2587        "could not allocate a unique dbmd temp file",
2588    ))
2589}
2590
2591#[cfg(test)]
2592mod tests {
2593    use super::*;
2594    use tempfile::TempDir;
2595
2596    #[test]
2597    fn write_atomic_creates_then_replaces_durably() {
2598        let tmp = TempDir::new().unwrap();
2599        let target = tmp.path().join("sub").join("file.txt"); // parent missing
2600
2601        write_atomic(&target, b"first").unwrap();
2602        assert_eq!(std::fs::read(&target).unwrap(), b"first");
2603
2604        // Replace in place — content swaps, no temp files left behind.
2605        write_atomic(&target, b"second").unwrap();
2606        assert_eq!(std::fs::read(&target).unwrap(), b"second");
2607
2608        let leftovers: Vec<_> = std::fs::read_dir(target.parent().unwrap())
2609            .unwrap()
2610            .filter_map(|e| e.ok())
2611            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
2612            .collect();
2613        assert!(leftovers.is_empty(), "no temp files may be left behind");
2614    }
2615
2616    #[test]
2617    fn write_atomic_is_byte_exact_including_empty() {
2618        let tmp = TempDir::new().unwrap();
2619        let target = tmp.path().join("empty.txt");
2620        write_atomic(&target, b"").unwrap();
2621        assert_eq!(std::fs::read(&target).unwrap(), b"");
2622    }
2623
2624    #[test]
2625    fn write_atomic_new_creates_but_refuses_existing() {
2626        let tmp = TempDir::new().unwrap();
2627        let target = tmp.path().join("sub").join("file.txt");
2628
2629        write_atomic_new(&target, b"first").unwrap();
2630        assert_eq!(std::fs::read(&target).unwrap(), b"first");
2631
2632        let err = write_atomic_new(&target, b"second").unwrap_err();
2633        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
2634        assert_eq!(
2635            std::fs::read(&target).unwrap(),
2636            b"first",
2637            "create-new failure must leave the existing destination untouched"
2638        );
2639
2640        assert_no_temp_files(target.parent().unwrap());
2641    }
2642
2643    #[test]
2644    fn write_atomic_new_allows_only_one_concurrent_creator() {
2645        use std::sync::{Arc, Barrier};
2646
2647        for round in 0..40 {
2648            let tmp = TempDir::new().unwrap();
2649            let target = tmp.path().join("file.txt");
2650            let barrier = Arc::new(Barrier::new(8));
2651
2652            let handles: Vec<_> = (0..8)
2653                .map(|i| {
2654                    let target = target.clone();
2655                    let barrier = Arc::clone(&barrier);
2656                    std::thread::spawn(move || {
2657                        let payload = format!("payload-{i}");
2658                        barrier.wait();
2659                        let result = write_atomic_new(&target, payload.as_bytes())
2660                            .map(|_| ())
2661                            .map_err(|e| e.kind());
2662                        (payload, result)
2663                    })
2664                })
2665                .collect();
2666
2667            let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
2668            let winners: Vec<_> = results
2669                .iter()
2670                .filter_map(|(payload, result)| result.is_ok().then_some(payload))
2671                .collect();
2672            let already_exists = results
2673                .iter()
2674                .filter(|(_, result)| {
2675                    matches!(result, Err(kind) if *kind == std::io::ErrorKind::AlreadyExists)
2676                })
2677                .count();
2678
2679            assert_eq!(
2680                winners.len(),
2681                1,
2682                "round {round}: exactly one creator may win, got {results:?}"
2683            );
2684            assert_eq!(
2685                already_exists, 7,
2686                "round {round}: every losing creator must get AlreadyExists, got {results:?}"
2687            );
2688
2689            let written = std::fs::read_to_string(&target).unwrap();
2690            assert_eq!(
2691                written, *winners[0],
2692                "round {round}: destination must contain the winner's payload"
2693            );
2694            assert_no_temp_files(tmp.path());
2695        }
2696    }
2697
2698    /// Regression for finding #22: an early return between temp-file creation and
2699    /// a successful rename (e.g. `write_all`/`sync_all` failing under ENOSPC/EIO)
2700    /// must NOT leave the hidden temp file orphaned in the data directory.
2701    ///
2702    /// Pre-fix, `create_temp_file` handed back a bare `PathBuf` with no `Drop`
2703    /// cleanup, so dropping it without a rename — exactly what `?` does on a
2704    /// write/sync failure — left the temp on disk. This reconstructs that path by
2705    /// dropping the guard without renaming and asserting the temp is gone.
2706    #[test]
2707    fn regression_armed_guard_removes_temp_on_early_drop() {
2708        let dir = TempDir::new().unwrap();
2709        let (file, guard) = create_temp_file(dir.path(), "file.txt").unwrap();
2710        let tmp_path = guard.path.clone();
2711        assert!(
2712            tmp_path.exists(),
2713            "temp file should exist after create_temp_file"
2714        );
2715
2716        // Simulate a write/sync failure bailing out before the rename: the file
2717        // handle and the (still-armed) guard go out of scope without a rename.
2718        drop(file);
2719        drop(guard);
2720
2721        assert!(
2722            !tmp_path.exists(),
2723            "armed guard must remove the orphaned temp file on early drop"
2724        );
2725        // No stray `.tmp.` files left in the directory.
2726        let leftovers: Vec<_> = std::fs::read_dir(dir.path())
2727            .unwrap()
2728            .filter_map(|e| e.ok())
2729            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
2730            .collect();
2731        assert!(leftovers.is_empty(), "no temp files may be left behind");
2732    }
2733
2734    /// Once disarmed (after a successful rename) the guard must NOT delete the
2735    /// path it was tracking — otherwise it would clobber the renamed destination.
2736    #[test]
2737    fn regression_disarmed_guard_leaves_file_intact() {
2738        let dir = TempDir::new().unwrap();
2739        let (file, mut guard) = create_temp_file(dir.path(), "kept.txt").unwrap();
2740        drop(file);
2741        let kept = guard.path.clone();
2742
2743        guard.disarm();
2744        drop(guard);
2745
2746        assert!(
2747            kept.exists(),
2748            "disarmed guard must leave the renamed destination untouched"
2749        );
2750    }
2751
2752    fn assert_no_temp_files(dir: &Path) {
2753        let leftovers: Vec<_> = std::fs::read_dir(dir)
2754            .unwrap()
2755            .filter_map(|e| e.ok())
2756            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
2757            .collect();
2758        assert!(leftovers.is_empty(), "no temp files may be left behind");
2759    }
2760
2761    /// Regression: rewriting an existing file via `write_atomic` must PRESERVE
2762    /// its permission bits. Pre-fix the temp file's default mode (0644) replaced
2763    /// a deliberately-restricted destination (0600) on every rewrite — a quiet
2764    /// permission-widening on user data. A first create still uses the default
2765    /// mode (there is no destination mode to copy).
2766    #[cfg(unix)]
2767    #[test]
2768    fn write_atomic_preserves_existing_destination_permissions() {
2769        use std::os::unix::fs::PermissionsExt;
2770
2771        let tmp = TempDir::new().unwrap();
2772        let target = tmp.path().join("private.md");
2773
2774        // Create, then restrict to 0600.
2775        write_atomic(&target, b"secret v1").unwrap();
2776        std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
2777        let before = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
2778        assert_eq!(before, 0o600, "fixture must start at 0600");
2779
2780        // Rewrite in place: the 0600 mode must survive (not reset to 0644).
2781        write_atomic(&target, b"secret v2").unwrap();
2782        let after = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
2783        assert_eq!(
2784            after, 0o600,
2785            "write_atomic must preserve the destination's 0600 mode, got {after:o}"
2786        );
2787        assert_eq!(std::fs::read(&target).unwrap(), b"secret v2");
2788    }
2789
2790    /// Exploit regression for the containment/write TOCTOU: a caller may have
2791    /// validated `store/records/safe.md`, then an attacker replaces `records`
2792    /// with a symlink to an external directory before the atomic writer opens
2793    /// it. Every ancestor is opened with `openat(O_DIRECTORY|O_NOFOLLOW)`, so
2794    /// the write is refused and the outside victim is byte-identical.
2795    #[cfg(unix)]
2796    #[test]
2797    fn write_atomic_refuses_symlinked_ancestor_without_touching_external_file() {
2798        use std::os::unix::fs::symlink;
2799
2800        let sandbox = TempDir::new().unwrap();
2801        let store = sandbox.path().join("store");
2802        let external = sandbox.path().join("external");
2803        std::fs::create_dir_all(store.join("records")).unwrap();
2804        std::fs::create_dir_all(&external).unwrap();
2805        let victim = external.join("safe.md");
2806        std::fs::write(&victim, b"external secret").unwrap();
2807
2808        std::fs::remove_dir(store.join("records")).unwrap();
2809        symlink(&external, store.join("records")).unwrap();
2810
2811        let error = write_atomic(&store.join("records/safe.md"), b"attacker output")
2812            .expect_err("a symlinked ancestor must fail closed");
2813        assert!(
2814            matches!(
2815                error.raw_os_error(),
2816                Some(code) if code == libc::ELOOP || code == libc::ENOTDIR
2817            ),
2818            "expected no-follow refusal, got {error:?}"
2819        );
2820        assert_eq!(std::fs::read(&victim).unwrap(), b"external secret");
2821    }
2822
2823    /// A leaf swap is equally unsafe for reads: after containment validation an
2824    /// attacker can replace the selected record with a symlink to a secret.
2825    /// `read_bounded_nofollow` opens the leaf once with `O_NOFOLLOW`, then sizes
2826    /// and reads that same descriptor, so no external bytes are returned.
2827    #[cfg(unix)]
2828    #[test]
2829    fn bounded_read_refuses_symlink_leaf() {
2830        use std::os::unix::fs::symlink;
2831
2832        let sandbox = TempDir::new().unwrap();
2833        let external = sandbox.path().join("secret");
2834        std::fs::write(&external, b"do not exfiltrate").unwrap();
2835        let selected = sandbox.path().join("selected.md");
2836        symlink(&external, &selected).unwrap();
2837
2838        let error = read_bounded_nofollow(&selected, 1024)
2839            .expect_err("the no-follow reader must reject a symlink leaf");
2840        assert_eq!(error.raw_os_error(), Some(libc::ELOOP));
2841    }
2842
2843    /// If the file grows after its descriptor metadata was read, the bounded
2844    /// descriptor read still enforces the actual byte ceiling (`take(max+1)`),
2845    /// rather than trusting the stale size.
2846    #[test]
2847    fn bounded_read_rejects_content_over_limit() {
2848        let sandbox = TempDir::new().unwrap();
2849        let selected = sandbox.path().join("selected.md");
2850        std::fs::write(&selected, b"12345").unwrap();
2851        let error = read_bounded_nofollow(&selected, 4)
2852            .expect_err("actual content above the cap must be refused");
2853        assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
2854    }
2855
2856    #[cfg(unix)]
2857    #[test]
2858    fn held_directory_reader_survives_ancestor_swap_without_disclosure() {
2859        use std::os::unix::fs::symlink;
2860
2861        let sandbox = TempDir::new().unwrap();
2862        let store = sandbox.path().join("store");
2863        let contacts = store.join("records/contacts");
2864        std::fs::create_dir_all(&contacts).unwrap();
2865        std::fs::write(contacts.join("selected.md"), b"owned").unwrap();
2866        let outside = sandbox.path().join("outside");
2867        std::fs::create_dir_all(&outside).unwrap();
2868        std::fs::write(outside.join("selected.md"), b"secret").unwrap();
2869
2870        let mut reader = BoundedDirReader::new(&store).unwrap();
2871        let relative = Path::new("records/contacts/selected.md");
2872        assert_eq!(reader.read(relative, 1024).unwrap(), b"owned");
2873
2874        let detached = store.join("records/contacts-detached");
2875        std::fs::rename(&contacts, &detached).unwrap();
2876        symlink(&outside, &contacts).unwrap();
2877
2878        assert_eq!(
2879            reader.read(relative, 1024).unwrap(),
2880            b"owned",
2881            "the cached directory capability must not reopen the swapped pathname"
2882        );
2883    }
2884
2885    #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
2886    #[test]
2887    fn rename_nofollow_is_atomic_no_replace() {
2888        let sandbox = TempDir::new().unwrap();
2889        let source = sandbox.path().join("source.md");
2890        let destination = sandbox.path().join("destination.md");
2891        std::fs::write(&source, b"source").unwrap();
2892        std::fs::write(&destination, b"existing").unwrap();
2893
2894        let error = rename_nofollow(&source, &destination)
2895            .expect_err("a destination created after preflight must not be clobbered");
2896        assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
2897        assert_eq!(std::fs::read(&source).unwrap(), b"source");
2898        assert_eq!(std::fs::read(&destination).unwrap(), b"existing");
2899    }
2900
2901    #[cfg(windows)]
2902    #[test]
2903    fn windows_atomic_replace_preserves_readonly_and_exact_bytes() {
2904        let sandbox = TempDir::new().unwrap();
2905        let target = sandbox.path().join("readonly.md");
2906        std::fs::write(&target, b"old").unwrap();
2907        let mut permissions = std::fs::metadata(&target).unwrap().permissions();
2908        permissions.set_readonly(true);
2909        std::fs::set_permissions(&target, permissions).unwrap();
2910
2911        write_atomic(&target, b"replacement").unwrap();
2912        assert_eq!(std::fs::read(&target).unwrap(), b"replacement");
2913        assert!(std::fs::metadata(&target).unwrap().permissions().readonly());
2914    }
2915
2916    #[cfg(windows)]
2917    #[test]
2918    fn windows_reader_refuses_a_reparse_leaf() {
2919        use std::os::windows::fs::symlink_file;
2920
2921        let sandbox = TempDir::new().unwrap();
2922        let external = sandbox.path().join("external.md");
2923        let selected = sandbox.path().join("selected.md");
2924        std::fs::write(&external, b"secret").unwrap();
2925        symlink_file(&external, &selected).unwrap();
2926        assert!(read_bounded_nofollow(&selected, 1024).is_err());
2927    }
2928
2929    #[cfg(windows)]
2930    #[test]
2931    fn windows_lock_serializes_competing_handles() {
2932        let sandbox = TempDir::new().unwrap();
2933        let root = open_directory_nofollow(sandbox.path()).unwrap();
2934        let first = lock_exclusive_beneath(&root, Path::new("lock")).unwrap();
2935        let root_for_thread = root.try_clone().unwrap();
2936        let (sender, receiver) = std::sync::mpsc::channel();
2937        std::thread::spawn(move || {
2938            sender
2939                .send(lock_exclusive_beneath(&root_for_thread, Path::new("lock")))
2940                .unwrap();
2941        });
2942        assert!(matches!(
2943            receiver.recv_timeout(std::time::Duration::from_millis(100)),
2944            Err(std::sync::mpsc::RecvTimeoutError::Timeout)
2945        ));
2946        drop(first);
2947        assert!(receiver
2948            .recv_timeout(std::time::Duration::from_secs(5))
2949            .unwrap()
2950            .is_ok());
2951    }
2952}