1#[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
40pub 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
62pub 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 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(¤t, 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(¤t);
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(¤t, 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(¤t, 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(¤t);
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(¤t, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
316 true,
317 )?);
318 }
319 Err(error) => return Err(error),
320 }
321 if contains_exact_regular_path(¤t, 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(¤t, 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(¤t);
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(¤t, FILE_READ_ATTRIBUTES, true, OPEN_EXISTING)?,
520 true,
521 )?);
522 }
523 Err(error) => return Err(error),
524 }
525 if contains_exact_regular_path(¤t, 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
685pub 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 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#[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 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#[cfg(unix)]
828pub(crate) fn open_directory_nofollow(path: &Path) -> std::io::Result<File> {
829 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#[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#[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#[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#[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
1403pub 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#[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#[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#[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 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#[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#[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#[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#[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 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#[cfg(test)]
2484struct TempGuard {
2485 path: PathBuf,
2486 armed: bool,
2487}
2488
2489#[cfg(test)]
2490impl TempGuard {
2491 fn disarm(&mut self) {
2494 self.armed = false;
2495 }
2496}
2497
2498#[cfg(test)]
2499impl Drop for TempGuard {
2500 fn drop(&mut self) {
2501 if self.armed {
2503 let _ = fs::remove_file(&self.path);
2504 }
2505 }
2506}
2507
2508#[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"); write_atomic(&target, b"first").unwrap();
2557 assert_eq!(std::fs::read(&target).unwrap(), b"first");
2558
2559 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 #[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 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 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 #[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 #[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 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 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 #[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 #[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 #[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}