1#[cfg(unix)]
10pub mod tree;
11
12mod directory;
13mod lock_wait;
14pub use directory::{
15 DirectoryCommitError, DirectoryCommitOutcome, DirectoryCommitStep, DirectoryCreationError,
16 admit_directory, commit_existing_directory_with, create_private_directory,
17};
18pub use lock_wait::{
19 SharedFileLock, lock_exclusive, lock_exclusive_with_wait, lock_shared, try_lock_exclusive,
20 unlock,
21};
22#[cfg(unix)]
23mod retained;
24#[cfg(unix)]
25pub use retained::{
26 RetainedWriteError, create_retained_at, publish_retained_at, write_retained_at_with,
27};
28mod named;
29pub use named::{NamedWriteError, write_named_with};
30#[cfg(unix)]
31mod export;
32#[cfg(unix)]
33pub use export::{NonAliasingWriteError, validate_non_aliasing, write_non_aliasing};
34#[cfg(unix)]
35mod symlink;
36#[cfg(unix)]
37pub use symlink::publish_symlink_with;
38mod verified_copy;
39pub use verified_copy::copy_verified;
40mod stream;
41#[cfg(unix)]
42pub use stream::write_at_validated_with;
43pub use stream::{
44 PublicationMode, WriteOptions, write_existing_parent_validated_with, write_validated_with,
45 write_with,
46};
47
48#[cfg(test)]
49mod tests;
50
51use std::{fs, io, path::Path};
52
53#[derive(Debug, thiserror::Error)]
55pub enum RegularFileLockError {
56 #[error("lock target is not a regular file")]
58 NotRegular,
59 #[error("regular file lock I/O failed")]
61 Io(#[source] io::Error),
62 #[error("durable lock-file creation failed")]
64 Publication(#[source] NamedWriteError<io::Error>),
65 #[cfg(not(unix))]
66 #[error("regular file locking is unsupported")]
68 UnsupportedPlatform,
69}
70
71impl From<RegularFileLockError> for io::Error {
76 fn from(error: RegularFileLockError) -> Self {
77 match error {
78 RegularFileLockError::Io(source) => source,
79 other @ RegularFileLockError::NotRegular => Self::other(other),
80 other @ RegularFileLockError::Publication(_) => Self::other(other),
81 #[cfg(not(unix))]
82 other @ RegularFileLockError::UnsupportedPlatform => Self::other(other),
83 }
84 }
85}
86
87pub fn write_bytes(
100 path: &Path,
101 options: WriteOptions,
102 bytes: &[u8],
103) -> Result<(), NamedWriteError<io::Error>> {
104 write_with(path, options, |file| std::io::Write::write_all(file, bytes))
105}
106
107pub fn copy_from<R: io::Read>(
121 source: &mut R,
122 destination: &Path,
123 options: WriteOptions,
124 permissions: Option<fs::Permissions>,
125) -> Result<u64, NamedWriteError<io::Error>> {
126 write_with(destination, options, |sink| {
127 let copied = io::copy(source, sink)?;
128 if let Some(permissions) = permissions {
129 sink.set_permissions(permissions)?;
130 }
131 Ok(copied)
132 })
133}
134
135pub fn prepare_private_parents(parent: &Path) -> io::Result<()> {
148 #[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
149 {
150 supported::prepare_private_parents(parent)
151 }
152 #[cfg(not(any(target_os = "linux", target_os = "android", target_vendor = "apple")))]
153 {
154 let _ = parent;
155 Err(io::Error::new(
156 io::ErrorKind::Unsupported,
157 "durable private parent creation is unsupported",
158 ))
159 }
160}
161
162pub fn open_regular_lock_file_with_parents(path: &Path) -> Result<fs::File, RegularFileLockError> {
182 #[cfg(unix)]
183 {
184 let metadata = match fs::symlink_metadata(path) {
185 Ok(metadata) => metadata,
186 Err(source) if source.kind() == io::ErrorKind::NotFound => {
187 match write_bytes(
188 path,
189 WriteOptions {
190 mode: PublicationMode::CreateNew,
191 permissions: 0o666,
192 },
193 &[],
194 ) {
195 Ok(()) => {}
196 Err(NamedWriteError::BeforePublication {
197 source,
198 cleanup_error: None,
199 }) if source.kind() == io::ErrorKind::AlreadyExists => {}
200 Err(source) => return Err(RegularFileLockError::Publication(source)),
201 }
202 fs::symlink_metadata(path).map_err(RegularFileLockError::Io)?
203 }
204 Err(source) => return Err(RegularFileLockError::Io(source)),
205 };
206 if !metadata.file_type().is_file() {
207 return Err(RegularFileLockError::NotRegular);
208 }
209
210 open_regular_lock_descriptor(path, None)
211 }
212
213 #[cfg(not(unix))]
214 {
215 let _ = path;
216 Err(RegularFileLockError::UnsupportedPlatform)
217 }
218}
219
220pub fn open_private_regular_lock_file(path: &Path) -> Result<fs::File, RegularFileLockError> {
238 #[cfg(unix)]
239 {
240 match fs::symlink_metadata(path) {
241 Ok(metadata) if metadata.file_type().is_file() => {}
242 Ok(_) => return Err(RegularFileLockError::NotRegular),
243 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
244 Err(source) => return Err(RegularFileLockError::Io(source)),
245 }
246 open_regular_lock_descriptor(path, Some(rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR))
247 }
248 #[cfg(not(unix))]
249 {
250 let _ = path;
251 Err(RegularFileLockError::UnsupportedPlatform)
252 }
253}
254
255pub fn open_existing_regular_lock_file(path: &Path) -> Result<fs::File, RegularFileLockError> {
265 #[cfg(unix)]
266 {
267 let metadata = fs::symlink_metadata(path).map_err(RegularFileLockError::Io)?;
268 if !metadata.file_type().is_file() {
269 return Err(RegularFileLockError::NotRegular);
270 }
271 open_regular_lock_descriptor(path, None)
272 }
273 #[cfg(not(unix))]
274 {
275 let _ = path;
276 Err(RegularFileLockError::UnsupportedPlatform)
277 }
278}
279
280#[cfg(unix)]
281fn open_regular_lock_descriptor(
282 path: &Path,
283 create_permissions: Option<rustix::fs::Mode>,
284) -> Result<fs::File, RegularFileLockError> {
285 use rustix::fs::{FileType, Mode, OFlags, fstat, open};
286
287 let mut flags = OFlags::RDWR | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
290 if create_permissions.is_some() {
291 flags |= OFlags::CREATE;
292 }
293 let fd = open(path, flags, create_permissions.unwrap_or(Mode::empty()))
294 .map_err(errno_to_lock_error)?;
295 let metadata = fstat(&fd).map_err(errno_to_lock_error)?;
296 if FileType::from_raw_mode(metadata.st_mode) != FileType::RegularFile {
297 return Err(RegularFileLockError::NotRegular);
298 }
299 Ok(fs::File::from(fd))
300}
301
302#[cfg(unix)]
303fn errno_to_lock_error(source: rustix::io::Errno) -> RegularFileLockError {
304 RegularFileLockError::Io(io::Error::from_raw_os_error(source.raw_os_error()))
305}
306
307#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
308mod supported {
309 use super::{NamedWriteError, PublicationMode, WriteOptions};
310 use crate::identity::FileIdentity;
311
312 use std::{
313 ffi::{OsStr, OsString},
314 fs, io,
315 path::{Path, PathBuf},
316 sync::atomic::{AtomicU64, Ordering},
317 };
318
319 use rustix::{
320 fd::{AsFd, BorrowedFd, OwnedFd},
321 fs::{self as unix_fs, AtFlags, Mode, OFlags, RenameFlags},
322 };
323
324 use std::os::unix::fs::DirBuilderExt;
325
326 const TEMP_ATTEMPTS: usize = 64;
327 static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
328
329 pub(super) enum WriterCustody {
330 Writable,
331 ReadOnly,
332 }
333
334 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
335 pub(super) enum FileCommitStep {
336 ParentDirectoryCreate,
337 CreatedDirectorySync,
338 CreatedDirectoryParentSync,
339 TemporaryFileCreate,
340 TemporaryFileWrite,
341 TemporaryFileSync,
342 Publication,
343 FinalParentSync,
344 }
345
346 #[cfg(test)]
349 pub(super) fn commit_with_hook(
350 path: &Path,
351 bytes: &[u8],
352 options: WriteOptions,
353 before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
354 ) -> Result<(), NamedWriteError<io::Error>> {
355 commit_path_with_options(
356 path,
357 options,
358 |_, file| std::io::Write::write_all(file, bytes),
359 (WriterCustody::Writable, |_, ()| Ok(())),
360 before,
361 )
362 }
363
364 pub(super) fn commit_path_with_options<T, E>(
365 path: &Path,
366 options: WriteOptions,
367 produce: impl FnOnce(&Path, &mut fs::File) -> Result<T, E>,
368 admission: (WriterCustody, impl FnOnce(&Path, &T) -> Result<(), E>),
369 mut before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
370 ) -> Result<T, NamedWriteError<E>> {
371 let options = (
372 options.mode,
373 native_permissions(options.permissions).map_err(NamedWriteError::before)?,
374 );
375 let (parent, file_name) = split_target(path).map_err(NamedWriteError::before)?;
376 create_parent_hierarchy(parent, &mut before, false).map_err(NamedWriteError::before)?;
377 let parent_fd = open_directory(parent).map_err(NamedWriteError::before)?;
378 commit_at_with_hook(
379 parent_fd.as_fd(),
380 Some(parent),
381 file_name,
382 options,
383 produce,
384 admission,
385 before,
386 )
387 }
388
389 pub(super) fn commit_at_with_hook<T, E>(
390 parent_fd: BorrowedFd<'_>,
391 parent_path: Option<&Path>,
392 file_name: &OsStr,
393 (mode, permissions): (PublicationMode, Mode),
394 produce: impl FnOnce(&Path, &mut fs::File) -> Result<T, E>,
395 admission: (WriterCustody, impl FnOnce(&Path, &T) -> Result<(), E>),
396 mut before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
397 ) -> Result<T, NamedWriteError<E>> {
398 validate_file_name(file_name).map_err(NamedWriteError::before)?;
399 let metadata = unix_fs::fstat(parent_fd)
400 .map_err(|error| NamedWriteError::before(errno_to_io(error)))?;
401 if unix_fs::FileType::from_raw_mode(metadata.st_mode) != unix_fs::FileType::Directory {
402 return Err(NamedWriteError::before(io::Error::from(
403 io::ErrorKind::NotADirectory,
404 )));
405 }
406 let parent = parent_path.unwrap_or_else(|| Path::new(""));
409 let path = parent.join(file_name);
410 let mut staging =
411 create_sibling_temp(parent_fd, parent_path, file_name, permissions, &mut before)
412 .map_err(NamedWriteError::before)?;
413
414 let mut produce = Some(produce);
417 let (writer_custody, admit) = admission;
418 let mut admit = Some(admit);
419 let mut value = None;
420 let published = (|| {
421 before(FileCommitStep::TemporaryFileWrite, &staging.path)
422 .map_err(NamedWriteError::before)?;
423 value = Some(
424 produce.take().expect("producer runs once")(&staging.path, &mut staging.file)
425 .map_err(|source| NamedWriteError::Producer {
426 source,
427 cleanup_error: None,
428 })?,
429 );
430 before(FileCommitStep::TemporaryFileSync, &staging.path)
431 .map_err(NamedWriteError::before)?;
432 staging.verify().map_err(NamedWriteError::before)?;
433 staging.file.sync_all().map_err(NamedWriteError::before)?;
434 let admit = admit.take().expect("admission runs once");
435 if matches!(writer_custody, WriterCustody::ReadOnly) {
436 staging.make_read_only().map_err(NamedWriteError::before)?;
437 }
438 admit(&staging.path, value.as_ref().expect("production succeeded")).map_err(
439 |source| NamedWriteError::Producer {
440 source,
441 cleanup_error: None,
442 },
443 )?;
444 before(FileCommitStep::Publication, &path).map_err(NamedWriteError::before)?;
445 staging.verify().map_err(NamedWriteError::before)?;
448 staging.publish(mode, file_name)?;
449 Ok(())
450 })();
451 published.map_err(|mut error| {
452 if let NamedWriteError::Producer { cleanup_error, .. }
455 | NamedWriteError::BeforePublication { cleanup_error, .. } = &mut error
456 {
457 *cleanup_error = staging.cleanup().err();
458 }
459 error
460 })?;
461 let value = value.expect("publication requires successful production");
462
463 before(FileCommitStep::FinalParentSync, parent)
464 .map_err(|source| NamedWriteError::AfterPublication { source })?;
465 unix_fs::fsync(parent_fd).map_err(|error| NamedWriteError::AfterPublication {
466 source: errno_to_io(error),
467 })?;
468 Ok(value)
469 }
470
471 pub(super) fn native_permissions(permissions: u32) -> io::Result<Mode> {
472 validate_permissions(permissions)?;
473 #[cfg(target_vendor = "apple")]
475 let permissions = u16::try_from(permissions).map_err(|_| {
476 io::Error::new(io::ErrorKind::InvalidInput, "invalid file permission bits")
477 })?;
478 Ok(Mode::from_raw_mode(permissions))
479 }
480
481 struct StagingFile<'a> {
484 file: fs::File,
485 name: OsString,
486 path: PathBuf,
487 parent: BorrowedFd<'a>,
488 parent_path: Option<&'a Path>,
489 }
490
491 impl StagingFile<'_> {
492 fn verify(&self) -> io::Result<()> {
493 verify_staging(&self.parent, self.parent_path, &self.name, &self.file)
494 }
495
496 fn make_read_only(&mut self) -> io::Result<()> {
497 let read_only = fs::File::from(
498 unix_fs::openat(
499 self.parent,
500 &self.name,
501 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
502 Mode::empty(),
503 )
504 .map_err(errno_to_io)?,
505 );
506 let held = unix_fs::fstat(&self.file).map_err(errno_to_io)?;
508 let reopened = unix_fs::fstat(&read_only).map_err(errno_to_io)?;
509 if !same_identity(&held, &reopened) {
510 return Err(staging_identity_error());
511 }
512 verify_staging(&self.parent, self.parent_path, &self.name, &read_only)?;
513 drop(std::mem::replace(&mut self.file, read_only));
514 Ok(())
515 }
516
517 fn cleanup(&self) -> io::Result<()> {
518 remove_owned_temp(&self.parent, &self.name, &self.file)
519 }
520
521 fn publish<E>(
522 &self,
523 mode: PublicationMode,
524 name: &OsStr,
525 ) -> Result<(), NamedWriteError<E>> {
526 match mode {
527 PublicationMode::Replace => {
528 unix_fs::renameat(self.parent, &self.name, self.parent, name)
529 .map_err(|error| NamedWriteError::before(errno_to_io(error)))
530 }
531 PublicationMode::CreateNew => {
532 publish_create_new(&self.parent, &self.name, name, &self.file)
533 }
534 }
535 }
536 }
537
538 fn same_identity(left: &unix_fs::Stat, right: &unix_fs::Stat) -> bool {
539 FileIdentity::from_stat(left) == FileIdentity::from_stat(right)
540 && unix_fs::FileType::from_raw_mode(left.st_mode)
541 == unix_fs::FileType::from_raw_mode(right.st_mode)
542 }
543
544 fn staging_identity_error() -> io::Error {
545 io::Error::new(
546 io::ErrorKind::InvalidData,
547 "staging or parent identity changed",
548 )
549 }
550
551 pub(super) fn verify_staging(
552 parent_fd: &impl AsFd,
553 parent: Option<&Path>,
554 name: &OsStr,
555 file: &fs::File,
556 ) -> io::Result<()> {
557 if let Some(parent) = parent {
558 let held_parent = unix_fs::fstat(parent_fd).map_err(errno_to_io)?;
559 let named_parent = unix_fs::statat(unix_fs::CWD, parent, AtFlags::SYMLINK_NOFOLLOW)
560 .map_err(errno_to_io)?;
561 if !same_identity(&held_parent, &named_parent) {
562 return Err(staging_identity_error());
563 }
564 }
565 let held = unix_fs::fstat(file).map_err(errno_to_io)?;
566 let named =
567 unix_fs::statat(parent_fd, name, AtFlags::SYMLINK_NOFOLLOW).map_err(errno_to_io)?;
568 if !same_identity(&held, &named)
569 || unix_fs::FileType::from_raw_mode(held.st_mode) != unix_fs::FileType::RegularFile
570 || held.st_nlink != 1
571 || named.st_nlink != 1
572 {
573 return Err(staging_identity_error());
574 }
575 Ok(())
576 }
577
578 fn remove_owned_temp(parent_fd: &impl AsFd, name: &OsStr, file: &fs::File) -> io::Result<()> {
579 let named = match unix_fs::statat(parent_fd, name, AtFlags::SYMLINK_NOFOLLOW) {
580 Ok(named) => named,
581 Err(rustix::io::Errno::NOENT) => return Ok(()),
582 Err(error) => return Err(errno_to_io(error)),
583 };
584 let held = unix_fs::fstat(file).map_err(errno_to_io)?;
585 if !same_identity(&held, &named) {
586 return Err(staging_identity_error());
587 }
588 unix_fs::unlinkat(parent_fd, name, AtFlags::empty()).map_err(errno_to_io)
589 }
590
591 pub(super) fn publish_create_new<E>(
592 parent_fd: &impl AsFd,
593 temp_name: &OsStr,
594 file_name: &OsStr,
595 temp_file: &fs::File,
596 ) -> Result<(), NamedWriteError<E>> {
597 let renamed = unix_fs::renameat_with(
598 parent_fd,
599 temp_name,
600 parent_fd,
601 file_name,
602 RenameFlags::NOREPLACE,
603 );
604 finish_create_new_publication(parent_fd, temp_name, file_name, renamed, || {
605 remove_owned_temp(parent_fd, temp_name, temp_file)
606 })
607 }
608
609 pub(super) fn finish_create_new_publication<E>(
610 parent_fd: &impl AsFd,
611 temp_name: &OsStr,
612 file_name: &OsStr,
613 renamed: rustix::io::Result<()>,
614 cleanup: impl FnOnce() -> io::Result<()>,
615 ) -> Result<(), NamedWriteError<E>> {
616 match renamed {
617 Ok(()) => Ok(()),
618 Err(
619 rustix::io::Errno::INVAL | rustix::io::Errno::NOSYS | rustix::io::Errno::OPNOTSUPP,
620 ) => {
621 unix_fs::linkat(parent_fd, temp_name, parent_fd, file_name, AtFlags::empty())
622 .map_err(|error| NamedWriteError::before(errno_to_io(error)))?;
623 cleanup().map_err(|source| NamedWriteError::AfterPublication { source })
626 }
627 Err(error) => Err(NamedWriteError::before(errno_to_io(error))),
628 }
629 }
630
631 #[cfg(test)]
632 pub(super) fn publish_create_new_after_error(
633 parent: &Path,
634 temp_name: &OsStr,
635 file_name: &OsStr,
636 error: rustix::io::Errno,
637 ) -> Result<(), NamedWriteError<io::Error>> {
638 let parent_fd = open_directory(parent).map_err(NamedWriteError::before)?;
639 finish_create_new_publication(&parent_fd, temp_name, file_name, Err(error), || {
640 remove_temp(&parent_fd, temp_name)
641 })
642 }
643
644 pub(super) fn validate_target_path(path: &Path) -> io::Result<()> {
645 let bytes = path.as_os_str().as_encoded_bytes();
646 if bytes.contains(&0) {
647 return Err(io::Error::new(
648 io::ErrorKind::InvalidInput,
649 "durable write target contains NUL",
650 ));
651 }
652 if bytes.ends_with(b"/") || bytes.ends_with(b"/.") {
655 return Err(io::Error::new(
656 io::ErrorKind::InvalidInput,
657 "durable write target requires a directory",
658 ));
659 }
660 Ok(())
661 }
662
663 pub(super) fn split_target(path: &Path) -> io::Result<(&Path, &OsStr)> {
664 validate_target_path(path)?;
665 let file_name = path.file_name().ok_or_else(|| {
666 io::Error::new(
667 io::ErrorKind::InvalidInput,
668 format!("durable write target has no file name: {}", path.display()),
669 )
670 })?;
671 let parent = path
672 .parent()
673 .filter(|parent| !parent.as_os_str().is_empty())
674 .unwrap_or_else(|| Path::new("."));
675 Ok((parent, file_name))
676 }
677
678 pub(super) fn validate_permissions(permissions: u32) -> io::Result<()> {
679 if permissions & !0o777 != 0 {
680 return Err(io::Error::new(
681 io::ErrorKind::InvalidInput,
682 "invalid file permission bits",
683 ));
684 }
685 Ok(())
686 }
687
688 pub(super) fn validate_file_name(name: &OsStr) -> io::Result<()> {
689 if name.is_empty()
690 || name == "."
691 || name == ".."
692 || name
693 .as_encoded_bytes()
694 .iter()
695 .any(|byte| matches!(byte, b'/' | 0))
696 {
697 return Err(io::Error::new(
698 io::ErrorKind::InvalidInput,
699 "expected one filename component",
700 ));
701 }
702 Ok(())
703 }
704
705 pub(super) fn prepare_private_parents(parent: &Path) -> io::Result<()> {
706 crate::path::validate_physical_directory_allow_missing(parent)?;
707 create_parent_hierarchy(parent, &mut |_, _| Ok(()), true)
708 }
709
710 fn create_parent_hierarchy(
711 parent: &Path,
712 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
713 private: bool,
714 ) -> io::Result<()> {
715 let mut missing = Vec::new();
716 let mut current = parent;
717 loop {
718 match fs::symlink_metadata(current) {
719 Ok(metadata) if metadata.is_dir() => break,
720 Ok(_) => {
721 return Err(io::Error::new(
722 io::ErrorKind::NotADirectory,
723 format!("output parent is not a directory: {}", current.display()),
724 ));
725 }
726 Err(error) if error.kind() == io::ErrorKind::NotFound => {
727 missing.push(current.to_path_buf());
728 current = current
729 .parent()
730 .filter(|ancestor| !ancestor.as_os_str().is_empty())
731 .unwrap_or_else(|| Path::new("."));
732 }
733 Err(error) => return Err(error),
734 }
735 }
736
737 for directory in missing.into_iter().rev() {
738 before(FileCommitStep::ParentDirectoryCreate, &directory)?;
739 let creation = if private {
740 fs::DirBuilder::new().mode(0o700).create(&directory)
741 } else {
742 fs::create_dir(&directory)
743 };
744 match creation {
745 Ok(()) => {}
746 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
747 if !fs::symlink_metadata(&directory)?.is_dir() {
748 return Err(io::Error::new(
749 io::ErrorKind::NotADirectory,
750 format!("output parent is not a directory: {}", directory.display()),
751 ));
752 }
753 }
754 Err(error) => return Err(error),
755 }
756 sync_created_directory(&directory, before)?;
759 }
760 Ok(())
761 }
762
763 fn sync_created_directory(
764 directory: &Path,
765 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
766 ) -> io::Result<()> {
767 before(FileCommitStep::CreatedDirectorySync, directory)?;
768 let directory_fd = open_directory(directory)?;
769 unix_fs::fsync(&directory_fd).map_err(errno_to_io)?;
770
771 let owner = directory
772 .parent()
773 .filter(|parent| !parent.as_os_str().is_empty())
774 .unwrap_or_else(|| Path::new("."));
775 before(FileCommitStep::CreatedDirectoryParentSync, owner)?;
776 let owner_fd = open_directory(owner)?;
777 unix_fs::fsync(&owner_fd).map_err(errno_to_io)
778 }
779
780 fn create_sibling_temp<'a>(
781 parent_fd: BorrowedFd<'a>,
782 parent_path: Option<&'a Path>,
783 file_name: &OsStr,
784 permissions: Mode,
785 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
786 ) -> io::Result<StagingFile<'a>> {
787 let parent = parent_path.unwrap_or_else(|| Path::new(""));
788 for _ in 0..TEMP_ATTEMPTS {
789 let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
790 let name = format!(".ic-host-tmp-{}-{sequence}", std::process::id());
791 if file_name
794 .as_encoded_bytes()
795 .eq_ignore_ascii_case(name.as_bytes())
796 {
797 continue;
798 }
799 let temp_name = OsString::from(name);
800 let temp_path = parent.join(&temp_name);
801 before(FileCommitStep::TemporaryFileCreate, &temp_path)?;
802 match unix_fs::openat(
803 parent_fd,
804 &temp_name,
805 OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC,
806 permissions,
807 ) {
808 Ok(file) => {
809 return Ok(StagingFile {
810 file: fs::File::from(file),
811 name: temp_name,
812 path: temp_path,
813 parent: parent_fd,
814 parent_path,
815 });
816 }
817 Err(error) if error == rustix::io::Errno::EXIST => {}
818 Err(error) => return Err(errno_to_io(error)),
819 }
820 }
821
822 Err(io::Error::new(
823 io::ErrorKind::AlreadyExists,
824 format!(
825 "could not allocate a unique sibling temporary file for {}",
826 parent.join(file_name).display()
827 ),
828 ))
829 }
830
831 fn open_directory(path: &Path) -> io::Result<OwnedFd> {
832 unix_fs::open(
833 path,
834 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
835 Mode::empty(),
836 )
837 .map_err(errno_to_io)
838 }
839
840 #[cfg(test)]
841 fn remove_temp(parent_fd: &impl AsFd, temp_name: &OsStr) -> io::Result<()> {
842 unix_fs::unlinkat(parent_fd, temp_name, AtFlags::empty()).map_err(errno_to_io)
843 }
844
845 fn errno_to_io(error: rustix::io::Errno) -> io::Error {
846 io::Error::from_raw_os_error(error.raw_os_error())
847 }
848}