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