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