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)]
347 pub(super) fn commit_with_hook(
348 path: &Path,
349 bytes: &[u8],
350 options: WriteOptions,
351 before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
352 ) -> Result<(), NamedWriteError<io::Error>> {
353 commit_path_with_options(
354 path,
355 options,
356 |_, file| std::io::Write::write_all(file, bytes),
357 (WriterCustody::Writable, |_, ()| Ok(())),
358 before,
359 )
360 }
361
362 pub(super) fn commit_path_with_options<T, E>(
363 path: &Path,
364 options: WriteOptions,
365 produce: impl FnOnce(&Path, &mut fs::File) -> Result<T, E>,
366 admission: (WriterCustody, impl FnOnce(&Path, &T) -> Result<(), E>),
367 mut before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
368 ) -> Result<T, NamedWriteError<E>> {
369 let options = (
370 options.mode,
371 native_permissions(options.permissions).map_err(NamedWriteError::before)?,
372 );
373 let (parent, file_name) = split_target(path).map_err(NamedWriteError::before)?;
374 create_parent_hierarchy(parent, &mut before, false).map_err(NamedWriteError::before)?;
375 let parent_fd = open_directory(parent).map_err(NamedWriteError::before)?;
376 commit_at_with_hook(
377 parent_fd.as_fd(),
378 Some(parent),
379 file_name,
380 options,
381 produce,
382 admission,
383 before,
384 )
385 }
386
387 pub(super) fn commit_at_with_hook<T, E>(
388 parent_fd: BorrowedFd<'_>,
389 parent_path: Option<&Path>,
390 file_name: &OsStr,
391 (mode, permissions): (PublicationMode, Mode),
392 produce: impl FnOnce(&Path, &mut fs::File) -> Result<T, E>,
393 admission: (WriterCustody, impl FnOnce(&Path, &T) -> Result<(), E>),
394 mut before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
395 ) -> Result<T, NamedWriteError<E>> {
396 validate_file_name(file_name).map_err(NamedWriteError::before)?;
397 let metadata = unix_fs::fstat(parent_fd)
398 .map_err(|error| NamedWriteError::before(errno_to_io(error)))?;
399 if unix_fs::FileType::from_raw_mode(metadata.st_mode) != unix_fs::FileType::Directory {
400 return Err(NamedWriteError::before(io::Error::from(
401 io::ErrorKind::NotADirectory,
402 )));
403 }
404 let parent = parent_path.unwrap_or_else(|| Path::new(""));
407 let path = parent.join(file_name);
408 let mut staging =
409 create_sibling_temp(parent_fd, parent_path, file_name, permissions, &mut before)
410 .map_err(NamedWriteError::before)?;
411
412 let mut produce = Some(produce);
415 let (writer_custody, admit) = admission;
416 let mut admit = Some(admit);
417 let mut value = None;
418 let published = (|| {
419 before(FileCommitStep::TemporaryFileWrite, &staging.path)
420 .map_err(NamedWriteError::before)?;
421 value = Some(
422 produce.take().expect("producer runs once")(&staging.path, &mut staging.file)
423 .map_err(|source| NamedWriteError::Producer {
424 source,
425 cleanup_error: None,
426 })?,
427 );
428 before(FileCommitStep::TemporaryFileSync, &staging.path)
429 .map_err(NamedWriteError::before)?;
430 staging.verify().map_err(NamedWriteError::before)?;
431 staging.file.sync_all().map_err(NamedWriteError::before)?;
432 let admit = admit.take().expect("admission runs once");
433 if matches!(writer_custody, WriterCustody::ReadOnly) {
434 staging.make_read_only().map_err(NamedWriteError::before)?;
435 }
436 admit(&staging.path, value.as_ref().expect("production succeeded")).map_err(
437 |source| NamedWriteError::Producer {
438 source,
439 cleanup_error: None,
440 },
441 )?;
442 before(FileCommitStep::Publication, &path).map_err(NamedWriteError::before)?;
443 staging.verify().map_err(NamedWriteError::before)?;
446 staging.publish(mode, file_name)?;
447 Ok(())
448 })();
449 published.map_err(|mut error| {
450 if let NamedWriteError::Producer { cleanup_error, .. }
453 | NamedWriteError::BeforePublication { cleanup_error, .. } = &mut error
454 {
455 *cleanup_error = staging.cleanup().err();
456 }
457 error
458 })?;
459 let value = value.expect("publication requires successful production");
460
461 before(FileCommitStep::FinalParentSync, parent)
462 .map_err(|source| NamedWriteError::AfterPublication { source })?;
463 unix_fs::fsync(parent_fd).map_err(|error| NamedWriteError::AfterPublication {
464 source: errno_to_io(error),
465 })?;
466 Ok(value)
467 }
468
469 pub(super) fn native_permissions(permissions: u32) -> io::Result<Mode> {
470 validate_permissions(permissions)?;
471 #[cfg(target_vendor = "apple")]
473 let permissions = u16::try_from(permissions).map_err(|_| {
474 io::Error::new(io::ErrorKind::InvalidInput, "invalid file permission bits")
475 })?;
476 Ok(Mode::from_raw_mode(permissions))
477 }
478
479 struct StagingFile<'a> {
482 file: fs::File,
483 name: OsString,
484 path: PathBuf,
485 parent: BorrowedFd<'a>,
486 parent_path: Option<&'a Path>,
487 }
488
489 impl StagingFile<'_> {
490 fn verify(&self) -> io::Result<()> {
491 verify_staging(&self.parent, self.parent_path, &self.name, &self.file)
492 }
493
494 fn make_read_only(&mut self) -> io::Result<()> {
495 let read_only = fs::File::from(
496 unix_fs::openat(
497 self.parent,
498 &self.name,
499 OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
500 Mode::empty(),
501 )
502 .map_err(errno_to_io)?,
503 );
504 let held = unix_fs::fstat(&self.file).map_err(errno_to_io)?;
506 let reopened = unix_fs::fstat(&read_only).map_err(errno_to_io)?;
507 if !same_identity(&held, &reopened) {
508 return Err(staging_identity_error());
509 }
510 verify_staging(&self.parent, self.parent_path, &self.name, &read_only)?;
511 drop(std::mem::replace(&mut self.file, read_only));
512 Ok(())
513 }
514
515 fn cleanup(&self) -> io::Result<()> {
516 remove_owned_temp(&self.parent, &self.name, &self.file)
517 }
518
519 fn publish<E>(
520 &self,
521 mode: PublicationMode,
522 name: &OsStr,
523 ) -> Result<(), NamedWriteError<E>> {
524 match mode {
525 PublicationMode::Replace => {
526 unix_fs::renameat(self.parent, &self.name, self.parent, name)
527 .map_err(|error| NamedWriteError::before(errno_to_io(error)))
528 }
529 PublicationMode::CreateNew => {
530 publish_create_new(&self.parent, &self.name, name, &self.file)
531 }
532 }
533 }
534 }
535
536 fn same_identity(left: &unix_fs::Stat, right: &unix_fs::Stat) -> bool {
537 FileIdentity::from_stat(left) == FileIdentity::from_stat(right)
538 && unix_fs::FileType::from_raw_mode(left.st_mode)
539 == unix_fs::FileType::from_raw_mode(right.st_mode)
540 }
541
542 fn staging_identity_error() -> io::Error {
543 io::Error::new(
544 io::ErrorKind::InvalidData,
545 "staging or parent identity changed",
546 )
547 }
548
549 pub(super) fn verify_staging(
550 parent_fd: &impl AsFd,
551 parent: Option<&Path>,
552 name: &OsStr,
553 file: &fs::File,
554 ) -> io::Result<()> {
555 if let Some(parent) = parent {
556 let held_parent = unix_fs::fstat(parent_fd).map_err(errno_to_io)?;
557 let named_parent = unix_fs::statat(unix_fs::CWD, parent, AtFlags::SYMLINK_NOFOLLOW)
558 .map_err(errno_to_io)?;
559 if !same_identity(&held_parent, &named_parent) {
560 return Err(staging_identity_error());
561 }
562 }
563 let held = unix_fs::fstat(file).map_err(errno_to_io)?;
564 let named =
565 unix_fs::statat(parent_fd, name, AtFlags::SYMLINK_NOFOLLOW).map_err(errno_to_io)?;
566 if !same_identity(&held, &named)
567 || unix_fs::FileType::from_raw_mode(held.st_mode) != unix_fs::FileType::RegularFile
568 || held.st_nlink != 1
569 || named.st_nlink != 1
570 {
571 return Err(staging_identity_error());
572 }
573 Ok(())
574 }
575
576 fn remove_owned_temp(parent_fd: &impl AsFd, name: &OsStr, file: &fs::File) -> io::Result<()> {
577 let named = match unix_fs::statat(parent_fd, name, AtFlags::SYMLINK_NOFOLLOW) {
578 Ok(named) => named,
579 Err(rustix::io::Errno::NOENT) => return Ok(()),
580 Err(error) => return Err(errno_to_io(error)),
581 };
582 let held = unix_fs::fstat(file).map_err(errno_to_io)?;
583 if !same_identity(&held, &named) {
584 return Err(staging_identity_error());
585 }
586 unix_fs::unlinkat(parent_fd, name, AtFlags::empty()).map_err(errno_to_io)
587 }
588
589 pub(super) fn publish_create_new<E>(
590 parent_fd: &impl AsFd,
591 temp_name: &OsStr,
592 file_name: &OsStr,
593 temp_file: &fs::File,
594 ) -> Result<(), NamedWriteError<E>> {
595 let renamed = unix_fs::renameat_with(
596 parent_fd,
597 temp_name,
598 parent_fd,
599 file_name,
600 RenameFlags::NOREPLACE,
601 );
602 finish_create_new_publication(parent_fd, temp_name, file_name, renamed, || {
603 remove_owned_temp(parent_fd, temp_name, temp_file)
604 })
605 }
606
607 pub(super) fn finish_create_new_publication<E>(
608 parent_fd: &impl AsFd,
609 temp_name: &OsStr,
610 file_name: &OsStr,
611 renamed: rustix::io::Result<()>,
612 cleanup: impl FnOnce() -> io::Result<()>,
613 ) -> Result<(), NamedWriteError<E>> {
614 match renamed {
615 Ok(()) => Ok(()),
616 Err(
617 rustix::io::Errno::INVAL | rustix::io::Errno::NOSYS | rustix::io::Errno::OPNOTSUPP,
618 ) => {
619 unix_fs::linkat(parent_fd, temp_name, parent_fd, file_name, AtFlags::empty())
620 .map_err(|error| NamedWriteError::before(errno_to_io(error)))?;
621 cleanup().map_err(|source| NamedWriteError::AfterPublication { source })
624 }
625 Err(error) => Err(NamedWriteError::before(errno_to_io(error))),
626 }
627 }
628
629 #[cfg(test)]
630 pub(super) fn publish_create_new_after_error(
631 parent: &Path,
632 temp_name: &OsStr,
633 file_name: &OsStr,
634 error: rustix::io::Errno,
635 ) -> Result<(), NamedWriteError<io::Error>> {
636 let parent_fd = open_directory(parent).map_err(NamedWriteError::before)?;
637 finish_create_new_publication(&parent_fd, temp_name, file_name, Err(error), || {
638 remove_temp(&parent_fd, temp_name)
639 })
640 }
641
642 pub(super) fn validate_target_path(path: &Path) -> io::Result<()> {
643 let bytes = path.as_os_str().as_encoded_bytes();
644 if bytes.contains(&0) {
645 return Err(io::Error::new(
646 io::ErrorKind::InvalidInput,
647 "durable write target contains NUL",
648 ));
649 }
650 if bytes.ends_with(b"/") || bytes.ends_with(b"/.") {
653 return Err(io::Error::new(
654 io::ErrorKind::InvalidInput,
655 "durable write target requires a directory",
656 ));
657 }
658 Ok(())
659 }
660
661 pub(super) fn split_target(path: &Path) -> io::Result<(&Path, &OsStr)> {
662 validate_target_path(path)?;
663 let file_name = path.file_name().ok_or_else(|| {
664 io::Error::new(
665 io::ErrorKind::InvalidInput,
666 format!("durable write target has no file name: {}", path.display()),
667 )
668 })?;
669 let parent = path
670 .parent()
671 .filter(|parent| !parent.as_os_str().is_empty())
672 .unwrap_or_else(|| Path::new("."));
673 Ok((parent, file_name))
674 }
675
676 pub(super) fn validate_permissions(permissions: u32) -> io::Result<()> {
677 if permissions & !0o777 != 0 {
678 return Err(io::Error::new(
679 io::ErrorKind::InvalidInput,
680 "invalid file permission bits",
681 ));
682 }
683 Ok(())
684 }
685
686 pub(super) fn validate_file_name(name: &OsStr) -> io::Result<()> {
687 if name.is_empty()
688 || name == "."
689 || name == ".."
690 || name
691 .as_encoded_bytes()
692 .iter()
693 .any(|byte| matches!(byte, b'/' | 0))
694 {
695 return Err(io::Error::new(
696 io::ErrorKind::InvalidInput,
697 "expected one filename component",
698 ));
699 }
700 Ok(())
701 }
702
703 pub(super) fn prepare_private_parents(parent: &Path) -> io::Result<()> {
704 crate::path::validate_physical_directory_allow_missing(parent)?;
705 create_parent_hierarchy(parent, &mut |_, _| Ok(()), true)
706 }
707
708 fn create_parent_hierarchy(
709 parent: &Path,
710 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
711 private: bool,
712 ) -> io::Result<()> {
713 let mut missing = Vec::new();
714 let mut current = parent;
715 loop {
716 match fs::symlink_metadata(current) {
717 Ok(metadata) if metadata.is_dir() => break,
718 Ok(_) => {
719 return Err(io::Error::new(
720 io::ErrorKind::NotADirectory,
721 format!("output parent is not a directory: {}", current.display()),
722 ));
723 }
724 Err(error) if error.kind() == io::ErrorKind::NotFound => {
725 missing.push(current.to_path_buf());
726 current = current
727 .parent()
728 .filter(|ancestor| !ancestor.as_os_str().is_empty())
729 .unwrap_or_else(|| Path::new("."));
730 }
731 Err(error) => return Err(error),
732 }
733 }
734
735 for directory in missing.into_iter().rev() {
736 before(FileCommitStep::ParentDirectoryCreate, &directory)?;
737 let creation = if private {
738 fs::DirBuilder::new().mode(0o700).create(&directory)
739 } else {
740 fs::create_dir(&directory)
741 };
742 match creation {
743 Ok(()) => {}
744 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
745 if !fs::symlink_metadata(&directory)?.is_dir() {
746 return Err(io::Error::new(
747 io::ErrorKind::NotADirectory,
748 format!("output parent is not a directory: {}", directory.display()),
749 ));
750 }
751 }
752 Err(error) => return Err(error),
753 }
754 sync_created_directory(&directory, before)?;
757 }
758 Ok(())
759 }
760
761 fn sync_created_directory(
762 directory: &Path,
763 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
764 ) -> io::Result<()> {
765 before(FileCommitStep::CreatedDirectorySync, directory)?;
766 let directory_fd = open_directory(directory)?;
767 unix_fs::fsync(&directory_fd).map_err(errno_to_io)?;
768
769 let owner = directory
770 .parent()
771 .filter(|parent| !parent.as_os_str().is_empty())
772 .unwrap_or_else(|| Path::new("."));
773 before(FileCommitStep::CreatedDirectoryParentSync, owner)?;
774 let owner_fd = open_directory(owner)?;
775 unix_fs::fsync(&owner_fd).map_err(errno_to_io)
776 }
777
778 fn create_sibling_temp<'a>(
779 parent_fd: BorrowedFd<'a>,
780 parent_path: Option<&'a Path>,
781 file_name: &OsStr,
782 permissions: Mode,
783 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
784 ) -> io::Result<StagingFile<'a>> {
785 let parent = parent_path.unwrap_or_else(|| Path::new(""));
786 for _ in 0..TEMP_ATTEMPTS {
787 let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
788 let name = format!(".ic-host-tmp-{}-{sequence}", std::process::id());
789 if file_name
792 .as_encoded_bytes()
793 .eq_ignore_ascii_case(name.as_bytes())
794 {
795 continue;
796 }
797 let temp_name = OsString::from(name);
798 let temp_path = parent.join(&temp_name);
799 before(FileCommitStep::TemporaryFileCreate, &temp_path)?;
800 match unix_fs::openat(
801 parent_fd,
802 &temp_name,
803 OFlags::WRONLY | OFlags::CREATE | OFlags::EXCL | OFlags::CLOEXEC,
804 permissions,
805 ) {
806 Ok(file) => {
807 return Ok(StagingFile {
808 file: fs::File::from(file),
809 name: temp_name,
810 path: temp_path,
811 parent: parent_fd,
812 parent_path,
813 });
814 }
815 Err(error) if error == rustix::io::Errno::EXIST => {}
816 Err(error) => return Err(errno_to_io(error)),
817 }
818 }
819
820 Err(io::Error::new(
821 io::ErrorKind::AlreadyExists,
822 format!(
823 "could not allocate a unique sibling temporary file for {}",
824 parent.join(file_name).display()
825 ),
826 ))
827 }
828
829 fn open_directory(path: &Path) -> io::Result<OwnedFd> {
830 unix_fs::open(
831 path,
832 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
833 Mode::empty(),
834 )
835 .map_err(errno_to_io)
836 }
837
838 #[cfg(test)]
839 fn remove_temp(parent_fd: &impl AsFd, temp_name: &OsStr) -> io::Result<()> {
840 unix_fs::unlinkat(parent_fd, temp_name, AtFlags::empty()).map_err(errno_to_io)
841 }
842
843 fn errno_to_io(error: rustix::io::Errno) -> io::Error {
844 io::Error::from_raw_os_error(error.raw_os_error())
845 }
846}