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)
1719}
1720
1721#[cfg(windows)]
1722pub(crate) fn write_atomic_beneath(
1723    root: &File,
1724    relative: &Path,
1725    bytes: &[u8],
1726    create_new: bool,
1727    durable: bool,
1728) -> std::io::Result<()> {
1729    windows_fs::atomic_write_beneath(root, relative, bytes, create_new, durable)
1730}
1731
1732/// Atomically replace a rebuildable file beneath a held root without forcing
1733/// the bytes or directory entry to stable storage.
1734#[cfg(unix)]
1735pub(crate) fn write_atomic_nondurable_beneath(
1736    root: &File,
1737    relative: &Path,
1738    bytes: &[u8],
1739) -> std::io::Result<()> {
1740    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
1741    write_atomic_at(directory, leaf, bytes, false, false)
1742}
1743
1744#[cfg(windows)]
1745pub(crate) fn write_atomic_nondurable_beneath(
1746    root: &File,
1747    relative: &Path,
1748    bytes: &[u8],
1749) -> std::io::Result<()> {
1750    windows_fs::atomic_write_beneath(root, relative, bytes, false, false)
1751}
1752
1753#[cfg(not(any(unix, windows)))]
1754pub(crate) fn write_atomic_nondurable_beneath(
1755    _root: &File,
1756    _relative: &Path,
1757    _bytes: &[u8],
1758) -> std::io::Result<()> {
1759    Err(secure_filesystem_unsupported())
1760}
1761
1762/// Open or create a regular advisory-lock file beneath a held root, then take
1763/// an exclusive `flock` on the exact inode. The parent traversal and leaf open
1764/// are no-follow, so replacing the store's original pathname cannot redirect
1765/// the lock into another tree.
1766#[cfg(unix)]
1767pub(crate) fn lock_exclusive_beneath(root: &File, relative: &Path) -> std::io::Result<File> {
1768    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1769
1770    let (directory, leaf) = open_parent_beneath(root, relative, true)?;
1771    // Darwin can transiently report ENOENT when two creators race on the same
1772    // O_CREAT+O_NOFOLLOW leaf. Retry that lookup race (and EINTR) against the
1773    // same held parent; every successful contender still opens the one shared
1774    // inode and serializes on `flock`.
1775    let mut retries = 0_u8;
1776    let fd = loop {
1777        let fd = unsafe {
1778            libc::openat(
1779                directory.as_raw_fd(),
1780                leaf.as_ptr(),
1781                libc::O_RDWR | libc::O_CREAT | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1782                0o600,
1783            )
1784        };
1785        if fd >= 0 {
1786            break fd;
1787        }
1788        let error = std::io::Error::last_os_error();
1789        if error.kind() == std::io::ErrorKind::Interrupted
1790            || (error.kind() == std::io::ErrorKind::NotFound && retries < 8)
1791        {
1792            retries = retries.saturating_add(1);
1793            std::thread::yield_now();
1794            continue;
1795        }
1796        return Err(error);
1797    };
1798    let file = unsafe { File::from_raw_fd(fd) };
1799    if !file.metadata()?.is_file() {
1800        return Err(std::io::Error::new(
1801            std::io::ErrorKind::PermissionDenied,
1802            "refusing to lock a non-regular file",
1803        ));
1804    }
1805    loop {
1806        if unsafe { libc::flock(file.as_raw_fd(), libc::LOCK_EX) } == 0 {
1807            break;
1808        }
1809        let error = std::io::Error::last_os_error();
1810        if error.kind() != std::io::ErrorKind::Interrupted {
1811            return Err(error);
1812        }
1813    }
1814    Ok(file)
1815}
1816
1817#[cfg(windows)]
1818pub(crate) fn lock_exclusive_beneath(root: &File, relative: &Path) -> std::io::Result<File> {
1819    windows_fs::lock_beneath(root, relative)
1820}
1821
1822#[cfg(not(any(unix, windows)))]
1823pub(crate) fn lock_exclusive_beneath(_root: &File, _relative: &Path) -> std::io::Result<File> {
1824    Err(secure_filesystem_unsupported())
1825}
1826
1827/// Open a directory beneath a held root without following any component.
1828#[cfg(unix)]
1829pub(crate) fn open_directory_beneath(
1830    root: &File,
1831    relative: &Path,
1832    create: bool,
1833) -> std::io::Result<File> {
1834    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1835
1836    if relative.is_absolute()
1837        || relative.components().any(|component| {
1838            matches!(
1839                component,
1840                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1841            )
1842        })
1843    {
1844        return Err(std::io::Error::new(
1845            std::io::ErrorKind::PermissionDenied,
1846            "store capability requires a contained relative directory",
1847        ));
1848    }
1849    let mut directory = root.try_clone()?;
1850    for component in relative.components() {
1851        let Component::Normal(name) = component else {
1852            continue;
1853        };
1854        let name = c_name(name)?;
1855        if create {
1856            let made = unsafe { libc::mkdirat(directory.as_raw_fd(), name.as_ptr(), 0o777) };
1857            if made != 0 {
1858                let error = std::io::Error::last_os_error();
1859                if error.raw_os_error() != Some(libc::EEXIST) {
1860                    return Err(error);
1861                }
1862            }
1863        }
1864        let fd = unsafe {
1865            libc::openat(
1866                directory.as_raw_fd(),
1867                name.as_ptr(),
1868                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1869            )
1870        };
1871        if fd < 0 {
1872            return Err(std::io::Error::last_os_error());
1873        }
1874        directory = unsafe { File::from_raw_fd(fd) };
1875        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
1876            return Err(std::io::Error::new(
1877                std::io::ErrorKind::PermissionDenied,
1878                "path crosses a nested db.md store boundary",
1879            ));
1880        }
1881    }
1882    Ok(directory)
1883}
1884
1885#[cfg(windows)]
1886pub(crate) fn open_directory_beneath(
1887    root: &File,
1888    relative: &Path,
1889    create: bool,
1890) -> std::io::Result<File> {
1891    windows_fs::open_directory_beneath(root, relative, create)
1892}
1893
1894#[cfg(not(any(unix, windows)))]
1895pub(crate) fn open_directory_beneath(
1896    _root: &File,
1897    _relative: &Path,
1898    _create: bool,
1899) -> std::io::Result<File> {
1900    Err(secure_filesystem_unsupported())
1901}
1902
1903pub(crate) fn directory_exists_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
1904    match open_directory_beneath(root, relative, false) {
1905        Ok(_) => Ok(true),
1906        Err(error)
1907            if matches!(
1908                error.kind(),
1909                std::io::ErrorKind::NotFound | std::io::ErrorKind::NotADirectory
1910            ) =>
1911        {
1912            Ok(false)
1913        }
1914        Err(error) => Err(error),
1915    }
1916}
1917
1918/// Confirm that every component of a relative regular-file path has the exact
1919/// byte spelling present on disk. This keeps validation platform-independent
1920/// on case-insensitive filesystems without canonicalizing the mutable root
1921/// pathname.
1922#[cfg(unix)]
1923pub(crate) fn path_case_matches_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
1924    use std::os::fd::{AsRawFd as _, FromRawFd as _};
1925    use std::os::unix::ffi::OsStrExt as _;
1926
1927    if relative.is_absolute()
1928        || relative.components().any(|component| {
1929            matches!(
1930                component,
1931                Component::ParentDir | Component::Prefix(_) | Component::RootDir
1932            )
1933        })
1934    {
1935        return Ok(false);
1936    }
1937    let components: Vec<_> = relative
1938        .components()
1939        .filter_map(|component| match component {
1940            Component::Normal(name) => Some(name),
1941            _ => None,
1942        })
1943        .collect();
1944    let mut directory = root.try_clone()?;
1945    for (index, name) in components.iter().enumerate() {
1946        let scan_fd = unsafe {
1947            libc::openat(
1948                directory.as_raw_fd(),
1949                c_name(std::ffi::OsStr::new("."))?.as_ptr(),
1950                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1951            )
1952        };
1953        if scan_fd < 0 {
1954            return Err(std::io::Error::last_os_error());
1955        }
1956        let stream = unsafe { libc::fdopendir(scan_fd) };
1957        if stream.is_null() {
1958            let error = std::io::Error::last_os_error();
1959            unsafe {
1960                libc::close(scan_fd);
1961            }
1962            return Err(error);
1963        }
1964        let mut exact = false;
1965        loop {
1966            let entry = unsafe { libc::readdir(stream) };
1967            if entry.is_null() {
1968                break;
1969            }
1970            let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
1971            if bytes == name.as_bytes() {
1972                exact = true;
1973                break;
1974            }
1975        }
1976        if unsafe { libc::closedir(stream) } != 0 {
1977            return Err(std::io::Error::last_os_error());
1978        }
1979        if !exact {
1980            return Ok(false);
1981        }
1982        if index + 1 == components.len() {
1983            return directory_contains_exact_regular(&directory, name);
1984        }
1985        let fd = unsafe {
1986            libc::openat(
1987                directory.as_raw_fd(),
1988                c_name(name)?.as_ptr(),
1989                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
1990            )
1991        };
1992        if fd < 0 {
1993            return Ok(false);
1994        }
1995        directory = unsafe { File::from_raw_fd(fd) };
1996        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
1997            return Ok(false);
1998        }
1999    }
2000    Ok(false)
2001}
2002
2003#[cfg(windows)]
2004pub(crate) fn path_case_matches_beneath(root: &File, relative: &Path) -> std::io::Result<bool> {
2005    if relative.is_absolute() {
2006        return Ok(false);
2007    }
2008    let components = relative
2009        .components()
2010        .filter_map(|component| match component {
2011            Component::Normal(name) => Some(name.to_os_string()),
2012            _ => None,
2013        })
2014        .collect::<Vec<_>>();
2015    let mut directory = root.try_clone()?;
2016    for (index, name) in components.iter().enumerate() {
2017        let path = windows_fs::directory_path(&directory)?;
2018        let exact = std::fs::read_dir(&path)?
2019            .collect::<Result<Vec<_>, _>>()?
2020            .into_iter()
2021            .find(|entry| entry.file_name() == *name);
2022        let Some(entry) = exact else { return Ok(false) };
2023        if index + 1 == components.len() {
2024            let attrs = windows_fs::entry_attributes(&entry.path())?;
2025            return Ok(attrs
2026                & (windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT
2027                    | windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY)
2028                == 0);
2029        }
2030        directory = windows_fs::open_directory(&entry.path())?;
2031        if directory_contains_exact_regular(&directory, "DB.md".as_ref())? {
2032            return Ok(false);
2033        }
2034    }
2035    Ok(false)
2036}
2037
2038#[cfg(not(any(unix, windows)))]
2039pub(crate) fn path_case_matches_beneath(_root: &File, _relative: &Path) -> std::io::Result<bool> {
2040    Err(secure_filesystem_unsupported())
2041}
2042
2043/// Immediate no-follow child directories beneath a held root.
2044#[cfg(unix)]
2045pub(crate) fn directory_names_beneath(
2046    root: &File,
2047    relative: &Path,
2048) -> std::io::Result<Vec<std::ffi::OsString>> {
2049    use std::os::fd::AsRawFd as _;
2050    use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
2051
2052    let directory = open_directory_beneath(root, relative, false)?;
2053    let scan_fd = unsafe {
2054        libc::openat(
2055            directory.as_raw_fd(),
2056            c_name(std::ffi::OsStr::new("."))?.as_ptr(),
2057            libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2058        )
2059    };
2060    if scan_fd < 0 {
2061        return Err(std::io::Error::last_os_error());
2062    }
2063    let stream = unsafe { libc::fdopendir(scan_fd) };
2064    if stream.is_null() {
2065        let error = std::io::Error::last_os_error();
2066        unsafe {
2067            libc::close(scan_fd);
2068        }
2069        return Err(error);
2070    }
2071    let mut names = Vec::new();
2072    loop {
2073        let entry = unsafe { libc::readdir(stream) };
2074        if entry.is_null() {
2075            break;
2076        }
2077        let bytes = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) }.to_bytes();
2078        if matches!(bytes, b"." | b"..") || bytes.starts_with(b".") {
2079            continue;
2080        }
2081        let name = std::ffi::OsString::from_vec(bytes.to_vec());
2082        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2083        if unsafe {
2084            libc::fstatat(
2085                directory.as_raw_fd(),
2086                c_name(&name)?.as_ptr(),
2087                &mut stat,
2088                libc::AT_SYMLINK_NOFOLLOW,
2089            )
2090        } == 0
2091            && (stat.st_mode & libc::S_IFMT) == libc::S_IFDIR
2092        {
2093            let child = open_directory_beneath(root, &relative.join(&name), false)?;
2094            if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
2095                names.push(name);
2096            }
2097        }
2098    }
2099    if unsafe { libc::closedir(stream) } != 0 {
2100        return Err(std::io::Error::last_os_error());
2101    }
2102    names.sort_by(|a, b| a.as_bytes().cmp(b.as_bytes()));
2103    Ok(names)
2104}
2105
2106#[cfg(windows)]
2107pub(crate) fn directory_names_beneath(
2108    root: &File,
2109    relative: &Path,
2110) -> std::io::Result<Vec<std::ffi::OsString>> {
2111    let directory = windows_fs::open_directory_beneath(root, relative, false)?;
2112    let path = windows_fs::directory_path(&directory)?;
2113    let mut names = Vec::new();
2114    for entry in std::fs::read_dir(path)? {
2115        let entry = entry?;
2116        let name = entry.file_name();
2117        if name.to_string_lossy().starts_with('.') {
2118            continue;
2119        }
2120        let attrs = windows_fs::entry_attributes(&entry.path())?;
2121        if attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT == 0
2122            && attrs & windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_DIRECTORY != 0
2123        {
2124            let child = windows_fs::open_directory(&entry.path())?;
2125            if !directory_contains_exact_regular(&child, "DB.md".as_ref())? {
2126                names.push(name);
2127            }
2128        }
2129    }
2130    names.sort();
2131    Ok(names)
2132}
2133
2134#[cfg(not(any(unix, windows)))]
2135pub(crate) fn directory_names_beneath(
2136    _root: &File,
2137    _relative: &Path,
2138) -> std::io::Result<Vec<std::ffi::OsString>> {
2139    Err(secure_filesystem_unsupported())
2140}
2141
2142#[cfg(not(any(unix, windows)))]
2143pub(crate) fn write_atomic_beneath(
2144    _root: &File,
2145    _relative: &Path,
2146    _bytes: &[u8],
2147    _create_new: bool,
2148    _durable: bool,
2149) -> std::io::Result<()> {
2150    Err(secure_filesystem_unsupported())
2151}
2152
2153#[cfg(unix)]
2154pub(crate) fn rename_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
2155    use std::os::fd::AsRawFd as _;
2156
2157    let (old_parent, old_leaf) = open_parent_beneath(root, old, false)?;
2158    let (new_parent, new_leaf) = open_parent_beneath(root, new, true)?;
2159    let mut source_stat: libc::stat = unsafe { std::mem::zeroed() };
2160    if unsafe {
2161        libc::fstatat(
2162            old_parent.as_raw_fd(),
2163            old_leaf.as_ptr(),
2164            &mut source_stat,
2165            libc::AT_SYMLINK_NOFOLLOW,
2166        )
2167    } != 0
2168    {
2169        return Err(std::io::Error::last_os_error());
2170    }
2171    if (source_stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
2172        return Err(std::io::Error::new(
2173            std::io::ErrorKind::PermissionDenied,
2174            "refusing to rename a non-regular file",
2175        ));
2176    }
2177    renameat_noreplace(
2178        old_parent.as_raw_fd(),
2179        &old_leaf,
2180        new_parent.as_raw_fd(),
2181        &new_leaf,
2182    )?;
2183    old_parent.sync_all()?;
2184    new_parent.sync_all()?;
2185    Ok(())
2186}
2187
2188#[cfg(windows)]
2189pub(crate) fn rename_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
2190    windows_fs::rename_beneath(root, old, new)
2191}
2192
2193#[cfg(windows)]
2194pub(crate) fn rename_directory_beneath(root: &File, old: &Path, new: &Path) -> std::io::Result<()> {
2195    windows_fs::rename_directory_beneath(root, old, new)
2196}
2197
2198#[cfg(not(any(unix, windows)))]
2199pub(crate) fn rename_beneath(_root: &File, _old: &Path, _new: &Path) -> std::io::Result<()> {
2200    Err(secure_filesystem_unsupported())
2201}
2202
2203#[cfg(unix)]
2204pub(crate) fn remove_file_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2205    use std::os::fd::AsRawFd as _;
2206
2207    let (parent, leaf) = open_parent_beneath(root, relative, false)?;
2208    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2209    if unsafe {
2210        libc::fstatat(
2211            parent.as_raw_fd(),
2212            leaf.as_ptr(),
2213            &mut stat,
2214            libc::AT_SYMLINK_NOFOLLOW,
2215        )
2216    } != 0
2217    {
2218        return Err(std::io::Error::last_os_error());
2219    }
2220    if (stat.st_mode & libc::S_IFMT) != libc::S_IFREG {
2221        return Err(std::io::Error::new(
2222            std::io::ErrorKind::PermissionDenied,
2223            "refusing to remove a non-regular file",
2224        ));
2225    }
2226    if unsafe { libc::unlinkat(parent.as_raw_fd(), leaf.as_ptr(), 0) } != 0 {
2227        return Err(std::io::Error::last_os_error());
2228    }
2229    parent.sync_all()
2230}
2231
2232#[cfg(windows)]
2233pub(crate) fn remove_file_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2234    windows_fs::remove_file_beneath(root, relative)
2235}
2236
2237/// Remove one private subtree beneath a held store root without following any
2238/// symlink. This is reserved for disposable control state such as completed
2239/// conflict bundles; riding store data uses explicit file operations instead.
2240#[cfg(unix)]
2241pub(crate) fn remove_tree_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2242    fn remove_at(parent: &File, name: &std::ffi::CStr) -> std::io::Result<()> {
2243        use std::os::fd::{AsRawFd as _, FromRawFd as _};
2244        let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2245        if unsafe {
2246            libc::fstatat(
2247                parent.as_raw_fd(),
2248                name.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_IFDIR {
2257            return Err(std::io::Error::new(
2258                std::io::ErrorKind::PermissionDenied,
2259                "private cleanup target is not a directory",
2260            ));
2261        }
2262        let fd = unsafe {
2263            libc::openat(
2264                parent.as_raw_fd(),
2265                name.as_ptr(),
2266                libc::O_RDONLY | libc::O_DIRECTORY | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2267            )
2268        };
2269        if fd < 0 {
2270            return Err(std::io::Error::last_os_error());
2271        }
2272        let directory = unsafe { File::from_raw_fd(fd) };
2273        let scan_fd = unsafe { libc::dup(directory.as_raw_fd()) };
2274        if scan_fd < 0 {
2275            return Err(std::io::Error::last_os_error());
2276        }
2277        let stream = unsafe { libc::fdopendir(scan_fd) };
2278        if stream.is_null() {
2279            unsafe { libc::close(scan_fd) };
2280            return Err(std::io::Error::last_os_error());
2281        }
2282        let mut names = Vec::new();
2283        loop {
2284            let entry = unsafe { libc::readdir(stream) };
2285            if entry.is_null() {
2286                break;
2287            }
2288            let raw = unsafe { std::ffi::CStr::from_ptr((*entry).d_name.as_ptr()) };
2289            if !matches!(raw.to_bytes(), b"." | b"..") {
2290                names.push(raw.to_owned());
2291            }
2292        }
2293        if unsafe { libc::closedir(stream) } != 0 {
2294            return Err(std::io::Error::last_os_error());
2295        }
2296        for child in names {
2297            let mut child_stat: libc::stat = unsafe { std::mem::zeroed() };
2298            if unsafe {
2299                libc::fstatat(
2300                    directory.as_raw_fd(),
2301                    child.as_ptr(),
2302                    &mut child_stat,
2303                    libc::AT_SYMLINK_NOFOLLOW,
2304                )
2305            } != 0
2306            {
2307                return Err(std::io::Error::last_os_error());
2308            }
2309            if (child_stat.st_mode & libc::S_IFMT) == libc::S_IFDIR {
2310                remove_at(&directory, &child)?;
2311            } else if unsafe { libc::unlinkat(directory.as_raw_fd(), child.as_ptr(), 0) } != 0 {
2312                return Err(std::io::Error::last_os_error());
2313            }
2314        }
2315        drop(directory);
2316        if unsafe { libc::unlinkat(parent.as_raw_fd(), name.as_ptr(), libc::AT_REMOVEDIR) } != 0 {
2317            return Err(std::io::Error::last_os_error());
2318        }
2319        parent.sync_all()
2320    }
2321
2322    let (parent, leaf) = open_parent_beneath(root, relative, false)?;
2323    remove_at(&parent, &leaf)
2324}
2325
2326#[cfg(windows)]
2327pub(crate) fn remove_tree_beneath(root: &File, relative: &Path) -> std::io::Result<()> {
2328    windows_fs::remove_tree_beneath(root, relative)
2329}
2330
2331#[cfg(not(any(unix, windows)))]
2332pub(crate) fn remove_file_beneath(_root: &File, _relative: &Path) -> std::io::Result<()> {
2333    Err(secure_filesystem_unsupported())
2334}
2335
2336#[cfg(not(any(unix, windows)))]
2337pub(crate) fn remove_tree_beneath(_root: &File, _relative: &Path) -> std::io::Result<()> {
2338    Err(secure_filesystem_unsupported())
2339}
2340
2341#[cfg(unix)]
2342fn write_atomic_unix(
2343    path: &Path,
2344    bytes: &[u8],
2345    create_new: bool,
2346    durable: bool,
2347) -> std::io::Result<()> {
2348    let (directory, leaf) = open_parent_unix(path, true)?;
2349    write_atomic_at(directory, leaf, bytes, create_new, durable)
2350}
2351
2352#[cfg(unix)]
2353fn write_atomic_at(
2354    directory: File,
2355    leaf: std::ffi::CString,
2356    bytes: &[u8],
2357    create_new: bool,
2358    durable: bool,
2359) -> std::io::Result<()> {
2360    use std::os::fd::{AsRawFd as _, FromRawFd as _};
2361
2362    static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
2363    let pid = std::process::id();
2364    let nanos = SystemTime::now()
2365        .duration_since(UNIX_EPOCH)
2366        .map(|duration| duration.as_nanos())
2367        .unwrap_or(0);
2368    let mut allocated = None;
2369    for _ in 0..128 {
2370        let name = std::ffi::OsString::from(format!(
2371            ".dbmd.tmp.{pid}.{nanos}.{}",
2372            TMP_SEQ.fetch_add(1, Ordering::Relaxed)
2373        ));
2374        let name = c_name(&name)?;
2375        let fd = unsafe {
2376            libc::openat(
2377                directory.as_raw_fd(),
2378                name.as_ptr(),
2379                libc::O_WRONLY | libc::O_CREAT | libc::O_EXCL | libc::O_CLOEXEC | libc::O_NOFOLLOW,
2380                0o666,
2381            )
2382        };
2383        if fd >= 0 {
2384            allocated = Some((name, unsafe { File::from_raw_fd(fd) }));
2385            break;
2386        }
2387        let error = std::io::Error::last_os_error();
2388        if error.kind() != std::io::ErrorKind::AlreadyExists {
2389            return Err(error);
2390        }
2391    }
2392    let (temp, mut file) = allocated.ok_or_else(|| {
2393        std::io::Error::new(
2394            std::io::ErrorKind::AlreadyExists,
2395            "could not allocate secure temporary file",
2396        )
2397    })?;
2398    let cleanup =
2399        |name: &std::ffi::CStr| unsafe { libc::unlinkat(directory.as_raw_fd(), name.as_ptr(), 0) };
2400    let write_result = file.write_all(bytes).and_then(|_| {
2401        if durable {
2402            file.sync_all()
2403        } else {
2404            file.flush()
2405        }
2406    });
2407    if let Err(error) = write_result {
2408        let _ = cleanup(&temp);
2409        return Err(error);
2410    }
2411
2412    // Preserve an existing regular destination's exact Unix mode. A symlink is
2413    // never dereferenced; chmod failure aborts rather than silently widening.
2414    let mut stat: libc::stat = unsafe { std::mem::zeroed() };
2415    let destination_stat = unsafe {
2416        libc::fstatat(
2417            directory.as_raw_fd(),
2418            leaf.as_ptr(),
2419            &mut stat,
2420            libc::AT_SYMLINK_NOFOLLOW,
2421        )
2422    };
2423    if destination_stat == 0 {
2424        if (stat.st_mode & libc::S_IFMT) == libc::S_IFLNK {
2425            let _ = cleanup(&temp);
2426            return Err(std::io::Error::new(
2427                std::io::ErrorKind::PermissionDenied,
2428                "refusing a symlink destination",
2429            ));
2430        }
2431        if unsafe { libc::fchmod(file.as_raw_fd(), stat.st_mode & 0o7777) } != 0 {
2432            let error = std::io::Error::last_os_error();
2433            let _ = cleanup(&temp);
2434            return Err(error);
2435        }
2436    } else {
2437        let error = std::io::Error::last_os_error();
2438        if error.kind() != std::io::ErrorKind::NotFound {
2439            let _ = cleanup(&temp);
2440            return Err(error);
2441        }
2442    }
2443    drop(file);
2444
2445    let installed = if create_new {
2446        unsafe {
2447            libc::linkat(
2448                directory.as_raw_fd(),
2449                temp.as_ptr(),
2450                directory.as_raw_fd(),
2451                leaf.as_ptr(),
2452                0,
2453            )
2454        }
2455    } else {
2456        unsafe {
2457            libc::renameat(
2458                directory.as_raw_fd(),
2459                temp.as_ptr(),
2460                directory.as_raw_fd(),
2461                leaf.as_ptr(),
2462            )
2463        }
2464    };
2465    if installed != 0 {
2466        let error = std::io::Error::last_os_error();
2467        let _ = cleanup(&temp);
2468        return Err(error);
2469    }
2470    if create_new {
2471        let _ = cleanup(&temp);
2472    }
2473    if durable {
2474        directory.sync_all()?;
2475    }
2476    Ok(())
2477}
2478
2479/// Drop-based cleanup for the hidden temp file `write_atomic` creates. While
2480/// armed, dropping the guard removes `path`. [`TempGuard::disarm`] is called
2481/// only after a successful rename, or after a successful temp-link cleanup in
2482/// [`write_atomic_new`], so the final destination is never touched.
2483#[cfg(test)]
2484struct TempGuard {
2485    path: PathBuf,
2486    armed: bool,
2487}
2488
2489#[cfg(test)]
2490impl TempGuard {
2491    /// Stop cleaning up `path` on drop — used once the temp has been renamed
2492    /// into place and is no longer a stray temp file.
2493    fn disarm(&mut self) {
2494        self.armed = false;
2495    }
2496}
2497
2498#[cfg(test)]
2499impl Drop for TempGuard {
2500    fn drop(&mut self) {
2501        // Best-effort cleanup if an error path bailed out before the rename.
2502        if self.armed {
2503            let _ = fs::remove_file(&self.path);
2504        }
2505    }
2506}
2507
2508/// Create a uniquely-named temp file in `dir` with `create_new` (never clobbers
2509/// a predictable name), retrying on the vanishingly-rare collision. The name is
2510/// hidden (`.`-prefixed) and tagged with pid + nanos + a process-wide counter so
2511/// concurrent writers in the same directory never pick the same path. Returns the
2512/// open handle plus an armed [`TempGuard`] so any early return cleans up the temp.
2513#[cfg(test)]
2514fn create_temp_file(dir: &Path, file_name: &str) -> std::io::Result<(File, TempGuard)> {
2515    static TMP_SEQ: AtomicU64 = AtomicU64::new(0);
2516    let pid = std::process::id();
2517    let nanos = SystemTime::now()
2518        .duration_since(UNIX_EPOCH)
2519        .map(|d| d.as_nanos())
2520        .unwrap_or(0);
2521
2522    for _ in 0..128 {
2523        let seq = TMP_SEQ.fetch_add(1, Ordering::Relaxed);
2524        let tmp = dir.join(format!(".{file_name}.tmp.{pid}.{nanos}.{seq}"));
2525        match OpenOptions::new().write(true).create_new(true).open(&tmp) {
2526            Ok(file) => {
2527                return Ok((
2528                    file,
2529                    TempGuard {
2530                        path: tmp,
2531                        armed: true,
2532                    },
2533                ))
2534            }
2535            Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
2536            Err(e) => return Err(e),
2537        }
2538    }
2539
2540    Err(std::io::Error::new(
2541        std::io::ErrorKind::AlreadyExists,
2542        "could not allocate a unique dbmd temp file",
2543    ))
2544}
2545
2546#[cfg(test)]
2547mod tests {
2548    use super::*;
2549    use tempfile::TempDir;
2550
2551    #[test]
2552    fn write_atomic_creates_then_replaces_durably() {
2553        let tmp = TempDir::new().unwrap();
2554        let target = tmp.path().join("sub").join("file.txt"); // parent missing
2555
2556        write_atomic(&target, b"first").unwrap();
2557        assert_eq!(std::fs::read(&target).unwrap(), b"first");
2558
2559        // Replace in place — content swaps, no temp files left behind.
2560        write_atomic(&target, b"second").unwrap();
2561        assert_eq!(std::fs::read(&target).unwrap(), b"second");
2562
2563        let leftovers: Vec<_> = std::fs::read_dir(target.parent().unwrap())
2564            .unwrap()
2565            .filter_map(|e| e.ok())
2566            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
2567            .collect();
2568        assert!(leftovers.is_empty(), "no temp files may be left behind");
2569    }
2570
2571    #[test]
2572    fn write_atomic_is_byte_exact_including_empty() {
2573        let tmp = TempDir::new().unwrap();
2574        let target = tmp.path().join("empty.txt");
2575        write_atomic(&target, b"").unwrap();
2576        assert_eq!(std::fs::read(&target).unwrap(), b"");
2577    }
2578
2579    #[test]
2580    fn write_atomic_new_creates_but_refuses_existing() {
2581        let tmp = TempDir::new().unwrap();
2582        let target = tmp.path().join("sub").join("file.txt");
2583
2584        write_atomic_new(&target, b"first").unwrap();
2585        assert_eq!(std::fs::read(&target).unwrap(), b"first");
2586
2587        let err = write_atomic_new(&target, b"second").unwrap_err();
2588        assert_eq!(err.kind(), std::io::ErrorKind::AlreadyExists);
2589        assert_eq!(
2590            std::fs::read(&target).unwrap(),
2591            b"first",
2592            "create-new failure must leave the existing destination untouched"
2593        );
2594
2595        assert_no_temp_files(target.parent().unwrap());
2596    }
2597
2598    #[test]
2599    fn write_atomic_new_allows_only_one_concurrent_creator() {
2600        use std::sync::{Arc, Barrier};
2601
2602        for round in 0..40 {
2603            let tmp = TempDir::new().unwrap();
2604            let target = tmp.path().join("file.txt");
2605            let barrier = Arc::new(Barrier::new(8));
2606
2607            let handles: Vec<_> = (0..8)
2608                .map(|i| {
2609                    let target = target.clone();
2610                    let barrier = Arc::clone(&barrier);
2611                    std::thread::spawn(move || {
2612                        let payload = format!("payload-{i}");
2613                        barrier.wait();
2614                        let result = write_atomic_new(&target, payload.as_bytes())
2615                            .map(|_| ())
2616                            .map_err(|e| e.kind());
2617                        (payload, result)
2618                    })
2619                })
2620                .collect();
2621
2622            let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
2623            let winners: Vec<_> = results
2624                .iter()
2625                .filter_map(|(payload, result)| result.is_ok().then_some(payload))
2626                .collect();
2627            let already_exists = results
2628                .iter()
2629                .filter(|(_, result)| {
2630                    matches!(result, Err(kind) if *kind == std::io::ErrorKind::AlreadyExists)
2631                })
2632                .count();
2633
2634            assert_eq!(
2635                winners.len(),
2636                1,
2637                "round {round}: exactly one creator may win, got {results:?}"
2638            );
2639            assert_eq!(
2640                already_exists, 7,
2641                "round {round}: every losing creator must get AlreadyExists, got {results:?}"
2642            );
2643
2644            let written = std::fs::read_to_string(&target).unwrap();
2645            assert_eq!(
2646                written, *winners[0],
2647                "round {round}: destination must contain the winner's payload"
2648            );
2649            assert_no_temp_files(tmp.path());
2650        }
2651    }
2652
2653    /// Regression for finding #22: an early return between temp-file creation and
2654    /// a successful rename (e.g. `write_all`/`sync_all` failing under ENOSPC/EIO)
2655    /// must NOT leave the hidden temp file orphaned in the data directory.
2656    ///
2657    /// Pre-fix, `create_temp_file` handed back a bare `PathBuf` with no `Drop`
2658    /// cleanup, so dropping it without a rename — exactly what `?` does on a
2659    /// write/sync failure — left the temp on disk. This reconstructs that path by
2660    /// dropping the guard without renaming and asserting the temp is gone.
2661    #[test]
2662    fn regression_armed_guard_removes_temp_on_early_drop() {
2663        let dir = TempDir::new().unwrap();
2664        let (file, guard) = create_temp_file(dir.path(), "file.txt").unwrap();
2665        let tmp_path = guard.path.clone();
2666        assert!(
2667            tmp_path.exists(),
2668            "temp file should exist after create_temp_file"
2669        );
2670
2671        // Simulate a write/sync failure bailing out before the rename: the file
2672        // handle and the (still-armed) guard go out of scope without a rename.
2673        drop(file);
2674        drop(guard);
2675
2676        assert!(
2677            !tmp_path.exists(),
2678            "armed guard must remove the orphaned temp file on early drop"
2679        );
2680        // No stray `.tmp.` files left in the directory.
2681        let leftovers: Vec<_> = std::fs::read_dir(dir.path())
2682            .unwrap()
2683            .filter_map(|e| e.ok())
2684            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
2685            .collect();
2686        assert!(leftovers.is_empty(), "no temp files may be left behind");
2687    }
2688
2689    /// Once disarmed (after a successful rename) the guard must NOT delete the
2690    /// path it was tracking — otherwise it would clobber the renamed destination.
2691    #[test]
2692    fn regression_disarmed_guard_leaves_file_intact() {
2693        let dir = TempDir::new().unwrap();
2694        let (file, mut guard) = create_temp_file(dir.path(), "kept.txt").unwrap();
2695        drop(file);
2696        let kept = guard.path.clone();
2697
2698        guard.disarm();
2699        drop(guard);
2700
2701        assert!(
2702            kept.exists(),
2703            "disarmed guard must leave the renamed destination untouched"
2704        );
2705    }
2706
2707    fn assert_no_temp_files(dir: &Path) {
2708        let leftovers: Vec<_> = std::fs::read_dir(dir)
2709            .unwrap()
2710            .filter_map(|e| e.ok())
2711            .filter(|e| e.file_name().to_string_lossy().contains(".tmp."))
2712            .collect();
2713        assert!(leftovers.is_empty(), "no temp files may be left behind");
2714    }
2715
2716    /// Regression: rewriting an existing file via `write_atomic` must PRESERVE
2717    /// its permission bits. Pre-fix the temp file's default mode (0644) replaced
2718    /// a deliberately-restricted destination (0600) on every rewrite — a quiet
2719    /// permission-widening on user data. A first create still uses the default
2720    /// mode (there is no destination mode to copy).
2721    #[cfg(unix)]
2722    #[test]
2723    fn write_atomic_preserves_existing_destination_permissions() {
2724        use std::os::unix::fs::PermissionsExt;
2725
2726        let tmp = TempDir::new().unwrap();
2727        let target = tmp.path().join("private.md");
2728
2729        // Create, then restrict to 0600.
2730        write_atomic(&target, b"secret v1").unwrap();
2731        std::fs::set_permissions(&target, std::fs::Permissions::from_mode(0o600)).unwrap();
2732        let before = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
2733        assert_eq!(before, 0o600, "fixture must start at 0600");
2734
2735        // Rewrite in place: the 0600 mode must survive (not reset to 0644).
2736        write_atomic(&target, b"secret v2").unwrap();
2737        let after = std::fs::metadata(&target).unwrap().permissions().mode() & 0o777;
2738        assert_eq!(
2739            after, 0o600,
2740            "write_atomic must preserve the destination's 0600 mode, got {after:o}"
2741        );
2742        assert_eq!(std::fs::read(&target).unwrap(), b"secret v2");
2743    }
2744
2745    /// Exploit regression for the containment/write TOCTOU: a caller may have
2746    /// validated `store/records/safe.md`, then an attacker replaces `records`
2747    /// with a symlink to an external directory before the atomic writer opens
2748    /// it. Every ancestor is opened with `openat(O_DIRECTORY|O_NOFOLLOW)`, so
2749    /// the write is refused and the outside victim is byte-identical.
2750    #[cfg(unix)]
2751    #[test]
2752    fn write_atomic_refuses_symlinked_ancestor_without_touching_external_file() {
2753        use std::os::unix::fs::symlink;
2754
2755        let sandbox = TempDir::new().unwrap();
2756        let store = sandbox.path().join("store");
2757        let external = sandbox.path().join("external");
2758        std::fs::create_dir_all(store.join("records")).unwrap();
2759        std::fs::create_dir_all(&external).unwrap();
2760        let victim = external.join("safe.md");
2761        std::fs::write(&victim, b"external secret").unwrap();
2762
2763        std::fs::remove_dir(store.join("records")).unwrap();
2764        symlink(&external, store.join("records")).unwrap();
2765
2766        let error = write_atomic(&store.join("records/safe.md"), b"attacker output")
2767            .expect_err("a symlinked ancestor must fail closed");
2768        assert!(
2769            matches!(
2770                error.raw_os_error(),
2771                Some(code) if code == libc::ELOOP || code == libc::ENOTDIR
2772            ),
2773            "expected no-follow refusal, got {error:?}"
2774        );
2775        assert_eq!(std::fs::read(&victim).unwrap(), b"external secret");
2776    }
2777
2778    /// A leaf swap is equally unsafe for reads: after containment validation an
2779    /// attacker can replace the selected record with a symlink to a secret.
2780    /// `read_bounded_nofollow` opens the leaf once with `O_NOFOLLOW`, then sizes
2781    /// and reads that same descriptor, so no external bytes are returned.
2782    #[cfg(unix)]
2783    #[test]
2784    fn bounded_read_refuses_symlink_leaf() {
2785        use std::os::unix::fs::symlink;
2786
2787        let sandbox = TempDir::new().unwrap();
2788        let external = sandbox.path().join("secret");
2789        std::fs::write(&external, b"do not exfiltrate").unwrap();
2790        let selected = sandbox.path().join("selected.md");
2791        symlink(&external, &selected).unwrap();
2792
2793        let error = read_bounded_nofollow(&selected, 1024)
2794            .expect_err("the no-follow reader must reject a symlink leaf");
2795        assert_eq!(error.raw_os_error(), Some(libc::ELOOP));
2796    }
2797
2798    /// If the file grows after its descriptor metadata was read, the bounded
2799    /// descriptor read still enforces the actual byte ceiling (`take(max+1)`),
2800    /// rather than trusting the stale size.
2801    #[test]
2802    fn bounded_read_rejects_content_over_limit() {
2803        let sandbox = TempDir::new().unwrap();
2804        let selected = sandbox.path().join("selected.md");
2805        std::fs::write(&selected, b"12345").unwrap();
2806        let error = read_bounded_nofollow(&selected, 4)
2807            .expect_err("actual content above the cap must be refused");
2808        assert_eq!(error.kind(), std::io::ErrorKind::InvalidData);
2809    }
2810
2811    #[cfg(unix)]
2812    #[test]
2813    fn held_directory_reader_survives_ancestor_swap_without_disclosure() {
2814        use std::os::unix::fs::symlink;
2815
2816        let sandbox = TempDir::new().unwrap();
2817        let store = sandbox.path().join("store");
2818        let contacts = store.join("records/contacts");
2819        std::fs::create_dir_all(&contacts).unwrap();
2820        std::fs::write(contacts.join("selected.md"), b"owned").unwrap();
2821        let outside = sandbox.path().join("outside");
2822        std::fs::create_dir_all(&outside).unwrap();
2823        std::fs::write(outside.join("selected.md"), b"secret").unwrap();
2824
2825        let mut reader = BoundedDirReader::new(&store).unwrap();
2826        let relative = Path::new("records/contacts/selected.md");
2827        assert_eq!(reader.read(relative, 1024).unwrap(), b"owned");
2828
2829        let detached = store.join("records/contacts-detached");
2830        std::fs::rename(&contacts, &detached).unwrap();
2831        symlink(&outside, &contacts).unwrap();
2832
2833        assert_eq!(
2834            reader.read(relative, 1024).unwrap(),
2835            b"owned",
2836            "the cached directory capability must not reopen the swapped pathname"
2837        );
2838    }
2839
2840    #[cfg(any(target_os = "linux", target_os = "android", target_os = "macos"))]
2841    #[test]
2842    fn rename_nofollow_is_atomic_no_replace() {
2843        let sandbox = TempDir::new().unwrap();
2844        let source = sandbox.path().join("source.md");
2845        let destination = sandbox.path().join("destination.md");
2846        std::fs::write(&source, b"source").unwrap();
2847        std::fs::write(&destination, b"existing").unwrap();
2848
2849        let error = rename_nofollow(&source, &destination)
2850            .expect_err("a destination created after preflight must not be clobbered");
2851        assert_eq!(error.kind(), std::io::ErrorKind::AlreadyExists);
2852        assert_eq!(std::fs::read(&source).unwrap(), b"source");
2853        assert_eq!(std::fs::read(&destination).unwrap(), b"existing");
2854    }
2855
2856    #[cfg(windows)]
2857    #[test]
2858    fn windows_atomic_replace_preserves_readonly_and_exact_bytes() {
2859        let sandbox = TempDir::new().unwrap();
2860        let target = sandbox.path().join("readonly.md");
2861        std::fs::write(&target, b"old").unwrap();
2862        let mut permissions = std::fs::metadata(&target).unwrap().permissions();
2863        permissions.set_readonly(true);
2864        std::fs::set_permissions(&target, permissions).unwrap();
2865
2866        write_atomic(&target, b"replacement").unwrap();
2867        assert_eq!(std::fs::read(&target).unwrap(), b"replacement");
2868        assert!(std::fs::metadata(&target).unwrap().permissions().readonly());
2869    }
2870
2871    #[cfg(windows)]
2872    #[test]
2873    fn windows_reader_refuses_a_reparse_leaf() {
2874        use std::os::windows::fs::symlink_file;
2875
2876        let sandbox = TempDir::new().unwrap();
2877        let external = sandbox.path().join("external.md");
2878        let selected = sandbox.path().join("selected.md");
2879        std::fs::write(&external, b"secret").unwrap();
2880        symlink_file(&external, &selected).unwrap();
2881        assert!(read_bounded_nofollow(&selected, 1024).is_err());
2882    }
2883
2884    #[cfg(windows)]
2885    #[test]
2886    fn windows_lock_serializes_competing_handles() {
2887        let sandbox = TempDir::new().unwrap();
2888        let root = open_directory_nofollow(sandbox.path()).unwrap();
2889        let first = lock_exclusive_beneath(&root, Path::new("lock")).unwrap();
2890        let root_for_thread = root.try_clone().unwrap();
2891        let (sender, receiver) = std::sync::mpsc::channel();
2892        std::thread::spawn(move || {
2893            sender
2894                .send(lock_exclusive_beneath(&root_for_thread, Path::new("lock")))
2895                .unwrap();
2896        });
2897        assert!(matches!(
2898            receiver.recv_timeout(std::time::Duration::from_millis(100)),
2899            Err(std::sync::mpsc::RecvTimeoutError::Timeout)
2900        ));
2901        drop(first);
2902        assert!(receiver
2903            .recv_timeout(std::time::Duration::from_secs(5))
2904            .unwrap()
2905            .is_ok());
2906    }
2907}