1use std::ffi::{OsStr, OsString};
2use std::io::{self, Write};
3use std::path::{Path, PathBuf};
4
5#[derive(Clone, Debug, Eq, Hash, PartialEq)]
7pub enum DurableFileIdentity {
8 #[cfg(unix)]
9 Unix { device: u64, inode: u64 },
10 #[cfg(windows)]
11 Windows { volume_serial: u32, file_index: u64 },
12}
13
14pub struct DurableRoot {
20 canonical_path: PathBuf,
21 #[cfg(any(
22 windows,
23 all(
24 unix,
25 any(target_os = "linux", target_os = "android", target_vendor = "apple")
26 )
27 ))]
28 directory: std::fs::File,
29}
30
31impl std::fmt::Debug for DurableRoot {
32 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33 formatter
34 .debug_struct("DurableRoot")
35 .field("canonical_path", &self.canonical_path)
36 .finish_non_exhaustive()
37 }
38}
39
40impl DurableRoot {
41 pub fn try_clone(&self) -> io::Result<Self> {
42 Ok(Self {
43 canonical_path: self.canonical_path.clone(),
44 #[cfg(any(
45 windows,
46 all(
47 unix,
48 any(target_os = "linux", target_os = "android", target_vendor = "apple")
49 )
50 ))]
51 directory: self.directory.try_clone()?,
52 })
53 }
54
55 pub fn open(root: impl AsRef<Path>) -> io::Result<Self> {
56 #[cfg(all(
57 unix,
58 any(target_os = "linux", target_os = "android", target_vendor = "apple")
59 ))]
60 {
61 #[cfg(any(target_os = "linux", target_os = "android"))]
62 if let Ok(relative) = root.as_ref().strip_prefix("/proc/self/fd") {
63 let mut components =
64 relative
65 .components()
66 .filter_map(|component| match component {
67 std::path::Component::Normal(value) => Some(value),
68 _ => None,
69 });
70 if let Some(fd) = components.next().filter(|value| {
71 value
72 .to_str()
73 .is_some_and(|value| value.parse::<i32>().is_ok())
74 }) {
75 use rustix::fs::{openat, Mode, OFlags};
76 let mut directory = std::fs::File::open(Path::new("/proc/self/fd").join(fd))?;
77 for component in components {
78 let opened = openat(
79 &directory,
80 Path::new(component),
81 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
82 Mode::empty(),
83 )
84 .map_err(io::Error::from)?;
85 directory = std::fs::File::from(opened);
86 }
87 use std::os::fd::AsRawFd;
88 let canonical_path = std::fs::read_link(
89 Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string()),
90 )?;
91 return Ok(Self {
92 canonical_path,
93 directory,
94 });
95 }
96 }
97 let directory = open_unix_directory_path(root.as_ref())?;
98 let directory = std::fs::File::from(directory);
99 #[cfg(any(target_os = "linux", target_os = "android"))]
100 let canonical_path = {
101 use std::os::fd::AsRawFd;
102 std::fs::read_link(
103 Path::new("/proc/self/fd").join(directory.as_raw_fd().to_string()),
104 )?
105 };
106 #[cfg(target_vendor = "apple")]
107 let canonical_path = {
108 use std::os::unix::ffi::OsStrExt;
109 let path = rustix::fs::getpath(&directory).map_err(io::Error::from)?;
110 PathBuf::from(OsStr::from_bytes(path.to_bytes()))
111 };
112 Ok(Self {
113 canonical_path,
114 directory,
115 })
116 }
117
118 #[cfg(windows)]
119 {
120 let directory = open_windows_nofollow(root.as_ref())?;
121 ensure_windows_directory(&directory, root.as_ref()).map_err(mongrel_error_to_io)?;
122 let canonical_path = root.as_ref().canonicalize()?;
123 return Ok(Self {
124 canonical_path,
125 directory,
126 });
127 }
128
129 #[cfg(not(any(
130 windows,
131 all(
132 unix,
133 any(target_os = "linux", target_os = "android", target_vendor = "apple")
134 )
135 )))]
136 Err(io::Error::new(
137 io::ErrorKind::Unsupported,
138 "descriptor-relative durable files are unsupported on this platform",
139 ))
140 }
141
142 pub fn canonical_path(&self) -> &Path {
143 &self.canonical_path
144 }
145
146 pub fn file_identity(&self) -> io::Result<DurableFileIdentity> {
148 #[cfg(all(
149 unix,
150 any(target_os = "linux", target_os = "android", target_vendor = "apple")
151 ))]
152 {
153 use std::os::unix::fs::MetadataExt;
154 let metadata = self.directory.metadata()?;
155 return Ok(DurableFileIdentity::Unix {
156 device: metadata.dev(),
157 inode: metadata.ino(),
158 });
159 }
160
161 #[cfg(windows)]
162 {
163 use std::os::windows::fs::MetadataExt;
164 let metadata = self.directory.metadata()?;
165 return Ok(DurableFileIdentity::Windows {
166 volume_serial: metadata.volume_serial_number().ok_or_else(|| {
167 io::Error::new(
168 io::ErrorKind::Unsupported,
169 "directory has no volume identity",
170 )
171 })?,
172 file_index: metadata.file_index().ok_or_else(|| {
173 io::Error::new(io::ErrorKind::Unsupported, "directory has no file identity")
174 })?,
175 });
176 }
177
178 #[allow(unreachable_code)]
179 Err(io::Error::new(
180 io::ErrorKind::Unsupported,
181 "durable root identity is unsupported on this platform",
182 ))
183 }
184
185 pub fn io_path(&self) -> io::Result<PathBuf> {
188 #[cfg(any(target_os = "linux", target_os = "android"))]
189 {
190 use std::os::fd::AsRawFd;
191 let prefix = "/proc/self/fd";
192 return Ok(Path::new(prefix)
193 .join(self.directory.as_raw_fd().to_string())
194 .join("."));
195 }
196
197 #[cfg(target_vendor = "apple")]
198 {
199 use std::os::unix::ffi::OsStrExt;
200 let path = rustix::fs::getpath(&self.directory).map_err(io::Error::from)?;
201 return Ok(PathBuf::from(OsStr::from_bytes(path.to_bytes())));
202 }
203
204 #[cfg(windows)]
205 return Ok(self.canonical_path.clone());
206
207 #[allow(unreachable_code)]
208 Err(io::Error::new(
209 io::ErrorKind::Unsupported,
210 "durable root has no supported operational path",
211 ))
212 }
213
214 pub fn open_directory(&self, relative: impl AsRef<Path>) -> io::Result<DurableRoot> {
215 let relative = relative.as_ref();
216 let components = checked_components(relative)?;
217 let canonical_path = components
218 .iter()
219 .fold(self.canonical_path.clone(), |path, component| {
220 path.join(component)
221 });
222
223 #[cfg(all(
224 unix,
225 any(target_os = "linux", target_os = "android", target_vendor = "apple")
226 ))]
227 {
228 let directory = self.unix_directory(relative)?;
229 return Ok(DurableRoot {
230 canonical_path,
231 directory: std::fs::File::from(directory),
232 });
233 }
234
235 #[cfg(windows)]
236 {
237 let (path, _ancestors) = self.windows_path(relative)?;
238 let directory = open_windows_nofollow(&path)?;
239 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
240 return Ok(DurableRoot {
241 canonical_path,
242 directory,
243 });
244 }
245
246 #[allow(unreachable_code)]
247 Err(io::Error::new(
248 io::ErrorKind::Unsupported,
249 "descriptor-relative durable files are unsupported on this platform",
250 ))
251 }
252
253 pub fn create_directory_all(&self, relative: impl AsRef<Path>) -> io::Result<()> {
254 self.create_directory_all_pinned(relative).map(|_| ())
255 }
256
257 pub fn create_directory_all_pinned(
258 &self,
259 relative: impl AsRef<Path>,
260 ) -> io::Result<DurableRoot> {
261 let components = checked_components(relative.as_ref())?;
262 let canonical_path = components
263 .iter()
264 .fold(self.canonical_path.clone(), |path, component| {
265 path.join(component)
266 });
267
268 #[cfg(all(
269 unix,
270 any(target_os = "linux", target_os = "android", target_vendor = "apple")
271 ))]
272 {
273 use rustix::fs::{fsync, mkdirat, openat, Mode, OFlags};
274 let mut directory = self.duplicate_unix_root()?;
275 for component in components {
276 let opened = openat(
277 &directory,
278 Path::new(&component),
279 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
280 Mode::empty(),
281 );
282 directory = match opened {
283 Ok(opened) => opened,
284 Err(error) if error == rustix::io::Errno::NOENT => {
285 mkdirat(
286 &directory,
287 Path::new(&component),
288 Mode::from_raw_mode(0o700),
289 )
290 .map_err(io::Error::from)?;
291 fsync(&directory).map_err(io::Error::from)?;
292 openat(
293 &directory,
294 Path::new(&component),
295 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
296 Mode::empty(),
297 )
298 .map_err(io::Error::from)?
299 }
300 Err(error) => return Err(io::Error::from(error)),
301 };
302 }
303 return Ok(DurableRoot {
304 canonical_path,
305 directory: std::fs::File::from(directory),
306 });
307 }
308
309 #[cfg(windows)]
310 {
311 let mut path = self.canonical_path.clone();
312 let mut ancestors = Vec::new();
313 for component in components {
314 path.push(component);
315 match open_windows_nofollow(&path) {
316 Ok(directory) => {
317 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
318 ancestors.push(directory);
319 }
320 Err(error) if error.kind() == io::ErrorKind::NotFound => {
321 std::fs::create_dir(&path)?;
322 sync_directory(path.parent().unwrap_or(&self.canonical_path))?;
323 let directory = open_windows_nofollow(&path)?;
324 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
325 ancestors.push(directory);
326 }
327 Err(error) => return Err(error),
328 }
329 }
330 let directory = ancestors.pop().ok_or_else(|| {
331 io::Error::new(
332 io::ErrorKind::InvalidInput,
333 "durable relative path is empty",
334 )
335 })?;
336 return Ok(DurableRoot {
337 canonical_path,
338 directory,
339 });
340 }
341
342 #[allow(unreachable_code)]
343 Err(io::Error::new(
344 io::ErrorKind::Unsupported,
345 "descriptor-relative durable files are unsupported on this platform",
346 ))
347 }
348
349 pub fn create_directory_new(&self, relative: impl AsRef<Path>) -> io::Result<()> {
350 #[cfg(all(
351 unix,
352 any(target_os = "linux", target_os = "android", target_vendor = "apple")
353 ))]
354 {
355 use rustix::fs::{fsync, mkdirat, Mode};
356 let (directory, name) = self.unix_parent(relative.as_ref())?;
357 mkdirat(&directory, Path::new(&name), Mode::from_raw_mode(0o700))
358 .map_err(io::Error::from)?;
359 return fsync(directory).map_err(io::Error::from);
360 }
361
362 #[cfg(windows)]
363 {
364 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
365 std::fs::create_dir(&path)?;
366 return sync_directory(path.parent().unwrap_or(&self.canonical_path));
367 }
368
369 #[allow(unreachable_code)]
370 Err(io::Error::new(
371 io::ErrorKind::Unsupported,
372 "descriptor-relative durable files are unsupported on this platform",
373 ))
374 }
375
376 pub fn open_regular(&self, relative: impl AsRef<Path>) -> io::Result<std::fs::File> {
377 #[cfg(all(
378 unix,
379 any(target_os = "linux", target_os = "android", target_vendor = "apple")
380 ))]
381 {
382 use rustix::fs::{fstat, openat, FileType, Mode, OFlags};
383 let (directory, name) = self.unix_parent(relative.as_ref())?;
384 let file = openat(
385 &directory,
386 Path::new(&name),
387 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
388 Mode::empty(),
389 )
390 .map_err(io::Error::from)?;
391 if FileType::from_raw_mode(fstat(&file).map_err(io::Error::from)?.st_mode)
392 != FileType::RegularFile
393 {
394 return Err(io::Error::new(
395 io::ErrorKind::InvalidInput,
396 "durable entry is not a regular file",
397 ));
398 }
399 return Ok(std::fs::File::from(file));
400 }
401
402 #[cfg(windows)]
403 {
404 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
405 let file = open_windows_nofollow(&path)?;
406 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
407 return Ok(file);
408 }
409
410 #[allow(unreachable_code)]
411 Err(io::Error::new(
412 io::ErrorKind::Unsupported,
413 "descriptor-relative durable files are unsupported on this platform",
414 ))
415 }
416
417 pub(crate) fn open_regular_read_write(
418 &self,
419 relative: impl AsRef<Path>,
420 ) -> io::Result<std::fs::File> {
421 #[cfg(all(
422 unix,
423 any(target_os = "linux", target_os = "android", target_vendor = "apple")
424 ))]
425 {
426 use rustix::fs::{fstat, openat, FileType, Mode, OFlags};
427 let (directory, name) = self.unix_parent(relative.as_ref())?;
428 let file = openat(
429 &directory,
430 Path::new(&name),
431 OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
432 Mode::empty(),
433 )
434 .map_err(io::Error::from)?;
435 if FileType::from_raw_mode(fstat(&file).map_err(io::Error::from)?.st_mode)
436 != FileType::RegularFile
437 {
438 return Err(io::Error::new(
439 io::ErrorKind::InvalidInput,
440 "durable entry is not a regular file",
441 ));
442 }
443 return Ok(std::fs::File::from(file));
444 }
445
446 #[cfg(windows)]
447 {
448 use std::os::windows::fs::OpenOptionsExt;
449 use windows_sys::Win32::Storage::FileSystem::{
450 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
451 };
452 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
453 let file = std::fs::OpenOptions::new()
454 .read(true)
455 .write(true)
456 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
457 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
458 .open(&path)?;
459 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
460 return Ok(file);
461 }
462
463 #[allow(unreachable_code)]
464 Err(io::Error::new(
465 io::ErrorKind::Unsupported,
466 "descriptor-relative durable files are unsupported on this platform",
467 ))
468 }
469
470 pub fn entry_exists(&self, relative: impl AsRef<Path>) -> io::Result<bool> {
471 #[cfg(all(
472 unix,
473 any(target_os = "linux", target_os = "android", target_vendor = "apple")
474 ))]
475 {
476 use rustix::fs::{openat, Mode, OFlags};
477 let (directory, name) = self.unix_parent(relative.as_ref())?;
478 return match openat(
479 &directory,
480 Path::new(&name),
481 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
482 Mode::empty(),
483 ) {
484 Ok(_) => Ok(true),
485 Err(error) if error == rustix::io::Errno::NOENT => Ok(false),
486 Err(error) => Err(io::Error::from(error)),
487 };
488 }
489
490 #[cfg(windows)]
491 {
492 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
493 return match open_windows_nofollow(&path) {
494 Ok(file) => {
495 ensure_windows_not_reparse(&file, &path).map_err(mongrel_error_to_io)?;
496 Ok(true)
497 }
498 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
499 Err(error) => Err(error),
500 };
501 }
502
503 #[allow(unreachable_code)]
504 Err(io::Error::new(
505 io::ErrorKind::Unsupported,
506 "descriptor-relative durable files are unsupported on this platform",
507 ))
508 }
509
510 pub fn write_new(&self, relative: impl AsRef<Path>, bytes: &[u8]) -> io::Result<()> {
511 let relative = relative.as_ref();
512 let mut file = self.open_create_new(relative)?;
513 let result = (|| {
514 file.write_all(bytes)?;
515 file.sync_all()
516 })();
517 if result.is_err() {
518 let _ = self.remove_file(relative);
519 return result;
520 }
521 self.sync_relative_parent(relative)
522 }
523
524 pub(crate) fn create_regular_new(
525 &self,
526 relative: impl AsRef<Path>,
527 ) -> io::Result<std::fs::File> {
528 self.open_create_new(relative.as_ref())
529 }
530
531 pub(crate) fn sync_entry_parent(&self, relative: impl AsRef<Path>) -> io::Result<()> {
532 self.sync_relative_parent(relative.as_ref())
533 }
534
535 pub fn copy_new_from(
536 &self,
537 relative: impl AsRef<Path>,
538 source: &mut std::fs::File,
539 ) -> io::Result<u64> {
540 let relative = relative.as_ref();
541 let mut destination = self.open_create_new(relative)?;
542 let result = (|| {
543 let bytes = io::copy(source, &mut destination)?;
544 destination.sync_all()?;
545 Ok(bytes)
546 })();
547 if result.is_err() {
548 let _ = self.remove_file(relative);
549 return result;
550 }
551 self.sync_relative_parent(relative)?;
552 result
553 }
554
555 pub fn write_atomic(&self, relative: impl AsRef<Path>, bytes: &[u8]) -> io::Result<()> {
556 self.write_atomic_with_after(relative, bytes, || {})
557 }
558
559 pub(crate) fn write_atomic_with_after<F>(
562 &self,
563 relative: impl AsRef<Path>,
564 bytes: &[u8],
565 after_publish: F,
566 ) -> io::Result<()>
567 where
568 F: FnOnce(),
569 {
570 self.write_atomic_controlled_with_after(
571 relative,
572 bytes,
573 || Ok::<(), io::Error>(()),
574 after_publish,
575 )
576 }
577
578 pub(crate) fn write_atomic_controlled_with_after<B, A, E>(
582 &self,
583 relative: impl AsRef<Path>,
584 bytes: &[u8],
585 before_publish: B,
586 after_publish: A,
587 ) -> std::result::Result<(), E>
588 where
589 B: FnOnce() -> std::result::Result<(), E>,
590 A: FnOnce(),
591 E: From<io::Error>,
592 {
593 let relative = relative.as_ref();
594 let (_, name) = checked_parent(relative).map_err(E::from)?;
595 let mut before_publish = Some(before_publish);
596 let mut after_publish = Some(after_publish);
597 for _ in 0..128 {
598 let mut nonce = [0_u8; 8];
599 getrandom::getrandom(&mut nonce)
600 .map_err(|error| E::from(io::Error::other(error.to_string())))?;
601 let suffix = nonce
602 .iter()
603 .map(|byte| format!("{byte:02x}"))
604 .collect::<String>();
605 let temporary = relative.with_file_name(format!(
606 ".{}.tmp-{}-{suffix}",
607 name.to_string_lossy(),
608 std::process::id()
609 ));
610 match self.write_new(&temporary, bytes) {
611 Ok(()) => {
612 let prepare = before_publish.take().ok_or_else(|| {
613 E::from(io::Error::other(
614 "durable atomic prepare callback already consumed",
615 ))
616 })?;
617 if let Err(error) = prepare() {
618 let _ = self.remove_file(&temporary);
619 return Err(error);
620 }
621 let publish = after_publish.take().ok_or_else(|| {
622 E::from(io::Error::other(
623 "durable atomic publish callback already consumed",
624 ))
625 })?;
626 let result = self.replace_file_with_after(&temporary, relative, publish);
627 if result.is_err() {
628 let _ = self.remove_file(&temporary);
629 }
630 return result.map_err(E::from);
631 }
632 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => continue,
633 Err(error) => return Err(E::from(error)),
634 }
635 }
636 Err(E::from(io::Error::new(
637 io::ErrorKind::AlreadyExists,
638 "could not allocate durable temporary file",
639 )))
640 }
641
642 pub fn open_lock_file(&self, relative: impl AsRef<Path>) -> io::Result<std::fs::File> {
643 #[cfg(all(
644 unix,
645 any(target_os = "linux", target_os = "android", target_vendor = "apple")
646 ))]
647 {
648 use rustix::fs::{fstat, openat, FileType, Mode, OFlags};
649 let (directory, name) = self.unix_parent(relative.as_ref())?;
650 let file = openat(
651 &directory,
652 Path::new(&name),
653 OFlags::RDWR | OFlags::CREATE | OFlags::CLOEXEC | OFlags::NOFOLLOW,
654 Mode::from_raw_mode(0o600),
655 )
656 .map_err(io::Error::from)?;
657 if FileType::from_raw_mode(fstat(&file).map_err(io::Error::from)?.st_mode)
658 != FileType::RegularFile
659 {
660 return Err(io::Error::new(
661 io::ErrorKind::InvalidInput,
662 "durable lock is not a regular file",
663 ));
664 }
665 return Ok(std::fs::File::from(file));
666 }
667
668 #[cfg(windows)]
669 {
670 use std::os::windows::fs::OpenOptionsExt;
671 use windows_sys::Win32::Storage::FileSystem::{
672 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
673 };
674 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
675 let file = std::fs::OpenOptions::new()
676 .read(true)
677 .write(true)
678 .create(true)
679 .truncate(false)
680 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
681 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
682 .open(&path)?;
683 ensure_windows_regular(&file, &path).map_err(mongrel_error_to_io)?;
684 return Ok(file);
685 }
686
687 #[allow(unreachable_code)]
688 Err(io::Error::new(
689 io::ErrorKind::Unsupported,
690 "descriptor-relative durable files are unsupported on this platform",
691 ))
692 }
693
694 pub fn list_regular_files(&self, relative: impl AsRef<Path>) -> io::Result<Vec<OsString>> {
695 #[cfg(all(
696 unix,
697 any(target_os = "linux", target_os = "android", target_vendor = "apple")
698 ))]
699 {
700 use rustix::fs::{fstat, openat, Dir, FileType, Mode, OFlags};
701 let relative = relative.as_ref();
702 let directory = if relative.as_os_str().is_empty() || relative == Path::new(".") {
703 self.duplicate_unix_root()?
704 } else {
705 self.unix_directory(relative)?
706 };
707 let mut entries = Dir::read_from(&directory).map_err(io::Error::from)?;
708 let mut names = Vec::new();
709 for entry in &mut entries {
710 let entry = entry.map_err(io::Error::from)?;
711 use std::os::unix::ffi::OsStrExt;
712 let bytes = entry.file_name().to_bytes();
713 if bytes == b"." || bytes == b".." {
714 continue;
715 }
716 let name = OsStr::from_bytes(bytes).to_os_string();
717 let child = openat(
718 &directory,
719 Path::new(&name),
720 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
721 Mode::empty(),
722 )
723 .map_err(io::Error::from)?;
724 if FileType::from_raw_mode(fstat(&child).map_err(io::Error::from)?.st_mode)
725 != FileType::RegularFile
726 {
727 return Err(io::Error::new(
728 io::ErrorKind::InvalidInput,
729 "durable directory contains a non-regular entry",
730 ));
731 }
732 names.push(name);
733 }
734 names.sort();
735 return Ok(names);
736 }
737
738 #[cfg(windows)]
739 {
740 let relative = relative.as_ref();
741 let (path, mut ancestors) =
742 if relative.as_os_str().is_empty() || relative == Path::new(".") {
743 (self.canonical_path.clone(), Vec::new())
744 } else {
745 self.windows_path(relative)?
746 };
747 let directory = open_windows_nofollow(&path)?;
748 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
749 ancestors.push(directory);
750 let mut names = Vec::new();
751 for entry in std::fs::read_dir(&path)? {
752 let entry = entry?;
753 let child = open_windows_nofollow(&entry.path())?;
754 ensure_windows_regular(&child, &entry.path()).map_err(mongrel_error_to_io)?;
755 names.push(entry.file_name());
756 }
757 names.sort();
758 return Ok(names);
759 }
760
761 #[allow(unreachable_code)]
762 Err(io::Error::new(
763 io::ErrorKind::Unsupported,
764 "descriptor-relative durable files are unsupported on this platform",
765 ))
766 }
767
768 pub(crate) fn walk_regular_files<P, D, F>(
771 &self,
772 mut include: P,
773 mut on_directory: D,
774 mut on_file: F,
775 ) -> crate::Result<()>
776 where
777 P: FnMut(&Path, bool) -> crate::Result<bool>,
778 D: FnMut(&Path) -> crate::Result<()>,
779 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
780 {
781 #[cfg(all(
782 unix,
783 any(target_os = "linux", target_os = "android", target_vendor = "apple")
784 ))]
785 {
786 return walk_unix_directory(
787 self.duplicate_unix_root()?,
788 Path::new(""),
789 &mut include,
790 &mut on_directory,
791 &mut on_file,
792 );
793 }
794
795 #[cfg(windows)]
796 {
797 return walk_windows_directory(
798 &self.canonical_path,
799 Path::new(""),
800 self.directory.try_clone()?,
801 &mut include,
802 &mut on_directory,
803 &mut on_file,
804 );
805 }
806
807 #[allow(unreachable_code)]
808 {
809 let _ = (&mut include, &mut on_directory, &mut on_file);
810 Err(crate::MongrelError::Other(
811 "no-follow directory traversal is unsupported on this platform".into(),
812 ))
813 }
814 }
815
816 pub fn remove_file(&self, relative: impl AsRef<Path>) -> io::Result<()> {
817 #[cfg(all(
818 unix,
819 any(target_os = "linux", target_os = "android", target_vendor = "apple")
820 ))]
821 {
822 use rustix::fs::{fsync, unlinkat, AtFlags};
823 let (directory, name) = self.unix_parent(relative.as_ref())?;
824 match unlinkat(&directory, Path::new(&name), AtFlags::empty()) {
825 Ok(()) => fsync(&directory).map_err(io::Error::from),
826 Err(error) if error == rustix::io::Errno::NOENT => Ok(()),
827 Err(error) => Err(io::Error::from(error)),
828 }
829 }
830
831 #[cfg(windows)]
832 {
833 let (path, _ancestors) = self.windows_path(relative.as_ref())?;
834 match std::fs::remove_file(&path) {
835 Ok(()) => sync_directory(path.parent().unwrap_or(&self.canonical_path)),
836 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(()),
837 Err(error) => Err(error),
838 }
839 }
840
841 #[cfg(not(any(
842 windows,
843 all(
844 unix,
845 any(target_os = "linux", target_os = "android", target_vendor = "apple")
846 )
847 )))]
848 Err(io::Error::new(
849 io::ErrorKind::Unsupported,
850 "descriptor-relative durable files are unsupported on this platform",
851 ))
852 }
853
854 pub(crate) fn rename_file_new(
855 &self,
856 source: impl AsRef<Path>,
857 destination: impl AsRef<Path>,
858 ) -> io::Result<()> {
859 #[cfg(all(
860 unix,
861 any(target_os = "linux", target_os = "android", target_vendor = "apple")
862 ))]
863 {
864 use rustix::fs::{fsync, renameat_with, RenameFlags};
865 use std::os::fd::AsRawFd;
866 let (source_parent, source_name) = self.unix_parent(source.as_ref())?;
867 let (destination_parent, destination_name) = self.unix_parent(destination.as_ref())?;
868 renameat_with(
869 &source_parent,
870 Path::new(&source_name),
871 &destination_parent,
872 Path::new(&destination_name),
873 RenameFlags::NOREPLACE,
874 )
875 .map_err(io::Error::from)?;
876 fsync(&destination_parent).map_err(io::Error::from)?;
877 if source_parent.as_raw_fd() != destination_parent.as_raw_fd() {
878 fsync(&source_parent).map_err(io::Error::from)?;
879 }
880 return Ok(());
881 }
882
883 #[cfg(windows)]
884 {
885 let (source, _source_ancestors) = self.windows_path(source.as_ref())?;
886 let (destination, _destination_ancestors) = self.windows_path(destination.as_ref())?;
887 return rename(&source, &destination);
888 }
889
890 #[allow(unreachable_code)]
891 Err(io::Error::new(
892 io::ErrorKind::Unsupported,
893 "descriptor-relative file rename is unsupported on this platform",
894 ))
895 }
896
897 pub fn rename_directory_new(
898 &self,
899 source: impl AsRef<Path>,
900 destination_root: &DurableRoot,
901 destination: impl AsRef<Path>,
902 ) -> io::Result<()> {
903 self.rename_directory_new_with_after(source, destination_root, destination, || {})
904 }
905
906 pub fn rename_directory_new_with_after<F>(
907 &self,
908 source: impl AsRef<Path>,
909 destination_root: &DurableRoot,
910 destination: impl AsRef<Path>,
911 after_publish: F,
912 ) -> io::Result<()>
913 where
914 F: FnOnce(),
915 {
916 #[cfg(all(
917 unix,
918 any(target_os = "linux", target_os = "android", target_vendor = "apple")
919 ))]
920 {
921 use rustix::fs::{fsync, renameat_with, RenameFlags};
922 let (source_parent, source_name) = self.unix_parent(source.as_ref())?;
923 let (destination_parent, destination_name) =
924 destination_root.unix_parent(destination.as_ref())?;
925 renameat_with(
926 &source_parent,
927 Path::new(&source_name),
928 &destination_parent,
929 Path::new(&destination_name),
930 RenameFlags::NOREPLACE,
931 )
932 .map_err(io::Error::from)?;
933 after_publish();
934 fsync(&destination_parent).map_err(io::Error::from)?;
935 return fsync(&source_parent).map_err(io::Error::from);
936 }
937
938 #[cfg(windows)]
939 {
940 let (source, mut source_ancestors) = self.windows_path(source.as_ref())?;
941 let (destination, destination_ancestors) =
942 destination_root.windows_path(destination.as_ref())?;
943 source_ancestors.extend(destination_ancestors);
944 let result = rename(&source, &destination);
945 if result.is_ok() {
946 after_publish();
947 }
948 return result;
949 }
950
951 #[allow(unreachable_code)]
952 Err(io::Error::new(
953 io::ErrorKind::Unsupported,
954 "descriptor-relative durable files are unsupported on this platform",
955 ))
956 }
957
958 pub fn remove_directory_all(&self, relative: impl AsRef<Path>) -> io::Result<()> {
959 #[cfg(all(
960 unix,
961 any(target_os = "linux", target_os = "android", target_vendor = "apple")
962 ))]
963 {
964 use rustix::fs::{fsync, unlinkat, AtFlags};
965 let (parent, name) = self.unix_parent(relative.as_ref())?;
966 let directory = match self.unix_directory(relative.as_ref()) {
967 Ok(directory) => directory,
968 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
969 Err(error) => return Err(error),
970 };
971 remove_unix_directory_contents(&directory)?;
972 unlinkat(&parent, Path::new(&name), AtFlags::REMOVEDIR).map_err(io::Error::from)?;
973 return fsync(parent).map_err(io::Error::from);
974 }
975
976 #[cfg(windows)]
977 {
978 let (path, mut ancestors) = self.windows_path(relative.as_ref())?;
979 let directory = match open_windows_nofollow(&path) {
980 Ok(directory) => directory,
981 Err(error) if error.kind() == io::ErrorKind::NotFound => return Ok(()),
982 Err(error) => return Err(error),
983 };
984 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
985 remove_windows_directory_contents(&path, &directory)?;
986 ancestors.push(directory);
987 std::fs::remove_dir(&path)?;
988 return sync_directory(path.parent().unwrap_or(&self.canonical_path));
989 }
990
991 #[allow(unreachable_code)]
992 Err(io::Error::new(
993 io::ErrorKind::Unsupported,
994 "descriptor-relative durable files are unsupported on this platform",
995 ))
996 }
997
998 #[cfg(all(
999 unix,
1000 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1001 ))]
1002 fn duplicate_unix_root(&self) -> io::Result<rustix::fd::OwnedFd> {
1003 use rustix::fs::{openat, Mode, OFlags};
1004 openat(
1005 &self.directory,
1006 Path::new("."),
1007 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
1008 Mode::empty(),
1009 )
1010 .map_err(io::Error::from)
1011 }
1012
1013 #[cfg(all(
1014 unix,
1015 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1016 ))]
1017 fn unix_directory(&self, relative: &Path) -> io::Result<rustix::fd::OwnedFd> {
1018 use rustix::fs::{openat, Mode, OFlags};
1019 let mut directory = self.duplicate_unix_root()?;
1020 for component in checked_components_allow_empty(relative)? {
1021 directory = openat(
1022 &directory,
1023 Path::new(&component),
1024 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
1025 Mode::empty(),
1026 )
1027 .map_err(io::Error::from)?;
1028 }
1029 Ok(directory)
1030 }
1031
1032 #[cfg(all(
1033 unix,
1034 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1035 ))]
1036 fn unix_parent(&self, relative: &Path) -> io::Result<(rustix::fd::OwnedFd, OsString)> {
1037 let (parent, name) = checked_parent(relative)?;
1038 Ok((self.unix_directory(&parent)?, name))
1039 }
1040
1041 #[cfg(windows)]
1042 fn windows_path(&self, relative: &Path) -> io::Result<(PathBuf, Vec<std::fs::File>)> {
1043 let components = checked_components(relative)?;
1044 let mut path = self.canonical_path.clone();
1045 let mut ancestors = Vec::new();
1046 for component in components.iter().take(components.len().saturating_sub(1)) {
1047 path.push(component);
1048 let directory = open_windows_nofollow(&path)?;
1049 ensure_windows_directory(&directory, &path).map_err(mongrel_error_to_io)?;
1050 ancestors.push(directory);
1051 }
1052 if let Some(name) = components.last() {
1053 path.push(name);
1054 }
1055 Ok((path, ancestors))
1056 }
1057
1058 fn open_create_new(&self, relative: &Path) -> io::Result<std::fs::File> {
1059 #[cfg(all(
1060 unix,
1061 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1062 ))]
1063 {
1064 use rustix::fs::{openat, Mode, OFlags};
1065 let (directory, name) = self.unix_parent(relative)?;
1066 return openat(
1067 &directory,
1068 Path::new(&name),
1069 OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC | OFlags::NOFOLLOW,
1070 Mode::from_raw_mode(0o600),
1071 )
1072 .map(std::fs::File::from)
1073 .map_err(io::Error::from);
1074 }
1075
1076 #[cfg(windows)]
1077 {
1078 use std::os::windows::fs::OpenOptionsExt;
1079 use windows_sys::Win32::Storage::FileSystem::{
1080 FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
1081 };
1082 let (path, _ancestors) = self.windows_path(relative)?;
1083 return std::fs::OpenOptions::new()
1084 .write(true)
1085 .create_new(true)
1086 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1087 .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
1088 .open(path);
1089 }
1090
1091 #[allow(unreachable_code)]
1092 Err(io::Error::new(
1093 io::ErrorKind::Unsupported,
1094 "descriptor-relative durable files are unsupported on this platform",
1095 ))
1096 }
1097
1098 fn replace_file_with_after<F>(
1099 &self,
1100 source: &Path,
1101 destination: &Path,
1102 after_publish: F,
1103 ) -> io::Result<()>
1104 where
1105 F: FnOnce(),
1106 {
1107 #[cfg(all(
1108 unix,
1109 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1110 ))]
1111 {
1112 use rustix::fs::{fsync, renameat};
1113 let (source_parent, source_name) = self.unix_parent(source)?;
1114 let (destination_parent, destination_name) = self.unix_parent(destination)?;
1115 renameat(
1116 &source_parent,
1117 Path::new(&source_name),
1118 &destination_parent,
1119 Path::new(&destination_name),
1120 )
1121 .map_err(io::Error::from)?;
1122 after_publish();
1123 fsync(&destination_parent).map_err(io::Error::from)?;
1124 if source.parent() != destination.parent() {
1125 fsync(&source_parent).map_err(io::Error::from)?;
1126 }
1127 return Ok(());
1128 }
1129
1130 #[cfg(windows)]
1131 {
1132 let (source, mut source_ancestors) = self.windows_path(source)?;
1133 let (destination, destination_ancestors) = self.windows_path(destination)?;
1134 source_ancestors.extend(destination_ancestors);
1135 return replace_with_after(&source, &destination, after_publish);
1136 }
1137
1138 #[allow(unreachable_code)]
1139 Err(io::Error::new(
1140 io::ErrorKind::Unsupported,
1141 "descriptor-relative durable files are unsupported on this platform",
1142 ))
1143 }
1144
1145 fn sync_relative_parent(&self, relative: &Path) -> io::Result<()> {
1146 #[cfg(all(
1147 unix,
1148 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1149 ))]
1150 {
1151 use rustix::fs::fsync;
1152 let (directory, _) = self.unix_parent(relative)?;
1153 return fsync(directory).map_err(io::Error::from);
1154 }
1155
1156 #[cfg(windows)]
1157 {
1158 let (path, _ancestors) = self.windows_path(relative)?;
1159 return sync_directory(path.parent().unwrap_or(&self.canonical_path));
1160 }
1161
1162 #[allow(unreachable_code)]
1163 Err(io::Error::new(
1164 io::ErrorKind::Unsupported,
1165 "descriptor-relative durable files are unsupported on this platform",
1166 ))
1167 }
1168}
1169
1170#[cfg(all(
1171 unix,
1172 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1173))]
1174fn open_unix_directory_path(path: &Path) -> io::Result<std::os::fd::OwnedFd> {
1175 use rustix::fs::{openat, Mode, OFlags, CWD};
1176 use std::path::Component;
1177
1178 let flags = OFlags::RDONLY | OFlags::CLOEXEC | OFlags::DIRECTORY;
1179 for component in path.components() {
1180 match component {
1181 Component::RootDir | Component::CurDir | Component::Normal(_) => {}
1182 Component::ParentDir | Component::Prefix(_) => {
1183 return Err(io::Error::new(
1184 io::ErrorKind::InvalidInput,
1185 "durable root path contains an unsafe component",
1186 ))
1187 }
1188 }
1189 }
1190 openat(CWD, path, flags, Mode::empty()).map_err(io::Error::from)
1195}
1196
1197#[cfg(all(
1198 unix,
1199 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1200))]
1201fn remove_unix_directory_contents(directory: &rustix::fd::OwnedFd) -> io::Result<()> {
1202 use rustix::fs::{fstat, fsync, openat, unlinkat, AtFlags, Dir, FileType, Mode, OFlags};
1203 use std::os::unix::ffi::OsStrExt;
1204
1205 let mut entries = Dir::read_from(directory).map_err(io::Error::from)?;
1206 let mut names = Vec::new();
1207 for entry in &mut entries {
1208 let entry = entry.map_err(io::Error::from)?;
1209 let bytes = entry.file_name().to_bytes();
1210 if bytes != b"." && bytes != b".." {
1211 names.push(OsStr::from_bytes(bytes).to_os_string());
1212 }
1213 }
1214 for name in names {
1215 let child = openat(
1216 directory,
1217 Path::new(&name),
1218 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
1219 Mode::empty(),
1220 )
1221 .map_err(io::Error::from)?;
1222 match FileType::from_raw_mode(fstat(&child).map_err(io::Error::from)?.st_mode) {
1223 FileType::Directory => {
1224 remove_unix_directory_contents(&child)?;
1225 unlinkat(directory, Path::new(&name), AtFlags::REMOVEDIR)
1226 .map_err(io::Error::from)?;
1227 }
1228 FileType::RegularFile => {
1229 unlinkat(directory, Path::new(&name), AtFlags::empty()).map_err(io::Error::from)?;
1230 }
1231 _ => {
1232 return Err(io::Error::new(
1233 io::ErrorKind::InvalidInput,
1234 "refuses non-regular durable entry",
1235 ))
1236 }
1237 }
1238 }
1239 fsync(directory).map_err(io::Error::from)
1240}
1241
1242#[cfg(windows)]
1243fn remove_windows_directory_contents(path: &Path, _directory: &std::fs::File) -> io::Result<()> {
1244 for entry in std::fs::read_dir(path)? {
1245 let entry = entry?;
1246 let child_path = entry.path();
1247 let child = open_windows_nofollow(&child_path)?;
1248 let metadata =
1249 ensure_windows_not_reparse(&child, &child_path).map_err(mongrel_error_to_io)?;
1250 if metadata.is_dir() {
1251 remove_windows_directory_contents(&child_path, &child)?;
1252 std::fs::remove_dir(&child_path)?;
1253 } else if metadata.is_file() {
1254 std::fs::remove_file(&child_path)?;
1255 } else {
1256 return Err(io::Error::new(
1257 io::ErrorKind::InvalidInput,
1258 "refuses non-regular durable entry",
1259 ));
1260 }
1261 }
1262 sync_directory(path)
1263}
1264
1265fn checked_components(path: &Path) -> io::Result<Vec<OsString>> {
1266 let components = checked_components_allow_empty(path)?;
1267 if components.is_empty() {
1268 return Err(io::Error::new(
1269 io::ErrorKind::InvalidInput,
1270 "durable relative path must not be empty",
1271 ));
1272 }
1273 Ok(components)
1274}
1275
1276fn checked_components_allow_empty(path: &Path) -> io::Result<Vec<OsString>> {
1277 let mut components = Vec::new();
1278 for component in path.components() {
1279 match component {
1280 std::path::Component::Normal(name) => components.push(name.to_os_string()),
1281 _ => {
1282 return Err(io::Error::new(
1283 io::ErrorKind::InvalidInput,
1284 format!("invalid durable relative path {}", path.display()),
1285 ))
1286 }
1287 }
1288 }
1289 Ok(components)
1290}
1291
1292fn checked_parent(path: &Path) -> io::Result<(PathBuf, OsString)> {
1293 let components = checked_components(path)?;
1294 let name = components.last().cloned().unwrap();
1295 let parent = components[..components.len() - 1]
1296 .iter()
1297 .collect::<PathBuf>();
1298 Ok((parent, name))
1299}
1300
1301#[cfg(windows)]
1302fn mongrel_error_to_io(error: crate::MongrelError) -> io::Error {
1303 io::Error::new(io::ErrorKind::InvalidInput, error.to_string())
1304}
1305
1306pub(crate) fn open_regular_nofollow(path: &Path) -> crate::Result<std::fs::File> {
1308 #[cfg(all(
1309 unix,
1310 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1311 ))]
1312 {
1313 use rustix::fs::{fstat, openat, FileType, Mode, OFlags, CWD};
1314
1315 let fd = openat(
1316 CWD,
1317 path,
1318 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
1319 Mode::empty(),
1320 )
1321 .map_err(io::Error::from)?;
1322 if FileType::from_raw_mode(fstat(&fd).map_err(io::Error::from)?.st_mode)
1323 != FileType::RegularFile
1324 {
1325 return Err(crate::MongrelError::InvalidArgument(format!(
1326 "refuses non-regular file {}",
1327 path.display()
1328 )));
1329 }
1330 Ok(std::fs::File::from(fd))
1331 }
1332
1333 #[cfg(windows)]
1334 {
1335 let file = open_windows_nofollow(path)?;
1336 ensure_windows_regular(&file, path)?;
1337 return Ok(file);
1338 }
1339
1340 #[cfg(not(any(
1341 windows,
1342 all(
1343 unix,
1344 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1345 )
1346 )))]
1347 {
1348 let _ = path;
1349 Err(crate::MongrelError::Other(
1350 "no-follow file opens are unsupported on this platform".into(),
1351 ))
1352 }
1353}
1354
1355pub(crate) fn walk_regular_files_nofollow<P, D, F>(
1359 root: &Path,
1360 mut include: P,
1361 mut on_directory: D,
1362 mut on_file: F,
1363) -> crate::Result<()>
1364where
1365 P: FnMut(&Path, bool) -> crate::Result<bool>,
1366 D: FnMut(&Path) -> crate::Result<()>,
1367 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
1368{
1369 #[cfg(all(
1370 unix,
1371 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1372 ))]
1373 {
1374 use rustix::fs::{openat, Mode, OFlags, CWD};
1375
1376 let directory = openat(
1377 CWD,
1378 root,
1379 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::DIRECTORY,
1380 Mode::empty(),
1381 )
1382 .map_err(io::Error::from)?;
1383 walk_unix_directory(
1384 directory,
1385 Path::new(""),
1386 &mut include,
1387 &mut on_directory,
1388 &mut on_file,
1389 )
1390 }
1391
1392 #[cfg(windows)]
1393 {
1394 let directory = open_windows_nofollow(root)?;
1395 ensure_windows_directory(&directory, root)?;
1396 return walk_windows_directory(
1397 root,
1398 Path::new(""),
1399 directory,
1400 &mut include,
1401 &mut on_directory,
1402 &mut on_file,
1403 );
1404 }
1405
1406 #[cfg(not(any(
1407 windows,
1408 all(
1409 unix,
1410 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1411 )
1412 )))]
1413 {
1414 let _ = (root, &mut include, &mut on_directory, &mut on_file);
1415 Err(crate::MongrelError::Other(
1416 "no-follow directory traversal is unsupported on this platform".into(),
1417 ))
1418 }
1419}
1420
1421#[cfg(all(
1422 unix,
1423 any(target_os = "linux", target_os = "android", target_vendor = "apple")
1424))]
1425fn walk_unix_directory<P, D, F>(
1426 directory: rustix::fd::OwnedFd,
1427 relative: &Path,
1428 include: &mut P,
1429 on_directory: &mut D,
1430 on_file: &mut F,
1431) -> crate::Result<()>
1432where
1433 P: FnMut(&Path, bool) -> crate::Result<bool>,
1434 D: FnMut(&Path) -> crate::Result<()>,
1435 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
1436{
1437 use rustix::fs::{fstat, openat, Dir, FileType, Mode, OFlags};
1438 use std::ffi::OsStr;
1439 use std::os::unix::ffi::OsStrExt;
1440
1441 let mut entries = Dir::read_from(&directory).map_err(io::Error::from)?;
1442 let mut names = Vec::new();
1443 for entry in &mut entries {
1444 let entry = entry.map_err(io::Error::from)?;
1445 let bytes = entry.file_name().to_bytes();
1446 if bytes != b"." && bytes != b".." {
1447 names.push(OsStr::from_bytes(bytes).to_os_string());
1448 }
1449 }
1450 names.sort();
1451
1452 for name in names {
1453 let child_relative = relative.join(&name);
1454 let child = openat(
1455 &directory,
1456 Path::new(&name),
1457 OFlags::RDONLY | OFlags::CLOEXEC | OFlags::NOFOLLOW | OFlags::NONBLOCK,
1458 Mode::empty(),
1459 )
1460 .map_err(io::Error::from)?;
1461 match FileType::from_raw_mode(fstat(&child).map_err(io::Error::from)?.st_mode) {
1462 FileType::Directory => {
1463 if include(&child_relative, true)? {
1464 on_directory(&child_relative)?;
1465 walk_unix_directory(child, &child_relative, include, on_directory, on_file)?;
1466 }
1467 }
1468 FileType::RegularFile => {
1469 if include(&child_relative, false)? {
1470 on_file(&child_relative, &mut std::fs::File::from(child))?;
1471 }
1472 }
1473 _ => {
1474 return Err(crate::MongrelError::InvalidArgument(format!(
1475 "refuses non-regular entry {}",
1476 child_relative.display()
1477 )));
1478 }
1479 }
1480 }
1481 Ok(())
1482}
1483
1484#[cfg(windows)]
1485fn open_windows_nofollow(path: &Path) -> io::Result<std::fs::File> {
1486 use std::os::windows::fs::OpenOptionsExt;
1487 use windows_sys::Win32::Storage::FileSystem::{
1488 FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT, FILE_SHARE_READ, FILE_SHARE_WRITE,
1489 };
1490
1491 let mut options = std::fs::OpenOptions::new();
1492 options
1493 .read(true)
1494 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
1495 .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT);
1496 options.open(path)
1497}
1498
1499#[cfg(windows)]
1500fn ensure_windows_not_reparse(
1501 file: &std::fs::File,
1502 path: &Path,
1503) -> crate::Result<std::fs::Metadata> {
1504 use std::os::windows::fs::MetadataExt;
1505 use windows_sys::Win32::Storage::FileSystem::FILE_ATTRIBUTE_REPARSE_POINT;
1506
1507 let metadata = file.metadata()?;
1508 if metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1509 return Err(crate::MongrelError::InvalidArgument(format!(
1510 "refuses reparse point {}",
1511 path.display()
1512 )));
1513 }
1514 Ok(metadata)
1515}
1516
1517#[cfg(windows)]
1518fn ensure_windows_regular(file: &std::fs::File, path: &Path) -> crate::Result<()> {
1519 if !ensure_windows_not_reparse(file, path)?.is_file() {
1520 return Err(crate::MongrelError::InvalidArgument(format!(
1521 "refuses non-regular file {}",
1522 path.display()
1523 )));
1524 }
1525 Ok(())
1526}
1527
1528#[cfg(windows)]
1529fn ensure_windows_directory(file: &std::fs::File, path: &Path) -> crate::Result<()> {
1530 if !ensure_windows_not_reparse(file, path)?.is_dir() {
1531 return Err(crate::MongrelError::InvalidArgument(format!(
1532 "refuses non-directory {}",
1533 path.display()
1534 )));
1535 }
1536 Ok(())
1537}
1538
1539#[cfg(windows)]
1540fn walk_windows_directory<P, D, F>(
1541 path: &Path,
1542 relative: &Path,
1543 _directory: std::fs::File,
1544 include: &mut P,
1545 on_directory: &mut D,
1546 on_file: &mut F,
1547) -> crate::Result<()>
1548where
1549 P: FnMut(&Path, bool) -> crate::Result<bool>,
1550 D: FnMut(&Path) -> crate::Result<()>,
1551 F: FnMut(&Path, &mut std::fs::File) -> crate::Result<()>,
1552{
1553 let mut entries = std::fs::read_dir(path)?.collect::<io::Result<Vec<_>>>()?;
1554 entries.sort_by_key(std::fs::DirEntry::file_name);
1555 for entry in entries {
1556 let child_path = entry.path();
1557 let child_relative = relative.join(entry.file_name());
1558 let mut child = open_windows_nofollow(&child_path)?;
1559 let metadata = ensure_windows_not_reparse(&child, &child_path)?;
1560 if metadata.is_dir() {
1561 if include(&child_relative, true)? {
1562 on_directory(&child_relative)?;
1563 walk_windows_directory(
1564 &child_path,
1565 &child_relative,
1566 child,
1567 include,
1568 on_directory,
1569 on_file,
1570 )?;
1571 }
1572 } else if metadata.is_file() {
1573 if include(&child_relative, false)? {
1574 on_file(&child_relative, &mut child)?;
1575 }
1576 } else {
1577 return Err(crate::MongrelError::InvalidArgument(format!(
1578 "refuses non-regular entry {}",
1579 child_path.display()
1580 )));
1581 }
1582 }
1583 Ok(())
1584}
1585
1586fn parent_or_current(path: &Path) -> &Path {
1587 path.parent()
1588 .filter(|parent| !parent.as_os_str().is_empty())
1589 .unwrap_or_else(|| Path::new("."))
1590}
1591
1592pub(crate) fn create_directory_all(path: &Path) -> io::Result<()> {
1594 if path.is_dir() {
1595 return sync_parent(path);
1596 }
1597 if path.exists() {
1598 return Err(io::Error::new(
1599 io::ErrorKind::AlreadyExists,
1600 format!("{} exists and is not a directory", path.display()),
1601 ));
1602 }
1603 let parent = parent_or_current(path);
1604 if !parent.is_dir() {
1605 create_directory_all(parent)?;
1606 }
1607 create_directory(path)
1608}
1609
1610pub(crate) fn create_directory(path: &Path) -> io::Result<()> {
1613 if path.is_dir() {
1614 return sync_parent(path);
1615 }
1616 if path.exists() {
1617 return Err(io::Error::new(
1618 io::ErrorKind::AlreadyExists,
1619 format!("{} exists and is not a directory", path.display()),
1620 ));
1621 }
1622
1623 #[cfg(unix)]
1624 {
1625 match std::fs::create_dir(path) {
1626 Ok(()) => sync_parent(path),
1627 Err(error) if error.kind() == io::ErrorKind::AlreadyExists && path.is_dir() => {
1628 sync_parent(path)
1629 }
1630 Err(error) => Err(error),
1631 }
1632 }
1633
1634 #[cfg(windows)]
1635 {
1636 let parent = parent_or_current(path);
1637 let stage = parent.join(format!(
1638 ".mongreldb-dir-{}-{}",
1639 std::process::id(),
1640 std::time::SystemTime::now()
1641 .duration_since(std::time::UNIX_EPOCH)
1642 .unwrap_or_default()
1643 .as_nanos()
1644 ));
1645 std::fs::create_dir(&stage)?;
1646 match rename(&stage, path) {
1647 Ok(()) => Ok(()),
1648 Err(error) if path.is_dir() => {
1649 let _ = std::fs::remove_dir(&stage);
1650 Ok(())
1651 }
1652 Err(error) => {
1653 let _ = std::fs::remove_dir(&stage);
1654 Err(error)
1655 }
1656 }
1657 }
1658
1659 #[cfg(not(any(unix, windows)))]
1660 {
1661 let _ = path;
1662 Err(io::Error::new(
1663 io::ErrorKind::Unsupported,
1664 "durable directory creation is unsupported on this platform",
1665 ))
1666 }
1667}
1668
1669pub(crate) fn remove_directory_all(path: &Path) -> io::Result<()> {
1671 remove_directory_all_with_after(path, || {})
1672}
1673
1674fn remove_directory_all_with_after<F>(path: &Path, after_unlink: F) -> io::Result<()>
1675where
1676 F: FnOnce(),
1677{
1678 match std::fs::remove_dir_all(path) {
1679 Ok(()) => {
1680 after_unlink();
1681 sync_parent(path)
1682 }
1683 Err(error) if error.kind() == io::ErrorKind::NotFound => {
1684 after_unlink();
1687 sync_parent(path)
1688 }
1689 Err(error) => Err(error),
1690 }
1691}
1692
1693#[cfg_attr(not(feature = "encryption"), allow(dead_code))]
1696pub(crate) fn write_atomic(path: &Path, bytes: &[u8]) -> io::Result<()> {
1697 write_atomic_with_after(path, bytes, || {})
1698}
1699
1700pub(crate) fn write_atomic_with_after<F>(
1703 path: &Path,
1704 bytes: &[u8],
1705 after_publish: F,
1706) -> io::Result<()>
1707where
1708 F: FnOnce(),
1709{
1710 let parent = parent_or_current(path);
1711 if !parent.is_dir() {
1712 create_directory_all(parent)?;
1713 }
1714 let file_name = path.file_name().ok_or_else(|| {
1715 io::Error::new(
1716 io::ErrorKind::InvalidInput,
1717 format!("{} has no file name", path.display()),
1718 )
1719 })?;
1720 let root = DurableRoot::open(parent)?;
1721 root.write_atomic_with_after(Path::new(file_name), bytes, after_publish)
1722}
1723
1724pub(crate) fn rename(source: &Path, destination: &Path) -> io::Result<()> {
1727 rename_with_after(source, destination, || {})
1728}
1729
1730pub(crate) fn rename_with_after<F>(
1733 source: &Path,
1734 destination: &Path,
1735 after_publish: F,
1736) -> io::Result<()>
1737where
1738 F: FnOnce(),
1739{
1740 #[cfg(all(
1741 unix,
1742 any(
1743 target_os = "linux",
1744 target_os = "android",
1745 target_vendor = "apple",
1746 target_os = "redox"
1747 )
1748 ))]
1749 {
1750 use rustix::fs::{renameat_with, RenameFlags, CWD};
1751
1752 renameat_with(CWD, source, CWD, destination, RenameFlags::NOREPLACE)
1753 .map_err(io::Error::from)?;
1754 after_publish();
1755 if source.parent() == destination.parent() {
1756 sync_parent(destination)?;
1757 } else {
1758 sync_parent(destination)?;
1761 sync_parent(source)?;
1762 }
1763 Ok(())
1764 }
1765
1766 #[cfg(all(
1767 unix,
1768 not(any(
1769 target_os = "linux",
1770 target_os = "android",
1771 target_vendor = "apple",
1772 target_os = "redox"
1773 ))
1774 ))]
1775 {
1776 let _ = (source, destination, after_publish);
1777 Err(io::Error::new(
1778 io::ErrorKind::Unsupported,
1779 "atomic no-replace rename is unsupported on this platform",
1780 ))
1781 }
1782
1783 #[cfg(windows)]
1784 {
1785 use std::os::windows::ffi::OsStrExt;
1786 use windows_sys::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_WRITE_THROUGH};
1787
1788 let source = source
1789 .as_os_str()
1790 .encode_wide()
1791 .chain(std::iter::once(0))
1792 .collect::<Vec<_>>();
1793 let destination = destination
1794 .as_os_str()
1795 .encode_wide()
1796 .chain(std::iter::once(0))
1797 .collect::<Vec<_>>();
1798 let result = unsafe {
1799 MoveFileExW(
1800 source.as_ptr(),
1801 destination.as_ptr(),
1802 MOVEFILE_WRITE_THROUGH,
1803 )
1804 };
1805 if result == 0 {
1806 return Err(io::Error::last_os_error());
1807 }
1808 after_publish();
1809 Ok(())
1810 }
1811
1812 #[cfg(not(any(unix, windows)))]
1813 {
1814 let _ = (source, destination, after_publish);
1815 Err(io::Error::new(
1816 io::ErrorKind::Unsupported,
1817 "durable rename is unsupported on this platform",
1818 ))
1819 }
1820}
1821
1822pub(crate) fn sync_directory(path: &Path) -> io::Result<()> {
1824 #[cfg(unix)]
1825 {
1826 std::fs::File::open(path)?.sync_all()
1827 }
1828
1829 #[cfg(windows)]
1830 {
1831 use std::os::windows::ffi::OsStrExt;
1832 use windows_sys::Win32::Foundation::{
1833 CloseHandle, GENERIC_READ, GENERIC_WRITE, INVALID_HANDLE_VALUE,
1834 };
1835 use windows_sys::Win32::Storage::FileSystem::{
1836 CreateFileW, FlushFileBuffers, FILE_FLAG_BACKUP_SEMANTICS, FILE_SHARE_DELETE,
1837 FILE_SHARE_READ, FILE_SHARE_WRITE, OPEN_EXISTING,
1838 };
1839
1840 let path = path
1841 .as_os_str()
1842 .encode_wide()
1843 .chain(std::iter::once(0))
1844 .collect::<Vec<_>>();
1845 let handle = unsafe {
1846 CreateFileW(
1847 path.as_ptr(),
1848 GENERIC_READ | GENERIC_WRITE,
1849 FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
1850 std::ptr::null(),
1851 OPEN_EXISTING,
1852 FILE_FLAG_BACKUP_SEMANTICS,
1853 std::ptr::null_mut(),
1854 )
1855 };
1856 if handle == INVALID_HANDLE_VALUE {
1857 return Err(io::Error::last_os_error());
1858 }
1859 let flushed = unsafe { FlushFileBuffers(handle) };
1860 let flush_error = (flushed == 0).then(io::Error::last_os_error);
1861 let closed = unsafe { CloseHandle(handle) };
1862 if let Some(error) = flush_error {
1863 return Err(error);
1864 }
1865 if closed == 0 {
1866 return Err(io::Error::last_os_error());
1867 }
1868 Ok(())
1869 }
1870
1871 #[cfg(not(any(unix, windows)))]
1872 {
1873 let _ = path;
1874 Err(io::Error::new(
1875 io::ErrorKind::Unsupported,
1876 "directory synchronization is unsupported on this platform",
1877 ))
1878 }
1879}
1880
1881fn sync_parent(path: &Path) -> io::Result<()> {
1882 sync_directory(parent_or_current(path))
1883}
1884
1885pub(crate) fn replace(source: &Path, destination: &Path) -> io::Result<()> {
1887 replace_with_after(source, destination, || {})
1888}
1889
1890pub(crate) fn replace_with_after<F>(
1893 source: &Path,
1894 destination: &Path,
1895 after_publish: F,
1896) -> io::Result<()>
1897where
1898 F: FnOnce(),
1899{
1900 #[cfg(unix)]
1901 {
1902 std::fs::rename(source, destination)?;
1903 after_publish();
1904 std::fs::File::open(destination.parent().unwrap_or_else(|| Path::new(".")))?.sync_all()
1905 }
1906
1907 #[cfg(windows)]
1908 {
1909 use std::os::windows::ffi::OsStrExt;
1910 use windows_sys::Win32::Storage::FileSystem::{
1911 MoveFileExW, MOVEFILE_REPLACE_EXISTING, MOVEFILE_WRITE_THROUGH,
1912 };
1913
1914 let source = source
1915 .as_os_str()
1916 .encode_wide()
1917 .chain(std::iter::once(0))
1918 .collect::<Vec<_>>();
1919 let destination = destination
1920 .as_os_str()
1921 .encode_wide()
1922 .chain(std::iter::once(0))
1923 .collect::<Vec<_>>();
1924 let result = unsafe {
1925 MoveFileExW(
1926 source.as_ptr(),
1927 destination.as_ptr(),
1928 MOVEFILE_REPLACE_EXISTING | MOVEFILE_WRITE_THROUGH,
1929 )
1930 };
1931 if result == 0 {
1932 return Err(io::Error::last_os_error());
1933 }
1934 after_publish();
1935 Ok(())
1936 }
1937
1938 #[cfg(not(any(unix, windows)))]
1939 {
1940 let _ = (source, destination, after_publish);
1941 Err(io::Error::new(
1942 io::ErrorKind::Unsupported,
1943 "durable atomic replacement is unsupported on this platform",
1944 ))
1945 }
1946}
1947
1948#[cfg(all(test, unix))]
1949mod tests {
1950 use super::*;
1951 use std::io::Read;
1952 use std::os::unix::fs::symlink;
1953 use std::sync::{Arc, Barrier};
1954
1955 #[test]
1956 fn no_follow_file_and_tree_reject_symlinks() {
1957 let root = tempfile::tempdir().unwrap();
1958 let outside = tempfile::NamedTempFile::new().unwrap();
1959 let link = root.path().join("escape");
1960 symlink(outside.path(), &link).unwrap();
1961
1962 assert!(open_regular_nofollow(&link).is_err());
1963 let result = walk_regular_files_nofollow(
1964 root.path(),
1965 |_, _| Ok(true),
1966 |_| Ok(()),
1967 |_, file| {
1968 let mut bytes = Vec::new();
1969 file.read_to_end(&mut bytes)?;
1970 Ok(())
1971 },
1972 );
1973 assert!(result.is_err());
1974 }
1975
1976 #[test]
1977 fn descriptor_root_rejects_intermediate_symlinks() {
1978 let root = tempfile::tempdir().unwrap();
1979 let outside = tempfile::tempdir().unwrap();
1980 std::fs::create_dir(root.path().join("state")).unwrap();
1981 std::fs::write(outside.path().join("receipt"), b"outside").unwrap();
1982 symlink(outside.path(), root.path().join("state").join("escape")).unwrap();
1983 let durable = DurableRoot::open(root.path()).unwrap();
1984
1985 assert!(durable.open_regular("state/escape/receipt").is_err());
1986 assert!(durable.write_new("state/escape/new", b"bad").is_err());
1987 assert!(!outside.path().join("new").exists());
1988 }
1989
1990 #[test]
1991 fn atomic_write_ignores_orphaned_fixed_temporary_name() {
1992 let root = tempfile::tempdir().unwrap();
1993 let destination = root.path().join("state");
1994 let orphan = root.path().join(".state.tmp");
1995 std::fs::write(&orphan, b"orphan").unwrap();
1996
1997 write_atomic(&destination, b"published").unwrap();
1998
1999 assert_eq!(std::fs::read(destination).unwrap(), b"published");
2000 assert_eq!(std::fs::read(orphan).unwrap(), b"orphan");
2001 }
2002
2003 #[test]
2004 fn descriptor_atomic_write_reports_visible_publication_before_return() {
2005 let root = tempfile::tempdir().unwrap();
2006 let durable = DurableRoot::open(root.path()).unwrap();
2007 let published = std::cell::Cell::new(false);
2008
2009 durable
2010 .write_atomic_with_after("state", b"published", || {
2011 assert_eq!(
2012 std::fs::read(root.path().join("state")).unwrap(),
2013 b"published"
2014 );
2015 published.set(true);
2016 })
2017 .unwrap();
2018
2019 assert!(published.get());
2020 }
2021
2022 #[test]
2023 fn parent_sync_failure_is_not_swallowed_after_publication() {
2024 let root = tempfile::tempdir().unwrap();
2025 let parent = root.path().join("parent");
2026 let moved = root.path().join("moved");
2027 std::fs::create_dir(&parent).unwrap();
2028 let source = parent.join("source");
2029 let destination = parent.join("destination");
2030 std::fs::write(&source, b"durable").unwrap();
2031
2032 let error = replace_with_after(&source, &destination, || {
2033 std::fs::rename(&parent, &moved).unwrap();
2034 })
2035 .unwrap_err();
2036
2037 assert_eq!(error.kind(), io::ErrorKind::NotFound);
2038 assert_eq!(
2039 std::fs::read(moved.join("destination")).unwrap(),
2040 b"durable"
2041 );
2042 }
2043
2044 #[test]
2045 fn cross_directory_rename_syncs_destination_before_source() {
2046 let root = tempfile::tempdir().unwrap();
2047 let source_parent = root.path().join("source-parent");
2048 let destination_parent = root.path().join("destination-parent");
2049 let moved_source_parent = root.path().join("moved-source-parent");
2050 std::fs::create_dir(&source_parent).unwrap();
2051 std::fs::create_dir(&destination_parent).unwrap();
2052 let source = source_parent.join("source");
2053 let destination = destination_parent.join("destination");
2054 std::fs::write(&source, b"durable").unwrap();
2055
2056 let error = rename_with_after(&source, &destination, || {
2057 std::fs::rename(&source_parent, &moved_source_parent).unwrap();
2058 })
2059 .unwrap_err();
2060
2061 assert_eq!(error.kind(), io::ErrorKind::NotFound);
2062 assert_eq!(std::fs::read(&destination).unwrap(), b"durable");
2063 }
2064
2065 #[test]
2066 fn no_replace_rename_never_clobbers_a_racing_destination() {
2067 for iteration in 0..128 {
2068 let root = tempfile::tempdir().unwrap();
2069 let source = root.path().join("source");
2070 let destination = root.path().join("destination");
2071 std::fs::write(&source, b"source").unwrap();
2072 let barrier = Arc::new(Barrier::new(3));
2073
2074 let create_barrier = Arc::clone(&barrier);
2075 let create_destination = destination.clone();
2076 let creator = std::thread::spawn(move || {
2077 create_barrier.wait();
2078 std::fs::OpenOptions::new()
2079 .write(true)
2080 .create_new(true)
2081 .open(create_destination)
2082 .map(|mut file| file.write_all(b"racer"))
2083 });
2084
2085 let rename_barrier = Arc::clone(&barrier);
2086 let rename_source = source.clone();
2087 let rename_destination = destination.clone();
2088 let renamer = std::thread::spawn(move || {
2089 rename_barrier.wait();
2090 rename(&rename_source, &rename_destination)
2091 });
2092
2093 barrier.wait();
2094 let create_result = creator.join().unwrap();
2095 let rename_result = renamer.join().unwrap();
2096 let destination_bytes = std::fs::read(&destination).unwrap();
2097
2098 match (create_result, rename_result) {
2099 (Ok(Ok(())), Err(error)) => {
2100 assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
2101 assert_eq!(destination_bytes, b"racer", "iteration {iteration}");
2102 assert_eq!(std::fs::read(&source).unwrap(), b"source");
2103 }
2104 (Err(error), Ok(())) => {
2105 assert_eq!(error.kind(), io::ErrorKind::AlreadyExists);
2106 assert_eq!(destination_bytes, b"source", "iteration {iteration}");
2107 assert!(!source.exists());
2108 }
2109 (create_result, rename_result) => panic!(
2110 "unexpected race results at iteration {iteration}: create={create_result:?}, rename={rename_result:?}"
2111 ),
2112 }
2113 }
2114 }
2115
2116 #[test]
2117 fn missing_directory_retry_still_reports_parent_sync_failure() {
2118 let root = tempfile::tempdir().unwrap();
2119 let parent = root.path().join("parent");
2120 let moved_parent = root.path().join("moved-parent");
2121 std::fs::create_dir(&parent).unwrap();
2122 let missing = parent.join("already-removed");
2123
2124 let error = remove_directory_all_with_after(&missing, || {
2125 std::fs::rename(&parent, &moved_parent).unwrap();
2126 })
2127 .unwrap_err();
2128
2129 assert_eq!(error.kind(), io::ErrorKind::NotFound);
2130 }
2131}