1#![deny(unsafe_code)]
8
9use std::ffi::{OsStr, OsString};
10use std::fs::File;
11use std::io;
12use std::path::{Path, PathBuf};
13
14mod cache_io;
15mod platform;
16#[cfg(windows)]
17#[allow(unsafe_code)]
18mod windows;
19#[cfg(windows)]
20mod windows_cas;
21
22pub use cache_io::DEFAULT_CACHE_RELEASE_WINDOW_BYTES;
23pub use cache_io::DurableFileCacheWriter;
24pub use cache_io::FileCacheReleaseEvidence;
25pub use cache_io::FileCacheReleaseOutcome;
26pub use cache_io::FileCacheReleaseTracker;
27pub use cache_io::FileCacheReleasingReader;
28pub use cache_io::cache_release_window_for_streams;
29pub use cache_io::release_file_cache;
30pub use cache_io::validate_cache_release_operation_windows;
31use platform::create_private_directory_platform;
32use platform::file_identity_platform;
33use platform::file_link_count_platform;
34use platform::file_space_usage_platform;
35use platform::install_new_file_platform;
36pub use platform::is_link_or_reparse;
37use platform::link_count;
38use platform::path_identity_platform;
39use platform::path_link_count_platform;
40use platform::rename_no_replace_platform;
41use platform::stable_child_names;
42use platform::stable_child_names_bounded;
43use platform::stable_create_child_directory;
44use platform::stable_link_child;
45use platform::stable_open_child_directory;
46use platform::stable_open_child_file;
47use platform::stable_open_directory;
48#[cfg(windows)]
49use platform::stable_open_directory_for_sync;
50use platform::stable_open_or_create_child_file;
51use platform::stable_open_publishing_child_file;
52use platform::stable_open_replaceable_child_file;
53use platform::stable_remove_child_directory_if_identity;
54use platform::stable_unlink_child_if_identity;
55use platform::visit_regular_files_platform;
56use platform::{replace_file_from_platform, replace_file_platform};
57#[cfg(windows)]
58pub use windows_cas::{WindowsCasWriter, WindowsLegacyCasAdopter, WindowsSealedCasFile};
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct FileIdentity {
63 pub volume_serial: u64,
65 pub file_id: [u8; 16],
67}
68
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct FileSpaceUsage {
72 pub logical_bytes: u64,
74 pub allocated_bytes: u64,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum DirectoryValidationStage {
82 NamedMetadata,
84 RetainedMetadata,
86 NamedIdentity,
88 RetainedIdentity,
90 IdentityChanged,
92}
93
94#[derive(Debug)]
96pub struct DirectoryValidationError {
97 stage: DirectoryValidationStage,
98 source: io::Error,
99}
100
101impl DirectoryValidationError {
102 fn new(stage: DirectoryValidationStage, source: io::Error) -> Self {
103 Self { stage, source }
104 }
105
106 #[must_use]
108 pub fn stage(&self) -> DirectoryValidationStage {
109 self.stage
110 }
111
112 #[must_use]
114 pub fn into_io_error(self) -> io::Error {
115 self.source
116 }
117}
118
119impl std::fmt::Display for DirectoryValidationError {
120 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121 std::fmt::Display::fmt(&self.source, formatter)
122 }
123}
124
125impl std::error::Error for DirectoryValidationError {
126 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127 Some(&self.source)
128 }
129}
130
131#[derive(Debug)]
134pub struct OpenedDirectoryHandle {
135 file: File,
136}
137
138impl OpenedDirectoryHandle {
139 #[must_use]
141 pub fn as_file(&self) -> &File {
142 &self.file
143 }
144
145 #[must_use]
147 pub fn into_file(self) -> File {
148 self.file
149 }
150}
151
152#[derive(Debug)]
155pub struct StableDirectory {
156 path: PathBuf,
157 file: File,
158 identity: FileIdentity,
159}
160
161#[derive(Debug)]
168pub struct UnpublishedArtifactGuard {
169 directory: StableDirectory,
170 candidate_names: Vec<OsString>,
171 identity: Option<FileIdentity>,
172 file: Option<File>,
173 published: bool,
174 parent_synced: bool,
175 armed: bool,
176}
177
178impl UnpublishedArtifactGuard {
179 pub fn identity(&self) -> io::Result<FileIdentity> {
184 self.identity
185 .ok_or_else(|| io::Error::other("unpublished artifact identity is not initialized"))
186 }
187
188 pub fn verify_identity_with(
195 &mut self,
196 setup_check: impl FnOnce() -> io::Result<()>,
197 ) -> io::Result<FileIdentity> {
198 let file = self
199 .file
200 .as_ref()
201 .ok_or_else(|| io::Error::other("unpublished artifact descriptor was transferred"))?;
202 let observed = file_identity(file)?;
203 self.identity = Some(observed);
204 setup_check()?;
205 Ok(observed)
206 }
207
208 pub fn temporary_name(&self) -> io::Result<&OsStr> {
210 self.candidate_names
211 .first()
212 .map(OsString::as_os_str)
213 .ok_or_else(|| io::Error::other("unpublished artifact has no temporary name"))
214 }
215
216 pub fn open_sibling(&self, name: &OsStr) -> io::Result<File> {
222 self.directory.open_child_file(name)
223 }
224
225 pub fn take_file(&mut self) -> io::Result<File> {
231 if self.identity.is_none() {
232 let file = self
233 .file
234 .as_ref()
235 .ok_or_else(|| io::Error::other("unpublished artifact file already transferred"))?;
236 self.identity = Some(file_identity(file)?);
237 }
238 self.file
239 .take()
240 .ok_or_else(|| io::Error::other("unpublished artifact file already transferred"))
241 }
242
243 pub fn transfer_unpublished_owner(
246 self,
247 file: &File,
248 ) -> io::Result<(StableDirectory, OsString, FileIdentity)> {
249 let directory = self.directory.try_clone()?;
250 self.transfer_unpublished_owner_in(directory, file)
251 }
252
253 pub fn transfer_unpublished_owner_in(
255 mut self,
256 directory: StableDirectory,
257 file: &File,
258 ) -> io::Result<(StableDirectory, OsString, FileIdentity)> {
259 if self.published || self.file.is_some() || self.candidate_names.len() != 1 {
260 return Err(io::Error::other(
261 "unpublished owner transfer invariants are incomplete",
262 ));
263 }
264 let expected = self.identity()?;
265 self.directory.revalidate_named()?;
266 directory.revalidate_named()?;
267 let name = self.candidate_names[0].clone();
268 let named = directory.open_child_file(&name)?;
269 if directory.identity() != self.directory.identity()
270 || file_identity(file)? != expected
271 || file_identity(&named)? != expected
272 || file_link_count(file)? != 1
273 {
274 return Err(io::Error::other(
275 "unpublished owner transfer identity changed",
276 ));
277 }
278 self.armed = false;
279 Ok((directory, name, expected))
280 }
281
282 pub fn install_child(&mut self, target: &OsStr) -> io::Result<()> {
287 validate_child_name(target)?;
288 let temporary = self
289 .candidate_names
290 .first()
291 .ok_or_else(|| io::Error::other("unpublished artifact has no temporary name"))?
292 .clone();
293 if !self.candidate_names.iter().any(|name| name == target) {
294 self.candidate_names.push(target.to_owned());
295 }
296 self.directory
297 .install_child(&temporary, self.identity()?, target)?;
298 self.published = true;
299 self.parent_synced = false;
300 Ok(())
301 }
302
303 pub fn sync_parent(&mut self) -> io::Result<()> {
308 self.sync_parent_with(StableDirectory::sync)
309 }
310
311 pub fn sync_parent_with(
313 &mut self,
314 acknowledge: impl FnOnce(&StableDirectory) -> io::Result<()>,
315 ) -> io::Result<()> {
316 acknowledge(&self.directory)?;
317 self.parent_synced = true;
318 Ok(())
319 }
320
321 pub fn commit(mut self) -> io::Result<()> {
327 if self.file.is_some() || !self.published || !self.parent_synced {
328 return Err(io::Error::other(
329 "unpublished artifact commit invariants are incomplete",
330 ));
331 }
332 self.armed = false;
333 Ok(())
334 }
335
336 pub fn cleanup(&mut self) -> io::Result<()> {
345 if !self.armed {
346 return Ok(());
347 }
348 let mut cleanup = Ok(());
349 let identity = match self.identity {
350 Some(identity) => Some(identity),
351 None => match self
352 .file
353 .as_ref()
354 .ok_or_else(|| io::Error::other("unpublished artifact descriptor is unavailable"))
355 .and_then(file_identity)
356 {
357 Ok(identity) => {
358 self.identity = Some(identity);
359 Some(identity)
360 }
361 Err(error) => {
362 cleanup = append_sanitized_cleanup(
363 cleanup,
364 &error,
365 "unpublished artifact identity recovery failed",
366 );
367 None
368 }
369 },
370 };
371 self.file.take();
372 for name in &self.candidate_names {
373 let Some(identity) = identity else {
374 break;
375 };
376 let removed = self.directory.open_child_file(name).and_then(|file| {
377 if file_identity(&file)? == identity {
378 drop(file);
379 self.directory.unlink_child_if_identity(name, identity)?;
380 }
381 Ok(())
382 });
383 match removed {
384 Ok(()) => {}
385 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
386 Err(error) => {
387 cleanup = append_sanitized_cleanup(
388 cleanup,
389 &error,
390 "unpublished artifact unlink failed",
391 );
392 }
393 }
394 }
395 cleanup = match self.directory.sync() {
396 Ok(()) => cleanup,
397 Err(error) => append_sanitized_cleanup(
398 cleanup,
399 &error,
400 "unpublished artifact directory synchronization failed",
401 ),
402 };
403 self.armed = false;
404 cleanup
405 }
406
407 pub fn cleanup_checked(
414 &mut self,
415 finalization_check: impl FnOnce() -> io::Result<()>,
416 ) -> io::Result<()> {
417 let cleanup = self.cleanup();
418 match finalization_check() {
419 Ok(()) => cleanup,
420 Err(error) => append_sanitized_cleanup(
421 cleanup,
422 &error,
423 "unpublished artifact cleanup finalization failed",
424 ),
425 }
426 }
427}
428
429impl Drop for UnpublishedArtifactGuard {
430 fn drop(&mut self) {
431 let _ = self.cleanup();
432 }
433}
434
435fn append_sanitized_cleanup(
436 primary: io::Result<()>,
437 cleanup: &io::Error,
438 context: &'static str,
439) -> io::Result<()> {
440 let cleanup = io::Error::new(cleanup.kind(), context);
441 match primary {
442 Ok(()) => Err(cleanup),
443 Err(primary) => Err(io::Error::new(
444 primary.kind(),
445 format!("{primary}; {cleanup}"),
446 )),
447 }
448}
449
450impl StableDirectory {
451 pub fn try_clone(&self) -> io::Result<Self> {
457 self.revalidate_named()?;
458 let clone = Self {
459 path: self.path.clone(),
460 file: self.file.try_clone()?,
461 identity: self.identity,
462 };
463 clone.revalidate_named()?;
464 Ok(clone)
465 }
466
467 #[cfg(unix)]
472 pub fn lock_shared(&self) -> io::Result<()> {
473 <File as fs4::FileExt>::lock_shared(&self.file)
474 }
475
476 #[cfg(unix)]
478 pub fn lock_exclusive(&self) -> io::Result<()> {
479 <File as fs4::FileExt>::lock(&self.file)
480 }
481
482 #[cfg(unix)]
484 pub fn try_lock_exclusive(&self) -> io::Result<bool> {
485 match <File as fs4::FileExt>::try_lock(&self.file) {
486 Ok(()) => Ok(true),
487 Err(fs4::TryLockError::WouldBlock) => Ok(false),
488 Err(fs4::TryLockError::Error(error)) => Err(error),
489 }
490 }
491
492 #[cfg(unix)]
494 pub fn unlock(&self) -> io::Result<()> {
495 <File as fs4::FileExt>::unlock(&self.file)
496 }
497
498 pub fn open(path: &Path) -> io::Result<Self> {
500 let file = stable_open_directory(path)?;
501 let identity = file_identity(&file)?;
502 let directory = Self {
503 path: path.to_path_buf(),
504 file,
505 identity,
506 };
507 directory.revalidate_named()?;
508 Ok(directory)
509 }
510
511 pub fn from_retained_handle(
514 path: PathBuf,
515 handle: OpenedDirectoryHandle,
516 identity: FileIdentity,
517 ) -> Result<Self, DirectoryValidationError> {
518 let directory = Self {
519 path,
520 file: handle.file,
521 identity,
522 };
523 directory.revalidate_named_detailed()?;
524 Ok(directory)
525 }
526
527 #[must_use]
530 pub fn as_file(&self) -> &File {
531 &self.file
532 }
533
534 #[must_use]
536 pub fn path(&self) -> &Path {
537 &self.path
538 }
539
540 #[must_use]
542 pub fn into_handle(self) -> OpenedDirectoryHandle {
543 OpenedDirectoryHandle { file: self.file }
544 }
545
546 pub fn revalidate_named_detailed(&self) -> Result<(), DirectoryValidationError> {
549 use DirectoryValidationStage as Stage;
550 let named = std::fs::symlink_metadata(&self.path)
551 .map_err(|error| DirectoryValidationError::new(Stage::NamedMetadata, error))?;
552 let retained = self
553 .file
554 .metadata()
555 .map_err(|error| DirectoryValidationError::new(Stage::RetainedMetadata, error))?;
556 if !named.is_dir() || is_link_or_reparse(&named) || !retained.is_dir() {
557 return Err(DirectoryValidationError::new(
558 Stage::IdentityChanged,
559 io::Error::other("stable directory path is linked or special"),
560 ));
561 }
562 #[cfg(unix)]
563 let named_identity = platform::metadata_identity(&named);
564 #[cfg(not(unix))]
565 let named_identity = path_identity(&self.path)
566 .map_err(|error| DirectoryValidationError::new(Stage::NamedIdentity, error))?;
567 if named_identity != self.identity {
568 return Err(DirectoryValidationError::new(
569 Stage::IdentityChanged,
570 io::Error::other("stable directory identity changed"),
571 ));
572 }
573 #[cfg(unix)]
574 let retained_identity = platform::metadata_identity(&retained);
575 #[cfg(not(unix))]
576 let retained_identity = file_identity(&self.file)
577 .map_err(|error| DirectoryValidationError::new(Stage::RetainedIdentity, error))?;
578 if retained_identity != self.identity {
579 return Err(DirectoryValidationError::new(
580 Stage::IdentityChanged,
581 io::Error::other("stable directory identity changed"),
582 ));
583 }
584 Ok(())
585 }
586
587 pub fn revalidate_named(&self) -> io::Result<()> {
589 self.revalidate_named_detailed()
590 .map_err(DirectoryValidationError::into_io_error)
591 }
592
593 pub fn open_child_directory(&self, name: &OsStr) -> io::Result<Self> {
595 validate_child_name(name)?;
596 self.revalidate_named()?;
597 let path = self.path.join(name);
598 let file = stable_open_child_directory(&self.file, &path, name)?;
599 let child = Self {
600 identity: file_identity(&file)?,
601 path,
602 file,
603 };
604 child.revalidate_named()?;
605 self.revalidate_named()?;
606 Ok(child)
607 }
608
609 pub fn create_child_directory(&self, name: &OsStr) -> io::Result<Self> {
611 validate_child_name(name)?;
612 self.revalidate_named()?;
613 stable_create_child_directory(&self.file, &self.path.join(name), name)?;
614 self.open_child_directory(name)
615 }
616
617 pub fn open_child_file(&self, name: &OsStr) -> io::Result<File> {
619 validate_child_name(name)?;
620 self.revalidate_named()?;
621 let path = self.path.join(name);
622 let file = stable_open_child_file(&self.file, &path, name, false)?;
623 validate_stable_child_file(&file, &path)?;
624 self.revalidate_named()?;
625 Ok(file)
626 }
627
628 pub fn revalidate_child_file(&self, name: &OsStr, file: &File) -> io::Result<()> {
633 validate_child_name(name)?;
634 self.revalidate_named()?;
635 validate_stable_child_file(file, &self.path.join(name))?;
636 self.revalidate_named()
637 }
638
639 pub fn open_publishing_child_file(
643 &self,
644 name: &OsStr,
645 expected: FileIdentity,
646 ) -> io::Result<File> {
647 validate_child_name(name)?;
648 self.revalidate_named()?;
649 let path = self.path.join(name);
650 let file = stable_open_publishing_child_file(&self.file, &path, name)?;
651 validate_stable_child_file(&file, &path)?;
652 if file_identity(&file)? != expected || file_link_count(&file)? != 1 {
653 return Err(io::Error::other(
654 "publication source identity or link count changed",
655 ));
656 }
657 self.revalidate_named()?;
658 Ok(file)
659 }
660
661 pub fn visit_regular_files(
671 &self,
672 remaining: &mut usize,
673 visit: &mut impl FnMut(&File) -> io::Result<()>,
674 ) -> io::Result<()> {
675 self.revalidate_named()?;
676 visit_regular_files_platform(self, remaining, visit)?;
677 self.revalidate_named()
678 }
679
680 pub fn create_child_file(&self, name: &OsStr) -> io::Result<File> {
682 validate_child_name(name)?;
683 self.revalidate_named()?;
684 let path = self.path.join(name);
685 let file = stable_open_child_file(&self.file, &path, name, true)?;
686 validate_stable_child_file(&file, &path)?;
687 self.revalidate_named()?;
688 Ok(file)
689 }
690
691 #[cfg(windows)]
696 pub fn create_cas_child_file(&self, name: &OsStr) -> io::Result<WindowsCasWriter> {
697 validate_child_name(name)?;
698 self.revalidate_named()?;
699 let path = self.path.join(name);
700 let file = windows::create_cas_writer(&path)?;
701 validate_stable_child_file(&file, &path)?;
702 self.revalidate_named()?;
703 Ok(WindowsCasWriter {
704 identity: file_identity(&file)?,
705 file,
706 })
707 }
708
709 #[cfg(windows)]
711 pub fn seal_cas_child_file(
712 &self,
713 name: &OsStr,
714 writer: WindowsCasWriter,
715 ) -> io::Result<WindowsSealedCasFile> {
716 validate_child_name(name)?;
717 self.revalidate_named()?;
718 let path = self.path.join(name);
719 let expected = writer.identity;
720 if file_identity(&writer.file)? != expected || path_identity(&path)? != expected {
721 return Err(io::Error::other(
722 "CAS writer identity changed before sealing",
723 ));
724 }
725 windows::seal_cas_writer(&writer.file)?;
726 if file_identity(&writer.file)? != expected || path_identity(&path)? != expected {
727 return Err(io::Error::other(
728 "CAS writer identity changed while sealing",
729 ));
730 }
731 let bridge = windows::open_cas_bridge(&path)?;
732 if file_identity(&bridge)? != expected {
733 return Err(io::Error::other(
734 "CAS bridge identity changed while sealing",
735 ));
736 }
737 drop(writer.file);
738 let reader = windows::open_sealed_cas_reader(&path)?;
739 validate_stable_child_file(&reader, &path)?;
740 if file_identity(&reader)? != expected || path_identity(&path)? != expected {
741 return Err(io::Error::other(
742 "CAS identity changed while reopening sealed reader",
743 ));
744 }
745 drop(bridge);
746 self.revalidate_named()?;
747 Ok(WindowsSealedCasFile(reader))
748 }
749
750 #[cfg(windows)]
752 pub fn open_cas_child_file(&self, name: &OsStr) -> io::Result<WindowsSealedCasFile> {
753 validate_child_name(name)?;
754 self.revalidate_named()?;
755 let path = self.path.join(name);
756 let reader = windows::open_sealed_cas_reader(&path)?;
757 validate_stable_child_file(&reader, &path)?;
758 if !reader.metadata()?.permissions().readonly()
759 || !windows::has_canonical_cas_dacl(&reader)?
760 {
761 return Err(io::Error::new(
762 io::ErrorKind::PermissionDenied,
763 "CAS child is not canonically sealed",
764 ));
765 }
766 self.revalidate_named()?;
767 Ok(WindowsSealedCasFile(reader))
768 }
769
770 #[cfg(windows)]
772 pub fn open_legacy_cas_child_for_adoption(
773 &self,
774 name: &OsStr,
775 ) -> io::Result<WindowsLegacyCasAdopter> {
776 validate_child_name(name)?;
777 self.revalidate_named()?;
778 let path = self.path.join(name);
779 let file = windows::open_legacy_cas_adopter(&path)?;
780 validate_stable_child_file(&file, &path)?;
781 let identity = file_identity(&file)?;
782 if path_identity(&path)? != identity {
783 return Err(io::Error::other("legacy CAS identity changed during open"));
784 }
785 self.revalidate_named()?;
786 Ok(WindowsLegacyCasAdopter { file, identity })
787 }
788
789 #[cfg(windows)]
791 pub fn adopt_legacy_cas_child(
792 &self,
793 name: &OsStr,
794 adopter: WindowsLegacyCasAdopter,
795 ) -> io::Result<WindowsSealedCasFile> {
796 validate_child_name(name)?;
797 self.revalidate_named()?;
798 let path = self.path.join(name);
799 windows::set_canonical_cas_dacl(&adopter.file)?;
800 let bridge = windows::open_cas_bridge(&path)?;
801 if file_identity(&bridge)? != adopter.identity {
802 return Err(io::Error::other("legacy CAS bridge identity changed"));
803 }
804 drop(adopter.file);
805 let reader = windows::open_sealed_cas_reader(&path)?;
806 if file_identity(&reader)? != adopter.identity || path_identity(&path)? != adopter.identity
807 {
808 return Err(io::Error::other(
809 "legacy CAS identity changed during adoption",
810 ));
811 }
812 drop(bridge);
813 self.revalidate_named()?;
814 Ok(WindowsSealedCasFile(reader))
815 }
816
817 pub fn create_replaceable_child_file(&self, name: &OsStr) -> io::Result<File> {
820 validate_child_name(name)?;
821 self.revalidate_named()?;
822 let path = self.path.join(name);
823 let file = stable_open_replaceable_child_file(&self.file, &path, name)?;
824 validate_stable_child_file(&file, &path)?;
825 self.revalidate_named()?;
826 Ok(file)
827 }
828
829 pub fn create_unpublished_replaceable_child(
836 &self,
837 name: &OsStr,
838 ) -> io::Result<UnpublishedArtifactGuard> {
839 let directory = self.try_clone()?;
843 let file = self.create_replaceable_child_file(name)?;
844 Ok(UnpublishedArtifactGuard {
845 directory,
846 candidate_names: vec![name.to_owned()],
847 identity: None,
848 file: Some(file),
849 published: false,
850 parent_synced: false,
851 armed: true,
852 })
853 }
854
855 pub fn open_or_create_child_file(&self, name: &OsStr) -> io::Result<File> {
857 validate_child_name(name)?;
858 self.revalidate_named()?;
859 let path = self.path.join(name);
860 let file = stable_open_or_create_child_file(&self.file, &path, name)?;
861 validate_stable_child_file(&file, &path)?;
862 self.revalidate_named()?;
863 Ok(file)
864 }
865
866 pub fn child_names(&self) -> io::Result<Vec<std::ffi::OsString>> {
868 self.revalidate_named()?;
869 stable_child_names(&self.file, &self.path)
870 }
871
872 pub fn child_names_bounded(&self, limit: usize) -> io::Result<Vec<std::ffi::OsString>> {
876 self.revalidate_named()?;
877 stable_child_names_bounded(&self.file, &self.path, limit)
878 }
879
880 pub fn link_child_into(
882 &self,
883 source_name: &OsStr,
884 source: &File,
885 expected_source: FileIdentity,
886 destination: &Self,
887 destination_name: &OsStr,
888 ) -> io::Result<(File, FileIdentity)> {
889 validate_child_name(source_name)?;
890 validate_child_name(destination_name)?;
891 self.revalidate_named()?;
892 destination.revalidate_named()?;
893 validate_stable_child_file(source, &self.path.join(source_name))?;
894 if file_identity(source)? != expected_source {
895 return Err(io::Error::other("hard-link source identity changed"));
896 }
897 stable_link_child(
898 &self.file,
899 &self.path,
900 source_name,
901 &destination.file,
902 &destination.path,
903 destination_name,
904 )?;
905 self.revalidate_named()?;
906 destination.revalidate_named()?;
907 let installed = destination.open_child_file(destination_name)?;
908 let installed_identity = file_identity(&installed)?;
909 if installed_identity != expected_source {
910 return Err(io::Error::other("hard-link destination identity mismatch"));
911 }
912 Ok((installed, installed_identity))
913 }
914
915 pub fn unlink_child_if_identity(&self, name: &OsStr, expected: FileIdentity) -> io::Result<()> {
918 validate_child_name(name)?;
919 self.revalidate_named()?;
920 stable_unlink_child_if_identity(&self.file, &self.path, name, expected)?;
921 self.revalidate_named()
922 }
923
924 pub fn remove_child_directory_if_identity(
928 &self,
929 name: &OsStr,
930 expected: FileIdentity,
931 ) -> io::Result<()> {
932 validate_child_name(name)?;
933 self.revalidate_named()?;
934 stable_remove_child_directory_if_identity(&self.file, &self.path, name, expected)?;
935 self.revalidate_named()
936 }
937
938 pub fn replace_child(
941 &self,
942 temporary: &OsStr,
943 expected_temporary: FileIdentity,
944 target: &OsStr,
945 ) -> io::Result<()> {
946 self.replace_child_typed(temporary, expected_temporary, target)
947 .map_err(|error| io::Error::other(error.to_string()))
948 }
949
950 pub fn replace_child_typed(
952 &self,
953 temporary: &OsStr,
954 expected_temporary: FileIdentity,
955 target: &OsStr,
956 ) -> Result<(), ReplaceFileError> {
957 let prepare = || -> io::Result<bool> {
958 validate_child_name(temporary)?;
959 validate_child_name(target)?;
960 self.revalidate_named()?;
961 let temporary_file = self.open_child_file(temporary)?;
962 if file_identity(&temporary_file)? != expected_temporary
963 || file_link_count(&temporary_file)? != 1
964 {
965 return Err(io::Error::other(
966 "atomic temporary child identity or link count changed",
967 ));
968 }
969 match self.open_child_file(target) {
970 Ok(_) => Ok(true),
971 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
972 Err(error) => Err(error),
973 }
974 };
975 if prepare().map_err(ReplaceFileError::NotReplaced)? {
976 replace_file_platform(
977 &self.file,
978 temporary,
979 target,
980 Some(expected_temporary),
981 None,
982 )?;
983 } else {
984 install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))
985 .map_err(ReplaceFileError::StateUnknown)?;
986 }
987 self.validate_replaced_child(target, expected_temporary)
988 .map_err(ReplaceFileError::StateUnknown)
989 }
990
991 fn validate_replaced_child(&self, target: &OsStr, expected: FileIdentity) -> io::Result<()> {
992 self.revalidate_named()?;
993 let installed = self.open_child_file(target)?;
994 if file_identity(&installed)? != expected || file_link_count(&installed)? != 1 {
995 return Err(io::Error::other(
996 "replacement target identity or link count changed",
997 ));
998 }
999 Ok(())
1000 }
1001
1002 pub fn replace_authenticated_child(
1007 &self,
1008 temporary: &OsStr,
1009 expected_temporary: FileIdentity,
1010 target: &OsStr,
1011 expected_target: FileIdentity,
1012 ) -> io::Result<()> {
1013 self.replace_authenticated_child_typed(
1014 temporary,
1015 expected_temporary,
1016 target,
1017 expected_target,
1018 )
1019 .map_err(|error| io::Error::other(error.to_string()))
1020 }
1021
1022 pub fn replace_authenticated_child_typed(
1024 &self,
1025 temporary: &OsStr,
1026 expected_temporary: FileIdentity,
1027 target: &OsStr,
1028 expected_target: FileIdentity,
1029 ) -> Result<(), ReplaceFileError> {
1030 validate_child_name(temporary).map_err(ReplaceFileError::NotReplaced)?;
1031 validate_child_name(target).map_err(ReplaceFileError::NotReplaced)?;
1032 self.revalidate_named()
1033 .map_err(ReplaceFileError::NotReplaced)?;
1034 replace_file_platform(
1035 &self.file,
1036 temporary,
1037 target,
1038 Some(expected_temporary),
1039 Some(expected_target),
1040 )?;
1041 self.validate_replaced_child(target, expected_temporary)
1042 .map_err(ReplaceFileError::StateUnknown)
1043 }
1044
1045 #[doc(hidden)]
1054 pub fn replace_authenticated_child_from(
1055 &self,
1056 source_directory: &Self,
1057 temporary: &OsStr,
1058 expected_temporary: FileIdentity,
1059 target: &OsStr,
1060 expected_target: FileIdentity,
1061 ) -> Result<(), ReplaceFileError> {
1062 validate_child_name(temporary).map_err(ReplaceFileError::NotReplaced)?;
1063 validate_child_name(target).map_err(ReplaceFileError::NotReplaced)?;
1064 source_directory
1065 .revalidate_named()
1066 .map_err(ReplaceFileError::NotReplaced)?;
1067 self.revalidate_named()
1068 .map_err(ReplaceFileError::NotReplaced)?;
1069 replace_file_from_platform(
1070 &source_directory.file,
1071 &self.file,
1072 temporary,
1073 target,
1074 Some(expected_temporary),
1075 Some(expected_target),
1076 )?;
1077 source_directory
1078 .revalidate_named()
1079 .map_err(ReplaceFileError::StateUnknown)?;
1080 self.validate_replaced_child(target, expected_temporary)
1081 .map_err(ReplaceFileError::StateUnknown)
1082 }
1083
1084 pub fn install_child(
1093 &self,
1094 temporary: &OsStr,
1095 expected_temporary: FileIdentity,
1096 target: &OsStr,
1097 ) -> io::Result<()> {
1098 validate_child_name(temporary)?;
1099 validate_child_name(target)?;
1100 self.revalidate_named()?;
1101 let temporary_file = self.open_child_file(temporary)?;
1102 if file_identity(&temporary_file)? != expected_temporary
1103 || file_link_count(&temporary_file)? != 1
1104 {
1105 return Err(io::Error::other(
1106 "atomic temporary child identity or link count changed",
1107 ));
1108 }
1109 drop(temporary_file);
1110 install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))?;
1111 self.revalidate_named()?;
1112 self.open_child_file(target).map(|_| ())
1113 }
1114
1115 pub fn install_child_typed(
1118 &self,
1119 temporary: &OsStr,
1120 expected_temporary: FileIdentity,
1121 target: &OsStr,
1122 ) -> Result<(), ReplaceFileError> {
1123 let prepare = || -> io::Result<()> {
1124 validate_child_name(temporary)?;
1125 validate_child_name(target)?;
1126 self.revalidate_named()?;
1127 let file = self.open_child_file(temporary)?;
1128 if file_identity(&file)? != expected_temporary || file_link_count(&file)? != 1 {
1129 return Err(io::Error::other(
1130 "atomic temporary child identity or link count changed",
1131 ));
1132 }
1133 Ok(())
1134 };
1135 prepare().map_err(ReplaceFileError::NotReplaced)?;
1136 install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))
1137 .map_err(|error| {
1138 if error.kind() == io::ErrorKind::AlreadyExists {
1139 ReplaceFileError::NotReplaced(error)
1140 } else {
1141 ReplaceFileError::StateUnknown(error)
1142 }
1143 })?;
1144 self.validate_replaced_child(target, expected_temporary)
1145 .map_err(ReplaceFileError::StateUnknown)
1146 }
1147
1148 pub fn sync(&self) -> io::Result<()> {
1150 self.revalidate_named()?;
1151 #[cfg(windows)]
1152 {
1153 let directory = stable_open_directory_for_sync(&self.path)?;
1154 if file_identity(&directory)? != self.identity {
1155 return Err(io::Error::other(
1156 "stable directory identity changed before sync",
1157 ));
1158 }
1159 directory.observed_sync_all()?;
1160 self.revalidate_named()
1161 }
1162 #[cfg(not(windows))]
1163 {
1164 self.file.observed_sync_all()?;
1165 self.revalidate_named()
1166 }
1167 }
1168
1169 #[must_use]
1171 pub fn identity(&self) -> FileIdentity {
1172 self.identity
1173 }
1174}
1175
1176pub fn open_directory_handle(path: &Path) -> io::Result<OpenedDirectoryHandle> {
1180 stable_open_directory(path).map(|file| OpenedDirectoryHandle { file })
1181}
1182
1183fn validate_child_name(name: &OsStr) -> io::Result<()> {
1184 let path = Path::new(name);
1185 if name.is_empty()
1186 || path.is_absolute()
1187 || path.components().count() != 1
1188 || matches!(name.to_str(), Some("." | ".."))
1189 {
1190 return Err(io::Error::new(
1191 io::ErrorKind::InvalidInput,
1192 "invalid child name",
1193 ));
1194 }
1195 Ok(())
1196}
1197
1198fn validate_stable_child_file(file: &File, path: &Path) -> io::Result<()> {
1199 let metadata = file.metadata()?;
1200 let named = std::fs::symlink_metadata(path)?;
1201 if !metadata.is_file()
1202 || !named.is_file()
1203 || is_link_or_reparse(&named)
1204 || file_identity(file)? != path_identity(path)?
1205 {
1206 return Err(io::Error::other(
1207 "stable child is linked, special, or substituted",
1208 ));
1209 }
1210 Ok(())
1211}
1212
1213#[cfg(windows)]
1215#[derive(Debug, Clone, PartialEq, Eq)]
1216pub struct WindowsVolumeInformation {
1217 pub filesystem_name: String,
1219 pub read_only: bool,
1221 pub fixed: bool,
1223}
1224
1225pub fn create_private_directory(path: &Path) -> io::Result<()> {
1230 create_private_directory_platform(path)
1231}
1232
1233pub fn file_identity(file: &File) -> io::Result<FileIdentity> {
1235 file_identity_platform(file)
1236}
1237
1238pub fn file_space_usage(file: &File) -> io::Result<FileSpaceUsage> {
1244 file_space_usage_platform(file)
1245}
1246
1247pub fn path_identity(path: &Path) -> io::Result<FileIdentity> {
1249 path_identity_platform(path)
1250}
1251
1252pub fn file_link_count(file: &File) -> io::Result<u64> {
1254 file_link_count_platform(file)
1255}
1256
1257pub fn path_link_count(path: &Path) -> io::Result<u64> {
1259 path_link_count_platform(path)
1260}
1261
1262#[cfg(windows)]
1267pub fn windows_volume_information(path: &Path) -> io::Result<WindowsVolumeInformation> {
1268 windows::volume_information(path)
1269}
1270
1271#[derive(Debug)]
1273pub enum ReplaceFileError {
1274 NotReplaced(io::Error),
1277 StateUnknown(io::Error),
1281}
1282
1283impl std::fmt::Display for ReplaceFileError {
1284 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1285 match self {
1286 Self::NotReplaced(error) => write!(formatter, "file was not replaced: {error}"),
1287 Self::StateUnknown(error) => {
1288 write!(
1289 formatter,
1290 "replacement state requires reconciliation: {error}"
1291 )
1292 }
1293 }
1294 }
1295}
1296
1297impl std::error::Error for ReplaceFileError {
1298 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1299 Some(match self {
1300 Self::NotReplaced(error) | Self::StateUnknown(error) => error,
1301 })
1302 }
1303}
1304
1305#[doc(hidden)]
1310#[must_use]
1311pub fn classify_failed_replacement(
1312 error: io::Error,
1313 source_before: FileIdentity,
1314 target_before: FileIdentity,
1315 source_after: Option<FileIdentity>,
1316 target_after: Option<FileIdentity>,
1317) -> ReplaceFileError {
1318 if source_after == Some(source_before) && target_after == Some(target_before) {
1319 ReplaceFileError::NotReplaced(error)
1320 } else {
1321 ReplaceFileError::StateUnknown(error)
1322 }
1323}
1324
1325pub fn replace_file(
1333 directory: &File,
1334 source_name: &OsStr,
1335 target_name: &OsStr,
1336) -> Result<(), ReplaceFileError> {
1337 verify_single_component(source_name).map_err(ReplaceFileError::NotReplaced)?;
1338 verify_single_component(target_name).map_err(ReplaceFileError::NotReplaced)?;
1339 replace_file_platform(directory, source_name, target_name, None, None)
1340}
1341
1342pub fn install_new_file(
1347 directory: &File,
1348 source_name: &OsStr,
1349 target_name: &OsStr,
1350) -> io::Result<()> {
1351 verify_single_component(source_name)?;
1352 verify_single_component(target_name)?;
1353 install_new_file_platform(directory, source_name, target_name, None)
1354}
1355
1356pub fn rename_no_replace(source: &Path, destination: &Path) -> io::Result<()> {
1361 rename_no_replace_platform(source, destination)
1362}
1363
1364fn verify_single_component(name: &OsStr) -> io::Result<()> {
1365 let mut components = Path::new(name).components();
1366 if !matches!(components.next(), Some(std::path::Component::Normal(_)))
1367 || components.next().is_some()
1368 {
1369 return Err(io::Error::new(
1370 io::ErrorKind::InvalidInput,
1371 "filesystem operation requires one plain name",
1372 ));
1373 }
1374 Ok(())
1375}
1376
1377fn verify_regular_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
1378 if is_link_or_reparse(metadata) || !metadata.is_file() || link_count(metadata) != 1 {
1379 return Err(io::Error::other(
1380 "replacement path is not a regular non-link file",
1381 ));
1382 }
1383 Ok(())
1384}
1385
1386fn verify_space_usage_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
1387 if is_link_or_reparse(metadata) || !metadata.is_file() {
1388 return Err(io::Error::other(
1389 "space usage handle is not a regular non-reparse file",
1390 ));
1391 }
1392 Ok(())
1393}
1394
1395#[cfg(test)]
1396mod tests;
1397
1398pub mod observation;
1400pub use observation::ObservedSync;