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