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