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