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