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