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