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