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(windows)]
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(windows)]
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 let metadata = match fs::symlink_metadata(path) {
181 Ok(metadata) => metadata,
182 Err(source) if source.kind() == io::ErrorKind::NotFound => {
183 match write_bytes(
184 path,
185 WriteOptions {
186 mode: PublicationMode::CreateNew,
187 permissions: 0o666,
188 },
189 &[],
190 ) {
191 Ok(()) => {}
192 Err(NamedWriteError::BeforePublication {
193 source,
194 cleanup_error: None,
195 }) if source.kind() == io::ErrorKind::AlreadyExists => {}
196 Err(source) => return Err(RegularFileLockError::Publication(source)),
197 }
198 fs::symlink_metadata(path).map_err(RegularFileLockError::Io)?
199 }
200 Err(source) => return Err(RegularFileLockError::Io(source)),
201 };
202 if !metadata.file_type().is_file() {
203 return Err(RegularFileLockError::NotRegular);
204 }
205
206 #[cfg(not(windows))]
207 {
208 open_regular_lock_descriptor(path, None)
209 }
210
211 #[cfg(windows)]
212 {
213 Err(RegularFileLockError::UnsupportedPlatform)
214 }
215}
216
217pub fn open_private_regular_lock_file(path: &Path) -> Result<fs::File, RegularFileLockError> {
235 #[cfg(not(windows))]
236 {
237 match fs::symlink_metadata(path) {
238 Ok(metadata) if metadata.file_type().is_file() => {}
239 Ok(_) => return Err(RegularFileLockError::NotRegular),
240 Err(source) if source.kind() == io::ErrorKind::NotFound => {}
241 Err(source) => return Err(RegularFileLockError::Io(source)),
242 }
243 open_regular_lock_descriptor(path, Some(rustix::fs::Mode::RUSR | rustix::fs::Mode::WUSR))
244 }
245 #[cfg(windows)]
246 {
247 let _ = path;
248 Err(RegularFileLockError::UnsupportedPlatform)
249 }
250}
251
252pub fn open_existing_regular_lock_file(path: &Path) -> Result<fs::File, RegularFileLockError> {
262 #[cfg(not(windows))]
263 {
264 let metadata = fs::symlink_metadata(path).map_err(RegularFileLockError::Io)?;
265 if !metadata.file_type().is_file() {
266 return Err(RegularFileLockError::NotRegular);
267 }
268 open_regular_lock_descriptor(path, None)
269 }
270 #[cfg(windows)]
271 {
272 let _ = path;
273 Err(RegularFileLockError::UnsupportedPlatform)
274 }
275}
276
277#[cfg(not(windows))]
278fn open_regular_lock_descriptor(
279 path: &Path,
280 create_permissions: Option<rustix::fs::Mode>,
281) -> Result<fs::File, RegularFileLockError> {
282 use rustix::fs::{FileType, Mode, OFlags, fstat, open};
283
284 let mut flags = OFlags::RDWR | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC;
287 if create_permissions.is_some() {
288 flags |= OFlags::CREATE;
289 }
290 let fd = open(path, flags, create_permissions.unwrap_or(Mode::empty()))
291 .map_err(errno_to_lock_error)?;
292 let metadata = fstat(&fd).map_err(errno_to_lock_error)?;
293 if FileType::from_raw_mode(metadata.st_mode) != FileType::RegularFile {
294 return Err(RegularFileLockError::NotRegular);
295 }
296 Ok(fs::File::from(fd))
297}
298
299#[cfg(not(windows))]
300fn errno_to_lock_error(source: rustix::io::Errno) -> RegularFileLockError {
301 RegularFileLockError::Io(io::Error::from_raw_os_error(source.raw_os_error()))
302}
303
304#[cfg(any(target_os = "linux", target_os = "android", target_vendor = "apple"))]
305mod supported {
306 use super::{NamedWriteError, PublicationMode, WriteOptions};
307 use crate::identity::FileIdentity;
308
309 use std::{
310 ffi::{OsStr, OsString},
311 fs, io,
312 path::{Component, Path, PathBuf},
313 sync::atomic::{AtomicU64, Ordering},
314 };
315
316 use rustix::{
317 fd::{AsFd, BorrowedFd, OwnedFd},
318 fs::{self as unix_fs, AtFlags, Mode, OFlags, RenameFlags},
319 };
320
321 use std::os::unix::fs::DirBuilderExt;
322
323 const TEMP_ATTEMPTS: usize = 64;
324 static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
325
326 pub(super) enum WriterCustody {
327 Writable,
328 ReadOnly,
329 }
330
331 #[derive(Clone, Copy, Debug, Eq, PartialEq)]
332 pub(super) enum FileCommitStep {
333 ParentDirectoryCreate,
334 CreatedDirectorySync,
335 CreatedDirectoryParentSync,
336 TemporaryFileCreate,
337 TemporaryFileWrite,
338 TemporaryFileSync,
339 Publication,
340 FinalParentSync,
341 }
342
343 #[cfg(test)]
344 pub(super) fn commit_with_hook(
345 path: &Path,
346 bytes: &[u8],
347 options: WriteOptions,
348 before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
349 ) -> Result<(), NamedWriteError<io::Error>> {
350 commit_path_with_options(
351 path,
352 options,
353 |_, file| std::io::Write::write_all(file, bytes),
354 (WriterCustody::Writable, |_, ()| Ok(())),
355 before,
356 )
357 }
358
359 pub(super) fn commit_path_with_options<T, E>(
360 path: &Path,
361 options: WriteOptions,
362 produce: impl FnOnce(&Path, &mut fs::File) -> Result<T, E>,
363 admission: (WriterCustody, impl FnOnce(&Path, &T) -> Result<(), E>),
364 mut before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
365 ) -> Result<T, NamedWriteError<E>> {
366 let options = (
367 options.mode,
368 native_permissions(options.permissions).map_err(NamedWriteError::before)?,
369 );
370 let (parent, file_name) = split_target(path).map_err(NamedWriteError::before)?;
371 create_parent_hierarchy(parent, &mut before, false).map_err(NamedWriteError::before)?;
372 let parent_fd = open_directory(parent).map_err(NamedWriteError::before)?;
373 commit_at_with_hook(
374 parent_fd.as_fd(),
375 Some(parent),
376 file_name,
377 options,
378 produce,
379 admission,
380 before,
381 )
382 }
383
384 pub(super) fn commit_at_with_hook<T, E>(
385 parent_fd: BorrowedFd<'_>,
386 parent_path: Option<&Path>,
387 file_name: &OsStr,
388 (mode, permissions): (PublicationMode, Mode),
389 produce: impl FnOnce(&Path, &mut fs::File) -> Result<T, E>,
390 admission: (WriterCustody, impl FnOnce(&Path, &T) -> Result<(), E>),
391 mut before: impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
392 ) -> Result<T, NamedWriteError<E>> {
393 validate_file_name(file_name).map_err(NamedWriteError::before)?;
394 let metadata = unix_fs::fstat(parent_fd)
395 .map_err(|error| NamedWriteError::before(errno_to_io(error)))?;
396 if unix_fs::FileType::from_raw_mode(metadata.st_mode) != unix_fs::FileType::Directory {
397 return Err(NamedWriteError::before(io::Error::from(
398 io::ErrorKind::NotADirectory,
399 )));
400 }
401 let parent = parent_path.unwrap_or_else(|| Path::new(""));
404 let path = parent.join(file_name);
405 let (temp_name, temp_path, mut temp_file) =
406 create_sibling_temp(&parent_fd, parent, file_name, permissions, &mut before)
407 .map_err(NamedWriteError::before)?;
408
409 let mut produce = Some(produce);
412 let (writer_custody, admit) = admission;
413 let mut admit = Some(admit);
414 let mut value = None;
415 let published = (|| {
416 before(FileCommitStep::TemporaryFileWrite, &temp_path)
417 .map_err(NamedWriteError::before)?;
418 value = Some(
419 produce.take().expect("producer runs once")(&temp_path, &mut temp_file).map_err(
420 |source| NamedWriteError::Producer {
421 source,
422 cleanup_error: None,
423 },
424 )?,
425 );
426 before(FileCommitStep::TemporaryFileSync, &temp_path)
427 .map_err(NamedWriteError::before)?;
428 verify_staging(&parent_fd, parent_path, &temp_name, &temp_file)
429 .map_err(NamedWriteError::before)?;
430 temp_file.sync_all().map_err(NamedWriteError::before)?;
431 let admit = admit.take().expect("admission runs once");
432 if matches!(writer_custody, WriterCustody::ReadOnly) {
433 let read_only = retain_read_only(&parent_fd, parent_path, &temp_name, &temp_file)
434 .map_err(NamedWriteError::before)?;
435 drop(std::mem::replace(&mut temp_file, read_only));
436 }
437 admit(&temp_path, value.as_ref().expect("production succeeded")).map_err(|source| {
438 NamedWriteError::Producer {
439 source,
440 cleanup_error: None,
441 }
442 })?;
443 before(FileCommitStep::Publication, &path).map_err(NamedWriteError::before)?;
444 verify_staging(&parent_fd, parent_path, &temp_name, &temp_file)
447 .map_err(NamedWriteError::before)?;
448 match mode {
449 PublicationMode::Replace => {
450 unix_fs::renameat(parent_fd, &temp_name, parent_fd, file_name)
451 .map_err(|error| NamedWriteError::before(errno_to_io(error)))
452 }
453 PublicationMode::CreateNew => {
454 publish_create_new(&parent_fd, &temp_name, file_name, &temp_file)
455 }
456 }?;
457 Ok(())
458 })();
459 published.map_err(|mut error| {
460 if let NamedWriteError::Producer { cleanup_error, .. }
463 | NamedWriteError::BeforePublication { cleanup_error, .. } = &mut error
464 {
465 *cleanup_error = remove_owned_temp(&parent_fd, &temp_name, &temp_file).err();
466 }
467 error
468 })?;
469 let value = value.expect("publication requires successful production");
470
471 before(FileCommitStep::FinalParentSync, parent)
472 .map_err(|source| NamedWriteError::AfterPublication { source })?;
473 unix_fs::fsync(parent_fd).map_err(|error| NamedWriteError::AfterPublication {
474 source: errno_to_io(error),
475 })?;
476 Ok(value)
477 }
478
479 pub(super) fn native_permissions(permissions: u32) -> io::Result<Mode> {
480 validate_permissions(permissions)?;
481 #[cfg(target_vendor = "apple")]
483 let permissions = u16::try_from(permissions).map_err(|_| {
484 io::Error::new(io::ErrorKind::InvalidInput, "invalid file permission bits")
485 })?;
486 Ok(Mode::from_raw_mode(permissions))
487 }
488
489 fn retain_read_only(
490 parent_fd: &impl AsFd,
491 parent_path: Option<&Path>,
492 name: &OsStr,
493 original: &fs::File,
494 ) -> io::Result<fs::File> {
495 let read_only = fs::File::from(
496 unix_fs::openat(
497 parent_fd,
498 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(original).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(parent_fd, parent_path, name, &read_only)?;
511 Ok(read_only)
512 }
513
514 fn same_identity(left: &unix_fs::Stat, right: &unix_fs::Stat) -> bool {
515 FileIdentity::from_stat(left) == FileIdentity::from_stat(right)
516 && unix_fs::FileType::from_raw_mode(left.st_mode)
517 == unix_fs::FileType::from_raw_mode(right.st_mode)
518 }
519
520 fn staging_identity_error() -> io::Error {
521 io::Error::new(
522 io::ErrorKind::InvalidData,
523 "staging or parent identity changed",
524 )
525 }
526
527 pub(super) fn verify_staging(
528 parent_fd: &impl AsFd,
529 parent: Option<&Path>,
530 name: &OsStr,
531 file: &fs::File,
532 ) -> io::Result<()> {
533 if let Some(parent) = parent {
534 let held_parent = unix_fs::fstat(parent_fd).map_err(errno_to_io)?;
535 let named_parent = unix_fs::statat(unix_fs::CWD, parent, AtFlags::SYMLINK_NOFOLLOW)
536 .map_err(errno_to_io)?;
537 if !same_identity(&held_parent, &named_parent) {
538 return Err(staging_identity_error());
539 }
540 }
541 let held = unix_fs::fstat(file).map_err(errno_to_io)?;
542 let named =
543 unix_fs::statat(parent_fd, name, AtFlags::SYMLINK_NOFOLLOW).map_err(errno_to_io)?;
544 if !same_identity(&held, &named)
545 || unix_fs::FileType::from_raw_mode(held.st_mode) != unix_fs::FileType::RegularFile
546 || held.st_nlink != 1
547 || named.st_nlink != 1
548 {
549 return Err(staging_identity_error());
550 }
551 Ok(())
552 }
553
554 fn remove_owned_temp(parent_fd: &impl AsFd, name: &OsStr, file: &fs::File) -> io::Result<()> {
555 let named = match unix_fs::statat(parent_fd, name, AtFlags::SYMLINK_NOFOLLOW) {
556 Ok(named) => named,
557 Err(rustix::io::Errno::NOENT) => return Ok(()),
558 Err(error) => return Err(errno_to_io(error)),
559 };
560 let held = unix_fs::fstat(file).map_err(errno_to_io)?;
561 if !same_identity(&held, &named) {
562 return Err(staging_identity_error());
563 }
564 unix_fs::unlinkat(parent_fd, name, AtFlags::empty()).map_err(errno_to_io)
565 }
566
567 pub(super) fn publish_create_new<E>(
568 parent_fd: &impl AsFd,
569 temp_name: &OsStr,
570 file_name: &OsStr,
571 temp_file: &fs::File,
572 ) -> Result<(), NamedWriteError<E>> {
573 let renamed = unix_fs::renameat_with(
574 parent_fd,
575 temp_name,
576 parent_fd,
577 file_name,
578 RenameFlags::NOREPLACE,
579 );
580 finish_create_new_publication(parent_fd, temp_name, file_name, renamed, || {
581 remove_owned_temp(parent_fd, temp_name, temp_file)
582 })
583 }
584
585 pub(super) fn finish_create_new_publication<E>(
586 parent_fd: &impl AsFd,
587 temp_name: &OsStr,
588 file_name: &OsStr,
589 renamed: rustix::io::Result<()>,
590 cleanup: impl FnOnce() -> io::Result<()>,
591 ) -> Result<(), NamedWriteError<E>> {
592 match renamed {
593 Ok(()) => Ok(()),
594 Err(
595 rustix::io::Errno::INVAL | rustix::io::Errno::NOSYS | rustix::io::Errno::OPNOTSUPP,
596 ) => {
597 unix_fs::linkat(parent_fd, temp_name, parent_fd, file_name, AtFlags::empty())
598 .map_err(|error| NamedWriteError::before(errno_to_io(error)))?;
599 cleanup().map_err(|source| NamedWriteError::AfterPublication { source })
602 }
603 Err(error) => Err(NamedWriteError::before(errno_to_io(error))),
604 }
605 }
606
607 #[cfg(test)]
608 pub(super) fn publish_create_new_after_error(
609 parent: &Path,
610 temp_name: &OsStr,
611 file_name: &OsStr,
612 error: rustix::io::Errno,
613 ) -> Result<(), NamedWriteError<io::Error>> {
614 let parent_fd = open_directory(parent).map_err(NamedWriteError::before)?;
615 finish_create_new_publication(&parent_fd, temp_name, file_name, Err(error), || {
616 remove_temp(&parent_fd, temp_name)
617 })
618 }
619
620 pub(super) fn validate_target_path(path: &Path) -> io::Result<()> {
621 let bytes = path.as_os_str().as_encoded_bytes();
622 if bytes.contains(&0) {
623 return Err(io::Error::new(
624 io::ErrorKind::InvalidInput,
625 "durable write target contains NUL",
626 ));
627 }
628 if bytes.ends_with(b"/") || bytes.ends_with(b"/.") {
631 return Err(io::Error::new(
632 io::ErrorKind::InvalidInput,
633 "durable write target requires a directory",
634 ));
635 }
636 Ok(())
637 }
638
639 pub(super) fn split_target(path: &Path) -> io::Result<(&Path, &OsStr)> {
640 validate_target_path(path)?;
641 let file_name = path.file_name().ok_or_else(|| {
642 io::Error::new(
643 io::ErrorKind::InvalidInput,
644 format!("durable write target has no file name: {}", path.display()),
645 )
646 })?;
647 let parent = path
648 .parent()
649 .filter(|parent| !parent.as_os_str().is_empty())
650 .unwrap_or_else(|| Path::new("."));
651 Ok((parent, file_name))
652 }
653
654 pub(super) fn validate_permissions(permissions: u32) -> io::Result<()> {
655 if permissions & !0o777 != 0 {
656 return Err(io::Error::new(
657 io::ErrorKind::InvalidInput,
658 "invalid file permission bits",
659 ));
660 }
661 Ok(())
662 }
663
664 pub(super) fn validate_file_name(name: &OsStr) -> io::Result<()> {
665 if name.is_empty()
666 || name == "."
667 || name == ".."
668 || name
669 .as_encoded_bytes()
670 .iter()
671 .any(|byte| matches!(byte, b'/' | 0))
672 {
673 return Err(io::Error::new(
674 io::ErrorKind::InvalidInput,
675 "expected one filename component",
676 ));
677 }
678 Ok(())
679 }
680
681 pub(super) fn prepare_private_parents(parent: &Path) -> io::Result<()> {
682 let mut component_path = PathBuf::new();
683 for component in parent.components() {
684 if component == Component::ParentDir {
685 return Err(io::Error::new(
686 io::ErrorKind::InvalidInput,
687 "private parent path may not contain '..'",
688 ));
689 }
690 component_path.push(component.as_os_str());
691 match fs::symlink_metadata(&component_path) {
692 Ok(metadata) if metadata.is_dir() => {}
693 Ok(_) => {
694 return Err(io::Error::new(
695 io::ErrorKind::NotADirectory,
696 format!(
697 "output parent is not a directory: {}",
698 component_path.display()
699 ),
700 ));
701 }
702 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
703 Err(error) => return Err(error),
704 }
705 }
706 create_parent_hierarchy(parent, &mut |_, _| Ok(()), true)
707 }
708
709 fn create_parent_hierarchy(
710 parent: &Path,
711 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
712 private: bool,
713 ) -> io::Result<()> {
714 let mut missing = Vec::new();
715 let mut current = parent;
716 loop {
717 match fs::symlink_metadata(current) {
718 Ok(metadata) if metadata.is_dir() => break,
719 Ok(_) => {
720 return Err(io::Error::new(
721 io::ErrorKind::NotADirectory,
722 format!("output parent is not a directory: {}", current.display()),
723 ));
724 }
725 Err(error) if error.kind() == io::ErrorKind::NotFound => {
726 missing.push(current.to_path_buf());
727 current = current
728 .parent()
729 .filter(|ancestor| !ancestor.as_os_str().is_empty())
730 .unwrap_or_else(|| Path::new("."));
731 }
732 Err(error) => return Err(error),
733 }
734 }
735
736 for directory in missing.into_iter().rev() {
737 before(FileCommitStep::ParentDirectoryCreate, &directory)?;
738 let creation = if private {
739 fs::DirBuilder::new().mode(0o700).create(&directory)
740 } else {
741 fs::create_dir(&directory)
742 };
743 match creation {
744 Ok(()) => {}
745 Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
746 if !fs::symlink_metadata(&directory)?.is_dir() {
747 return Err(io::Error::new(
748 io::ErrorKind::NotADirectory,
749 format!("output parent is not a directory: {}", directory.display()),
750 ));
751 }
752 }
753 Err(error) => return Err(error),
754 }
755 sync_created_directory(&directory, before)?;
758 }
759 Ok(())
760 }
761
762 fn sync_created_directory(
763 directory: &Path,
764 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
765 ) -> io::Result<()> {
766 before(FileCommitStep::CreatedDirectorySync, directory)?;
767 let directory_fd = open_directory(directory)?;
768 unix_fs::fsync(&directory_fd).map_err(errno_to_io)?;
769
770 let owner = directory
771 .parent()
772 .filter(|parent| !parent.as_os_str().is_empty())
773 .unwrap_or_else(|| Path::new("."));
774 before(FileCommitStep::CreatedDirectoryParentSync, owner)?;
775 let owner_fd = open_directory(owner)?;
776 unix_fs::fsync(&owner_fd).map_err(errno_to_io)
777 }
778
779 fn create_sibling_temp(
780 parent_fd: &impl AsFd,
781 parent: &Path,
782 file_name: &OsStr,
783 permissions: Mode,
784 before: &mut impl FnMut(FileCommitStep, &Path) -> io::Result<()>,
785 ) -> io::Result<(OsString, PathBuf, fs::File)> {
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) => return Ok((temp_name, temp_path, fs::File::from(file))),
807 Err(error) if error == rustix::io::Errno::EXIST => {}
808 Err(error) => return Err(errno_to_io(error)),
809 }
810 }
811
812 Err(io::Error::new(
813 io::ErrorKind::AlreadyExists,
814 format!(
815 "could not allocate a unique sibling temporary file for {}",
816 parent.join(file_name).display()
817 ),
818 ))
819 }
820
821 fn open_directory(path: &Path) -> io::Result<OwnedFd> {
822 unix_fs::open(
823 path,
824 OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
825 Mode::empty(),
826 )
827 .map_err(errno_to_io)
828 }
829
830 #[cfg(test)]
831 fn remove_temp(parent_fd: &impl AsFd, temp_name: &OsStr) -> io::Result<()> {
832 unix_fs::unlinkat(parent_fd, temp_name, AtFlags::empty()).map_err(errno_to_io)
833 }
834
835 fn errno_to_io(error: rustix::io::Errno) -> io::Error {
836 io::Error::from_raw_os_error(error.raw_os_error())
837 }
838}