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