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