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