1use std::{error::Error, fmt, path::Path, str::FromStr};
4
5const DIRECTORY_IDENTITY_PREFIX: &str = "rustferry-directory-v1:";
6const REGULAR_FILE_IDENTITY_PREFIX: &str = "rustferry-regular-file-v1:";
7const WINDOWS_PLATFORM: &str = "windows:";
8const UNIX_PLATFORM: &str = "unix:";
9const WINDOWS_VOLUME_HEX_LEN: usize = 16;
10const WINDOWS_FILE_ID_HEX_LEN: usize = 32;
11const UNIX_COMPONENT_HEX_LEN: usize = 16;
12
13#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15#[non_exhaustive]
16pub enum DirectoryIdentityOperation {
17 OpenDirectory,
19 OpenRegularFile,
21 QueryAttributes,
23 QueryLinkCount,
25 QueryMetadata,
27 CloneHandle,
29 FlushDirectory,
31 RemoveRegularFile,
33}
34
35#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37#[non_exhaustive]
38pub enum DirectoryIdentityErrorKind {
39 InvalidPath,
41 NotDirectory,
43 NotRegularFile,
45 ReparsePoint,
47 MultipleLinks,
49 IdentityUnavailable,
51 OperatingSystem(DirectoryIdentityOperation),
53 InvalidEncoding,
55 IdentityMismatch,
57 UnsupportedPlatform,
59}
60
61#[derive(Clone, Copy, Debug, Eq, PartialEq)]
63pub struct DirectoryIdentityError {
64 kind: DirectoryIdentityErrorKind,
65 os_code: Option<i32>,
66}
67
68impl DirectoryIdentityError {
69 const fn new(kind: DirectoryIdentityErrorKind, os_code: Option<i32>) -> Self {
70 Self { kind, os_code }
71 }
72
73 pub const fn kind(&self) -> DirectoryIdentityErrorKind {
75 self.kind
76 }
77
78 pub const fn os_code(&self) -> Option<i32> {
80 self.os_code
81 }
82}
83
84impl fmt::Display for DirectoryIdentityError {
85 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
86 match self.kind {
87 DirectoryIdentityErrorKind::InvalidPath => {
88 formatter.write_str("filesystem identity requires a valid absolute path")
89 }
90 DirectoryIdentityErrorKind::NotDirectory => {
91 formatter.write_str("directory identity target is not a directory")
92 }
93 DirectoryIdentityErrorKind::NotRegularFile => {
94 formatter.write_str("file identity target is not a regular file")
95 }
96 DirectoryIdentityErrorKind::ReparsePoint => formatter
97 .write_str("filesystem identity target is a symbolic link or reparse point"),
98 DirectoryIdentityErrorKind::MultipleLinks => {
99 formatter.write_str("file identity target does not have exactly one link")
100 }
101 DirectoryIdentityErrorKind::IdentityUnavailable => {
102 formatter.write_str("filesystem did not provide a persistent object identity")
103 }
104 DirectoryIdentityErrorKind::OperatingSystem(operation) => {
105 write!(
106 formatter,
107 "filesystem identity operation {operation:?} failed"
108 )
109 }
110 DirectoryIdentityErrorKind::InvalidEncoding => {
111 formatter.write_str("persisted filesystem identity has an invalid encoding")
112 }
113 DirectoryIdentityErrorKind::IdentityMismatch => {
114 formatter.write_str("filesystem identity does not match the persisted value")
115 }
116 DirectoryIdentityErrorKind::UnsupportedPlatform => {
117 formatter.write_str("filesystem identity is unsupported on this platform")
118 }
119 }
120 }
121}
122
123impl Error for DirectoryIdentityError {}
124
125#[derive(Clone, Debug, Eq, Hash, PartialEq)]
130pub struct DirectoryFilesystemIdentity(String);
131
132impl DirectoryFilesystemIdentity {
133 pub fn capture(path: &Path) -> Result<Self, DirectoryIdentityError> {
140 if !path.is_absolute() {
141 return Err(DirectoryIdentityError::new(
142 DirectoryIdentityErrorKind::InvalidPath,
143 None,
144 ));
145 }
146 platform::capture(path)
147 }
148
149 pub fn as_str(&self) -> &str {
151 &self.0
152 }
153
154 #[cfg(windows)]
155 fn from_windows(volume: u64, file_id: [u8; 16]) -> Self {
156 Self(format!(
157 "{DIRECTORY_IDENTITY_PREFIX}{WINDOWS_PLATFORM}{volume:016x}:{}",
158 hex::encode(file_id)
159 ))
160 }
161
162 #[cfg(unix)]
163 fn from_unix(device: u64, inode: u64) -> Self {
164 Self(format!(
165 "{DIRECTORY_IDENTITY_PREFIX}{UNIX_PLATFORM}{device:016x}:{inode:016x}"
166 ))
167 }
168}
169
170impl AsRef<str> for DirectoryFilesystemIdentity {
171 fn as_ref(&self) -> &str {
172 self.as_str()
173 }
174}
175
176impl fmt::Display for DirectoryFilesystemIdentity {
177 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
178 formatter.write_str(self.as_str())
179 }
180}
181
182impl FromStr for DirectoryFilesystemIdentity {
183 type Err = DirectoryIdentityError;
184
185 fn from_str(value: &str) -> Result<Self, Self::Err> {
186 if !valid_identity_encoding(value, DIRECTORY_IDENTITY_PREFIX) {
187 return Err(invalid_encoding());
188 }
189 Ok(Self(value.to_owned()))
190 }
191}
192
193pub struct RetainedDirectoryIdentity {
201 file: std::fs::File,
202 identity: DirectoryFilesystemIdentity,
203}
204
205impl RetainedDirectoryIdentity {
206 pub fn open(path: &Path) -> Result<Self, DirectoryIdentityError> {
218 if !path.is_absolute() {
219 return Err(DirectoryIdentityError::new(
220 DirectoryIdentityErrorKind::InvalidPath,
221 None,
222 ));
223 }
224 let (file, identity) = platform::open_retained_directory(path)?;
225 Ok(Self { file, identity })
226 }
227
228 pub fn identity(&self) -> &DirectoryFilesystemIdentity {
230 &self.identity
231 }
232
233 pub fn as_file(&self) -> &std::fs::File {
235 &self.file
236 }
237
238 pub fn try_clone_file(&self) -> Result<std::fs::File, DirectoryIdentityError> {
247 self.file.try_clone().map_err(|error| {
248 DirectoryIdentityError::new(
249 DirectoryIdentityErrorKind::OperatingSystem(
250 DirectoryIdentityOperation::CloneHandle,
251 ),
252 error.raw_os_error(),
253 )
254 })
255 }
256
257 pub fn verify_path(&self, path: &Path) -> Result<(), DirectoryIdentityError> {
268 if !path.is_absolute() {
269 return Err(DirectoryIdentityError::new(
270 DirectoryIdentityErrorKind::InvalidPath,
271 None,
272 ));
273 }
274 platform::verify_retained_directory_path(&self.file, &self.identity, path)
275 }
276
277 #[cfg(windows)]
295 pub fn sync_metadata(&self, path: &Path) -> Result<(), DirectoryIdentityError> {
296 if !path.is_absolute() {
297 return Err(DirectoryIdentityError::new(
298 DirectoryIdentityErrorKind::InvalidPath,
299 None,
300 ));
301 }
302 platform::sync_retained_directory_metadata(&self.file, &self.identity, path)
303 }
304}
305
306impl fmt::Debug for RetainedDirectoryIdentity {
307 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
308 formatter
309 .debug_struct("RetainedDirectoryIdentity")
310 .field("identity", &self.identity)
311 .finish_non_exhaustive()
312 }
313}
314
315#[derive(Clone, Debug, Eq, Hash, PartialEq)]
320pub struct RegularFileFilesystemIdentity(String);
321
322impl RegularFileFilesystemIdentity {
323 pub fn capture(path: &Path) -> Result<Self, DirectoryIdentityError> {
330 if !path.is_absolute() {
331 return Err(DirectoryIdentityError::new(
332 DirectoryIdentityErrorKind::InvalidPath,
333 None,
334 ));
335 }
336 platform::capture_regular_file(path)
337 }
338
339 pub fn as_str(&self) -> &str {
341 &self.0
342 }
343
344 #[cfg(windows)]
345 fn from_windows(volume: u64, file_id: [u8; 16]) -> Self {
346 Self(format!(
347 "{REGULAR_FILE_IDENTITY_PREFIX}{WINDOWS_PLATFORM}{volume:016x}:{}",
348 hex::encode(file_id)
349 ))
350 }
351
352 #[cfg(unix)]
353 fn from_unix(device: u64, inode: u64) -> Self {
354 Self(format!(
355 "{REGULAR_FILE_IDENTITY_PREFIX}{UNIX_PLATFORM}{device:016x}:{inode:016x}"
356 ))
357 }
358}
359
360impl AsRef<str> for RegularFileFilesystemIdentity {
361 fn as_ref(&self) -> &str {
362 self.as_str()
363 }
364}
365
366impl fmt::Display for RegularFileFilesystemIdentity {
367 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
368 formatter.write_str(self.as_str())
369 }
370}
371
372impl FromStr for RegularFileFilesystemIdentity {
373 type Err = DirectoryIdentityError;
374
375 fn from_str(value: &str) -> Result<Self, Self::Err> {
376 if !valid_identity_encoding(value, REGULAR_FILE_IDENTITY_PREFIX) {
377 return Err(invalid_encoding());
378 }
379 Ok(Self(value.to_owned()))
380 }
381}
382
383#[derive(Debug)]
390pub struct RetainedRegularFileIdentity {
391 file: std::fs::File,
392 identity: RegularFileFilesystemIdentity,
393}
394
395impl RetainedRegularFileIdentity {
396 pub fn open(path: &Path) -> Result<Self, DirectoryIdentityError> {
403 if !path.is_absolute() {
404 return Err(DirectoryIdentityError::new(
405 DirectoryIdentityErrorKind::InvalidPath,
406 None,
407 ));
408 }
409 let (file, identity) = platform::open_retained_regular_file(path)?;
410 Ok(Self { file, identity })
411 }
412
413 pub fn identity(&self) -> &RegularFileFilesystemIdentity {
415 &self.identity
416 }
417
418 pub fn verify_path(&self, path: &Path) -> Result<(), DirectoryIdentityError> {
425 if !path.is_absolute() {
426 return Err(DirectoryIdentityError::new(
427 DirectoryIdentityErrorKind::InvalidPath,
428 None,
429 ));
430 }
431 platform::verify_retained_regular_file_path(&self.file, &self.identity, path)
432 }
433}
434
435#[cfg(windows)]
441#[derive(Debug)]
442pub struct ExactRegularFileRemoval {
443 file: std::fs::File,
444 identity: RegularFileFilesystemIdentity,
445}
446
447#[cfg(windows)]
448impl ExactRegularFileRemoval {
449 pub fn identity(&self) -> &RegularFileFilesystemIdentity {
453 &self.identity
454 }
455
456 pub fn remove(self) -> Result<(), DirectoryIdentityError> {
467 platform::remove_exact_regular_file(self)
468 }
469}
470
471pub fn regular_file_identity_from_file(
482 file: &std::fs::File,
483) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
484 platform::regular_file_identity_from_file(file)
485}
486
487pub fn directory_identity_from_file(
498 file: &std::fs::File,
499) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
500 platform::directory_identity_from_file(file)
501}
502
503#[cfg(unix)]
505#[doc(hidden)]
506pub fn retained_directory_is_unlinked(file: &std::fs::File) -> std::io::Result<bool> {
507 platform::retained_directory_is_unlinked(file)
508}
509
510#[cfg(windows)]
523pub fn open_regular_file_for_exact_removal(
524 path: &Path,
525) -> Result<ExactRegularFileRemoval, DirectoryIdentityError> {
526 if !path.is_absolute() {
527 return Err(DirectoryIdentityError::new(
528 DirectoryIdentityErrorKind::InvalidPath,
529 None,
530 ));
531 }
532 platform::open_regular_file_for_exact_removal(path)
533}
534
535pub fn verify_directory_identity(
542 path: &Path,
543 expected: &DirectoryFilesystemIdentity,
544) -> Result<(), DirectoryIdentityError> {
545 let actual = DirectoryFilesystemIdentity::capture(path)?;
546 if actual != *expected {
547 return Err(DirectoryIdentityError::new(
548 DirectoryIdentityErrorKind::IdentityMismatch,
549 None,
550 ));
551 }
552 Ok(())
553}
554
555pub fn verify_regular_file_identity(
562 path: &Path,
563 expected: &RegularFileFilesystemIdentity,
564) -> Result<(), DirectoryIdentityError> {
565 let actual = RegularFileFilesystemIdentity::capture(path)?;
566 if actual != *expected {
567 return Err(DirectoryIdentityError::new(
568 DirectoryIdentityErrorKind::IdentityMismatch,
569 None,
570 ));
571 }
572 Ok(())
573}
574
575fn invalid_encoding() -> DirectoryIdentityError {
576 DirectoryIdentityError::new(DirectoryIdentityErrorKind::InvalidEncoding, None)
577}
578
579fn valid_identity_encoding(value: &str, prefix: &str) -> bool {
580 let Some(encoded) = value.strip_prefix(prefix) else {
581 return false;
582 };
583 if let Some(encoded) = encoded.strip_prefix(WINDOWS_PLATFORM) {
584 valid_pair(encoded, WINDOWS_VOLUME_HEX_LEN, WINDOWS_FILE_ID_HEX_LEN)
585 } else if let Some(encoded) = encoded.strip_prefix(UNIX_PLATFORM) {
586 valid_pair(encoded, UNIX_COMPONENT_HEX_LEN, UNIX_COMPONENT_HEX_LEN)
587 } else {
588 false
589 }
590}
591
592fn valid_pair(value: &str, first_len: usize, second_len: usize) -> bool {
593 let Some((first, second)) = value.split_once(':') else {
594 return false;
595 };
596 first.len() == first_len
597 && second.len() == second_len
598 && valid_lower_hex(first)
599 && valid_lower_hex(second)
600}
601
602fn valid_lower_hex(value: &str) -> bool {
603 value
604 .bytes()
605 .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
606}
607
608#[cfg(windows)]
609#[allow(unsafe_code)]
610mod platform {
611 use std::{
612 fs::File,
613 mem::size_of,
614 os::windows::{ffi::OsStrExt as _, io::FromRawHandle as _},
615 path::Path,
616 ptr,
617 };
618
619 use windows_sys::Wdk::Storage::FileSystem::NtFlushBuffersFileEx;
620 use windows_sys::Win32::{
621 Foundation::{INVALID_HANDLE_VALUE, RtlNtStatusToDosError},
622 Storage::FileSystem::{
623 BY_HANDLE_FILE_INFORMATION, CreateFileW, DELETE, FILE_ATTRIBUTE_DEVICE,
624 FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT, FILE_ATTRIBUTE_TAG_INFO,
625 FILE_DISPOSITION_INFO, FILE_FLAG_BACKUP_SEMANTICS, FILE_FLAG_OPEN_REPARSE_POINT,
626 FILE_GENERIC_READ, FILE_ID_INFO, FILE_READ_ATTRIBUTES, FILE_SHARE_READ,
627 FILE_SHARE_WRITE, FILE_TYPE_DISK, FILE_WRITE_DATA, FileAttributeTagInfo,
628 FileDispositionInfo, FileIdInfo, GetFileInformationByHandle,
629 GetFileInformationByHandleEx, GetFileType, OPEN_EXISTING, SetFileInformationByHandle,
630 },
631 System::IO::IO_STATUS_BLOCK,
632 };
633
634 use super::{
635 DirectoryFilesystemIdentity, DirectoryIdentityError, DirectoryIdentityErrorKind,
636 DirectoryIdentityOperation, ExactRegularFileRemoval, RegularFileFilesystemIdentity,
637 };
638
639 pub(super) fn capture(
640 path: &Path,
641 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
642 capture_with_hook(path, || {})
643 }
644
645 pub(super) fn open_retained_directory(
646 path: &Path,
647 ) -> Result<(File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
648 open_retained_directory_with_hook(path, || {})
649 }
650
651 #[cfg(test)]
652 pub(super) fn open_retained_directory_after_open(
653 path: &Path,
654 after_open: impl FnOnce(),
655 ) -> Result<(File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
656 open_retained_directory_with_hook(path, after_open)
657 }
658
659 #[cfg(test)]
660 pub(super) fn open_retained_directory_without_write(
661 path: &Path,
662 ) -> Result<(File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
663 let wide_path = wide_path(path)?;
664 let directory = open_directory_handle_with_access(
665 &wide_path,
666 FILE_GENERIC_READ,
667 DirectoryIdentityOperation::OpenDirectory,
668 )?;
669 let identity = directory_identity_from_file(&directory)?;
670 verify_retained_directory_path_wide(&directory, &identity, &wide_path)?;
671 Ok((directory, identity))
672 }
673
674 fn open_retained_directory_with_hook(
675 path: &Path,
676 after_open: impl FnOnce(),
677 ) -> Result<(File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
678 let wide_path = wide_path(path)?;
679 let directory = open_retained_directory_handle(&wide_path)?;
680 after_open();
681 let identity = directory_identity_from_file(&directory)?;
682 verify_retained_directory_path_wide(&directory, &identity, &wide_path)?;
683 Ok((directory, identity))
684 }
685
686 pub(super) fn verify_retained_directory_path(
687 directory: &File,
688 expected: &DirectoryFilesystemIdentity,
689 path: &Path,
690 ) -> Result<(), DirectoryIdentityError> {
691 let wide_path = wide_path(path)?;
692 verify_retained_directory_path_wide(directory, expected, &wide_path)
693 }
694
695 pub(super) fn sync_retained_directory_metadata(
696 directory: &File,
697 expected: &DirectoryFilesystemIdentity,
698 path: &Path,
699 ) -> Result<(), DirectoryIdentityError> {
700 let wide_path = wide_path(path)?;
701 verify_retained_directory_path_wide(directory, expected, &wide_path)?;
702 let sync_directory = open_directory_handle_with_access(
706 &wide_path,
707 FILE_GENERIC_READ | FILE_WRITE_DATA,
708 DirectoryIdentityOperation::FlushDirectory,
709 )?;
710 if directory_identity_from_file(&sync_directory)? != *expected {
711 return Err(identity_mismatch());
712 }
713
714 flush_directory_handle(&sync_directory)?;
715 if directory_identity_from_file(&sync_directory)? != *expected {
716 return Err(identity_mismatch());
717 }
718 verify_retained_directory_path_wide(directory, expected, &wide_path)
719 }
720
721 fn flush_directory_handle(directory: &File) -> Result<(), DirectoryIdentityError> {
722 let mut io_status = IO_STATUS_BLOCK::default();
723 let status = unsafe {
727 NtFlushBuffersFileEx(
728 directory.as_raw_handle(),
729 0,
730 ptr::null(),
731 0,
732 &raw mut io_status,
733 )
734 };
735 if status != 0 {
736 let os_code = i32::try_from(unsafe { RtlNtStatusToDosError(status) }).ok();
738 return Err(DirectoryIdentityError::new(
739 DirectoryIdentityErrorKind::OperatingSystem(
740 DirectoryIdentityOperation::FlushDirectory,
741 ),
742 os_code,
743 ));
744 }
745 Ok(())
746 }
747
748 #[cfg(test)]
749 pub(super) fn flush_directory_handle_for_test(
750 directory: &File,
751 ) -> Result<(), DirectoryIdentityError> {
752 flush_directory_handle(directory)
753 }
754
755 fn verify_retained_directory_path_wide(
756 directory: &File,
757 expected: &DirectoryFilesystemIdentity,
758 wide_path: &[u16],
759 ) -> Result<(), DirectoryIdentityError> {
760 if directory_identity_from_file(directory)? != *expected {
761 return Err(identity_mismatch());
762 }
763 let rebound = open_identity_handle(wide_path, DirectoryIdentityOperation::OpenDirectory)?;
764 if directory_identity_from_file(&rebound)? != *expected
765 || directory_identity_from_file(directory)? != *expected
766 {
767 return Err(identity_mismatch());
768 }
769 Ok(())
770 }
771
772 #[cfg(test)]
773 pub(super) fn capture_after_open(
774 path: &Path,
775 after_open: impl FnOnce(),
776 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
777 capture_with_hook(path, after_open)
778 }
779
780 fn capture_with_hook(
781 path: &Path,
782 after_open: impl FnOnce(),
783 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
784 let path = wide_path(path)?;
785 let directory = open_identity_handle(&path, DirectoryIdentityOperation::OpenDirectory)?;
786 after_open();
787
788 let attributes = query_attributes(&directory)?;
789 validate_directory_attributes(attributes)?;
790
791 let identity = query_identity(&directory)?;
792 validate_directory_attributes(query_attributes(&directory)?)?;
793 let rebound = open_identity_handle(&path, DirectoryIdentityOperation::OpenDirectory)?;
794 validate_directory_attributes(query_attributes(&rebound)?)?;
795 let rebound_identity = query_identity(&rebound)?;
796 validate_directory_attributes(query_attributes(&rebound)?)?;
797 if !identities_match(&identity, &rebound_identity) {
798 return Err(identity_mismatch());
799 }
800 Ok(DirectoryFilesystemIdentity::from_windows(
801 identity.VolumeSerialNumber,
802 identity.FileId.Identifier,
803 ))
804 }
805
806 pub(super) fn capture_regular_file(
807 path: &Path,
808 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
809 capture_regular_file_with_hook(path, || {})
810 }
811
812 pub(super) fn open_retained_regular_file(
813 path: &Path,
814 ) -> Result<(File, RegularFileFilesystemIdentity), DirectoryIdentityError> {
815 let wide_path = wide_path(path)?;
816 let file = open_retained_regular_file_handle(&wide_path)?;
817 let identity = regular_file_identity_from_file(&file)?;
818 verify_retained_regular_file_path_wide(&file, &identity, &wide_path)?;
819 Ok((file, identity))
820 }
821
822 pub(super) fn verify_retained_regular_file_path(
823 file: &File,
824 expected: &RegularFileFilesystemIdentity,
825 path: &Path,
826 ) -> Result<(), DirectoryIdentityError> {
827 let wide_path = wide_path(path)?;
828 verify_retained_regular_file_path_wide(file, expected, &wide_path)
829 }
830
831 fn verify_retained_regular_file_path_wide(
832 file: &File,
833 expected: &RegularFileFilesystemIdentity,
834 wide_path: &[u16],
835 ) -> Result<(), DirectoryIdentityError> {
836 if regular_file_identity_from_file(file)? != *expected {
837 return Err(identity_mismatch());
838 }
839 let rebound = open_identity_handle(wide_path, DirectoryIdentityOperation::OpenRegularFile)?;
840 if regular_file_identity_from_file(&rebound)? != *expected
841 || regular_file_identity_from_file(file)? != *expected
842 {
843 return Err(identity_mismatch());
844 }
845 Ok(())
846 }
847
848 pub(super) fn open_regular_file_for_exact_removal(
849 path: &Path,
850 ) -> Result<ExactRegularFileRemoval, DirectoryIdentityError> {
851 let path = wide_path(path)?;
852 let file = open_exact_removal_handle(&path)?;
853 let identity = regular_file_identity_from_file(&file)?;
854 Ok(ExactRegularFileRemoval { file, identity })
855 }
856
857 pub(super) fn remove_exact_regular_file(
858 removal: ExactRegularFileRemoval,
859 ) -> Result<(), DirectoryIdentityError> {
860 let ExactRegularFileRemoval { file, identity } = removal;
861 let actual = regular_file_identity_from_file(&file)?;
862 if actual != identity {
863 return Err(identity_mismatch());
864 }
865 validate_regular_file_information(&file, &query_regular_file_information(&file)?)?;
866
867 let disposition = FILE_DISPOSITION_INFO { DeleteFile: true };
868 let size =
869 structure_size::<FILE_DISPOSITION_INFO>(DirectoryIdentityOperation::RemoveRegularFile)?;
870 let succeeded = unsafe {
873 SetFileInformationByHandle(
874 file.as_raw_handle(),
875 FileDispositionInfo,
876 (&raw const disposition).cast(),
877 size,
878 )
879 };
880 if succeeded == 0 {
881 return Err(last_error(DirectoryIdentityErrorKind::OperatingSystem(
882 DirectoryIdentityOperation::RemoveRegularFile,
883 )));
884 }
885 Ok(())
886 }
887
888 #[cfg(test)]
889 pub(super) fn capture_regular_file_after_open(
890 path: &Path,
891 after_open: impl FnOnce(),
892 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
893 capture_regular_file_with_hook(path, after_open)
894 }
895
896 fn capture_regular_file_with_hook(
897 path: &Path,
898 after_open: impl FnOnce(),
899 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
900 let path = wide_path(path)?;
901 let file = open_identity_handle(&path, DirectoryIdentityOperation::OpenRegularFile)?;
902 after_open();
903
904 let information = query_regular_file_information(&file)?;
905 validate_regular_file_information(&file, &information)?;
906
907 let identity = query_identity(&file)?;
908 validate_regular_file_information(&file, &query_regular_file_information(&file)?)?;
909 let rebound = open_identity_handle(&path, DirectoryIdentityOperation::OpenRegularFile)?;
910 validate_regular_file_information(&rebound, &query_regular_file_information(&rebound)?)?;
911 let rebound_identity = query_identity(&rebound)?;
912 validate_regular_file_information(&rebound, &query_regular_file_information(&rebound)?)?;
913 if !identities_match(&identity, &rebound_identity) {
914 return Err(identity_mismatch());
915 }
916 validate_regular_file_information(&file, &query_regular_file_information(&file)?)?;
917 Ok(RegularFileFilesystemIdentity::from_windows(
918 identity.VolumeSerialNumber,
919 identity.FileId.Identifier,
920 ))
921 }
922
923 fn open_identity_handle(
924 path: &[u16],
925 operation: DirectoryIdentityOperation,
926 ) -> Result<File, DirectoryIdentityError> {
927 open_directory_handle_with_access(path, FILE_READ_ATTRIBUTES, operation)
928 }
929
930 fn open_retained_directory_handle(path: &[u16]) -> Result<File, DirectoryIdentityError> {
931 open_directory_handle_with_access(
932 path,
933 FILE_GENERIC_READ,
934 DirectoryIdentityOperation::OpenDirectory,
935 )
936 }
937
938 fn open_directory_handle_with_access(
939 path: &[u16],
940 desired_access: u32,
941 operation: DirectoryIdentityOperation,
942 ) -> Result<File, DirectoryIdentityError> {
943 let handle = unsafe {
948 CreateFileW(
949 path.as_ptr(),
950 desired_access,
951 FILE_SHARE_READ | FILE_SHARE_WRITE,
952 ptr::null(),
953 OPEN_EXISTING,
954 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
955 ptr::null_mut(),
956 )
957 };
958 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
959 return Err(last_error(DirectoryIdentityErrorKind::OperatingSystem(
960 operation,
961 )));
962 }
963 Ok(unsafe { File::from_raw_handle(handle) })
965 }
966
967 pub(super) fn directory_identity_from_file(
968 directory: &File,
969 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
970 validate_directory_attributes(query_attributes(directory)?)?;
971 let identity = query_identity(directory)?;
972 validate_directory_attributes(query_attributes(directory)?)?;
973 Ok(DirectoryFilesystemIdentity::from_windows(
974 identity.VolumeSerialNumber,
975 identity.FileId.Identifier,
976 ))
977 }
978
979 fn open_exact_removal_handle(path: &[u16]) -> Result<File, DirectoryIdentityError> {
980 let handle = unsafe {
984 CreateFileW(
985 path.as_ptr(),
986 FILE_GENERIC_READ | DELETE,
987 FILE_SHARE_READ,
988 ptr::null(),
989 OPEN_EXISTING,
990 FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT,
991 ptr::null_mut(),
992 )
993 };
994 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
995 return Err(last_error(DirectoryIdentityErrorKind::OperatingSystem(
996 DirectoryIdentityOperation::OpenRegularFile,
997 )));
998 }
999 Ok(unsafe { File::from_raw_handle(handle) })
1001 }
1002
1003 fn open_retained_regular_file_handle(path: &[u16]) -> Result<File, DirectoryIdentityError> {
1004 let handle = unsafe {
1008 CreateFileW(
1009 path.as_ptr(),
1010 FILE_GENERIC_READ,
1011 FILE_SHARE_READ,
1012 ptr::null(),
1013 OPEN_EXISTING,
1014 FILE_FLAG_OPEN_REPARSE_POINT,
1015 ptr::null_mut(),
1016 )
1017 };
1018 if handle == INVALID_HANDLE_VALUE || handle.is_null() {
1019 return Err(last_error(DirectoryIdentityErrorKind::OperatingSystem(
1020 DirectoryIdentityOperation::OpenRegularFile,
1021 )));
1022 }
1023 Ok(unsafe { File::from_raw_handle(handle) })
1025 }
1026
1027 pub(super) fn regular_file_identity_from_file(
1028 file: &File,
1029 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1030 validate_regular_file_information(file, &query_regular_file_information(file)?)?;
1031 let identity = query_identity(file)?;
1032 validate_regular_file_information(file, &query_regular_file_information(file)?)?;
1033 Ok(RegularFileFilesystemIdentity::from_windows(
1034 identity.VolumeSerialNumber,
1035 identity.FileId.Identifier,
1036 ))
1037 }
1038
1039 fn query_attributes(file: &File) -> Result<FILE_ATTRIBUTE_TAG_INFO, DirectoryIdentityError> {
1040 let mut attributes = FILE_ATTRIBUTE_TAG_INFO::default();
1041 let size =
1042 structure_size::<FILE_ATTRIBUTE_TAG_INFO>(DirectoryIdentityOperation::QueryAttributes)?;
1043 let succeeded = unsafe {
1046 GetFileInformationByHandleEx(
1047 file.as_raw_handle(),
1048 FileAttributeTagInfo,
1049 (&raw mut attributes).cast(),
1050 size,
1051 )
1052 };
1053 if succeeded == 0 {
1054 return Err(last_error(DirectoryIdentityErrorKind::OperatingSystem(
1055 DirectoryIdentityOperation::QueryAttributes,
1056 )));
1057 }
1058 Ok(attributes)
1059 }
1060
1061 fn validate_directory_attributes(
1062 attributes: FILE_ATTRIBUTE_TAG_INFO,
1063 ) -> Result<(), DirectoryIdentityError> {
1064 if attributes.FileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1065 return Err(DirectoryIdentityError::new(
1066 DirectoryIdentityErrorKind::ReparsePoint,
1067 None,
1068 ));
1069 }
1070 if attributes.FileAttributes & FILE_ATTRIBUTE_DIRECTORY == 0 {
1071 return Err(DirectoryIdentityError::new(
1072 DirectoryIdentityErrorKind::NotDirectory,
1073 None,
1074 ));
1075 }
1076 Ok(())
1077 }
1078
1079 fn query_identity(file: &File) -> Result<FILE_ID_INFO, DirectoryIdentityError> {
1080 let mut identity = FILE_ID_INFO::default();
1081 let size = u32::try_from(size_of::<FILE_ID_INFO>()).map_err(|_| {
1082 DirectoryIdentityError::new(DirectoryIdentityErrorKind::IdentityUnavailable, None)
1083 })?;
1084 let succeeded = unsafe {
1087 GetFileInformationByHandleEx(
1088 file.as_raw_handle(),
1089 FileIdInfo,
1090 (&raw mut identity).cast(),
1091 size,
1092 )
1093 };
1094 if succeeded == 0 {
1095 return Err(last_error(DirectoryIdentityErrorKind::IdentityUnavailable));
1096 }
1097 Ok(identity)
1098 }
1099
1100 fn query_regular_file_information(
1101 file: &File,
1102 ) -> Result<BY_HANDLE_FILE_INFORMATION, DirectoryIdentityError> {
1103 let mut information = BY_HANDLE_FILE_INFORMATION::default();
1104 let succeeded =
1107 unsafe { GetFileInformationByHandle(file.as_raw_handle(), &raw mut information) };
1108 if succeeded == 0 {
1109 return Err(last_error(DirectoryIdentityErrorKind::OperatingSystem(
1110 DirectoryIdentityOperation::QueryLinkCount,
1111 )));
1112 }
1113 Ok(information)
1114 }
1115
1116 fn validate_regular_file_information(
1117 file: &File,
1118 information: &BY_HANDLE_FILE_INFORMATION,
1119 ) -> Result<(), DirectoryIdentityError> {
1120 if information.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT != 0 {
1121 return Err(DirectoryIdentityError::new(
1122 DirectoryIdentityErrorKind::ReparsePoint,
1123 None,
1124 ));
1125 }
1126 if information.dwFileAttributes & (FILE_ATTRIBUTE_DIRECTORY | FILE_ATTRIBUTE_DEVICE) != 0
1128 || unsafe { GetFileType(file.as_raw_handle()) } != FILE_TYPE_DISK
1129 {
1130 return Err(DirectoryIdentityError::new(
1131 DirectoryIdentityErrorKind::NotRegularFile,
1132 None,
1133 ));
1134 }
1135 if information.nNumberOfLinks != 1 {
1136 return Err(DirectoryIdentityError::new(
1137 DirectoryIdentityErrorKind::MultipleLinks,
1138 None,
1139 ));
1140 }
1141 Ok(())
1142 }
1143
1144 fn identities_match(first: &FILE_ID_INFO, second: &FILE_ID_INFO) -> bool {
1145 first.VolumeSerialNumber == second.VolumeSerialNumber
1146 && first.FileId.Identifier == second.FileId.Identifier
1147 }
1148
1149 fn identity_mismatch() -> DirectoryIdentityError {
1150 DirectoryIdentityError::new(DirectoryIdentityErrorKind::IdentityMismatch, None)
1151 }
1152
1153 fn structure_size<T>(
1154 operation: DirectoryIdentityOperation,
1155 ) -> Result<u32, DirectoryIdentityError> {
1156 u32::try_from(size_of::<T>()).map_err(|_| {
1157 DirectoryIdentityError::new(
1158 DirectoryIdentityErrorKind::OperatingSystem(operation),
1159 None,
1160 )
1161 })
1162 }
1163
1164 fn wide_path(path: &Path) -> Result<Vec<u16>, DirectoryIdentityError> {
1165 const BACKSLASH: u16 = b'\\' as u16;
1166 const FORWARD_SLASH: u16 = b'/' as u16;
1167 const VERBATIM_PREFIX: &[u16] = &[BACKSLASH, BACKSLASH, b'?' as u16, BACKSLASH];
1168 const VERBATIM_UNC_PREFIX: &[u16] = &[
1169 BACKSLASH,
1170 BACKSLASH,
1171 b'?' as u16,
1172 BACKSLASH,
1173 b'U' as u16,
1174 b'N' as u16,
1175 b'C' as u16,
1176 BACKSLASH,
1177 ];
1178
1179 let mut encoded = path.as_os_str().encode_wide().collect::<Vec<_>>();
1180 if encoded.is_empty() || !path.is_absolute() || encoded.contains(&0) {
1181 return Err(DirectoryIdentityError::new(
1182 DirectoryIdentityErrorKind::InvalidPath,
1183 None,
1184 ));
1185 }
1186 if encoded.starts_with(VERBATIM_PREFIX) {
1187 encoded.push(0);
1188 return Ok(encoded);
1189 }
1190 for unit in &mut encoded {
1191 if *unit == FORWARD_SLASH {
1192 *unit = BACKSLASH;
1193 }
1194 }
1195 let mut wide = if encoded.starts_with(&[BACKSLASH, BACKSLASH]) {
1196 let mut wide = Vec::with_capacity(VERBATIM_UNC_PREFIX.len() + encoded.len() - 2 + 1);
1197 wide.extend_from_slice(VERBATIM_UNC_PREFIX);
1198 wide.extend_from_slice(&encoded[2..]);
1199 wide
1200 } else {
1201 let mut wide = Vec::with_capacity(VERBATIM_PREFIX.len() + encoded.len() + 1);
1202 wide.extend_from_slice(VERBATIM_PREFIX);
1203 wide.extend_from_slice(&encoded);
1204 wide
1205 };
1206 wide.push(0);
1207 Ok(wide)
1208 }
1209
1210 fn last_error(kind: DirectoryIdentityErrorKind) -> DirectoryIdentityError {
1211 DirectoryIdentityError::new(kind, std::io::Error::last_os_error().raw_os_error())
1212 }
1213
1214 use std::os::windows::io::AsRawHandle as _;
1215}
1216
1217#[cfg(unix)]
1218mod platform {
1219 use std::{
1220 fs::OpenOptions,
1221 os::unix::fs::{MetadataExt as _, OpenOptionsExt as _},
1222 path::Path,
1223 };
1224
1225 #[cfg(target_vendor = "apple")]
1226 use std::{
1227 ffi::OsStr,
1228 os::{fd::AsRawFd as _, unix::ffi::OsStrExt as _},
1229 };
1230
1231 use super::{
1232 DirectoryFilesystemIdentity, DirectoryIdentityError, DirectoryIdentityErrorKind,
1233 DirectoryIdentityOperation, RegularFileFilesystemIdentity,
1234 };
1235
1236 pub(super) fn capture(
1237 path: &Path,
1238 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
1239 capture_with_hook(path, || {})
1240 }
1241
1242 pub(super) fn open_retained_directory(
1243 path: &Path,
1244 ) -> Result<(std::fs::File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
1245 open_retained_directory_with_hook(path, || {})
1246 }
1247
1248 #[cfg(test)]
1249 pub(super) fn open_retained_directory_after_open(
1250 path: &Path,
1251 after_open: impl FnOnce(),
1252 ) -> Result<(std::fs::File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
1253 open_retained_directory_with_hook(path, after_open)
1254 }
1255
1256 fn open_retained_directory_with_hook(
1257 path: &Path,
1258 after_open: impl FnOnce(),
1259 ) -> Result<(std::fs::File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
1260 let directory = open_directory(path)?;
1261 after_open();
1262 let identity = directory_identity_from_file(&directory)?;
1263 verify_retained_directory_path(&directory, &identity, path)?;
1264 Ok((directory, identity))
1265 }
1266
1267 pub(super) fn verify_retained_directory_path(
1268 directory: &std::fs::File,
1269 expected: &DirectoryFilesystemIdentity,
1270 path: &Path,
1271 ) -> Result<(), DirectoryIdentityError> {
1272 if directory_identity_from_file(directory)? != *expected {
1273 return Err(identity_mismatch());
1274 }
1275 let rebound = open_directory(path)?;
1276 if directory_identity_from_file(&rebound)? != *expected
1277 || directory_identity_from_file(directory)? != *expected
1278 {
1279 return Err(identity_mismatch());
1280 }
1281 Ok(())
1282 }
1283
1284 #[cfg_attr(target_vendor = "apple", allow(unsafe_code))]
1285 pub(super) fn retained_directory_is_unlinked(
1286 directory: &std::fs::File,
1287 ) -> std::io::Result<bool> {
1288 #[cfg(not(target_vendor = "apple"))]
1289 return directory.metadata().map(|metadata| metadata.nlink() == 0);
1290
1291 #[cfg(target_vendor = "apple")]
1292 {
1293 let mut path = [0_i8; libc::PATH_MAX as usize];
1294 let status =
1296 unsafe { libc::fcntl(directory.as_raw_fd(), libc::F_GETPATH, path.as_mut_ptr()) };
1297 if status != 0 {
1298 let error = std::io::Error::last_os_error();
1299 return if error.raw_os_error() == Some(libc::ENOENT) {
1300 Ok(true)
1301 } else {
1302 Err(error)
1303 };
1304 }
1305 let terminator = path.iter().position(|byte| *byte == 0).ok_or_else(|| {
1306 std::io::Error::new(std::io::ErrorKind::InvalidData, "invalid retained path")
1307 })?;
1308 let path = path[..terminator]
1309 .iter()
1310 .map(|byte| (*byte).cast_unsigned())
1311 .collect::<Vec<_>>();
1312 let rebound = match std::fs::File::open(Path::new(OsStr::from_bytes(&path))) {
1313 Ok(rebound) => rebound,
1314 Err(error) if error.kind() == std::io::ErrorKind::NotFound => return Ok(true),
1315 Err(error) => return Err(error),
1316 };
1317 let retained = directory.metadata()?;
1318 let rebound = rebound.metadata()?;
1319 Ok(retained.dev() != rebound.dev() || retained.ino() != rebound.ino())
1320 }
1321 }
1322
1323 #[cfg(test)]
1324 pub(super) fn capture_after_open(
1325 path: &Path,
1326 after_open: impl FnOnce(),
1327 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
1328 capture_with_hook(path, after_open)
1329 }
1330
1331 fn capture_with_hook(
1332 path: &Path,
1333 after_open: impl FnOnce(),
1334 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
1335 let directory = open_directory(path)?;
1336 after_open();
1337 let identity = query_directory_identity(&directory)?;
1338 let reopened = open_directory(path)?;
1339 if query_directory_identity(&reopened)? != identity {
1340 return Err(identity_mismatch());
1341 }
1342 Ok(DirectoryFilesystemIdentity::from_unix(
1343 identity.0, identity.1,
1344 ))
1345 }
1346
1347 fn open_directory(path: &Path) -> Result<std::fs::File, DirectoryIdentityError> {
1348 reject_final_symlink(path)?;
1349 OpenOptions::new()
1350 .read(true)
1351 .custom_flags(libc::O_DIRECTORY | libc::O_NOFOLLOW | libc::O_CLOEXEC)
1352 .open(path)
1353 .map_err(|error| open_error(path, &error))
1354 }
1355
1356 fn query_directory_identity(
1357 directory: &std::fs::File,
1358 ) -> Result<(u64, u64), DirectoryIdentityError> {
1359 let metadata = directory.metadata().map_err(|error| {
1360 DirectoryIdentityError::new(
1361 DirectoryIdentityErrorKind::OperatingSystem(
1362 DirectoryIdentityOperation::QueryMetadata,
1363 ),
1364 error.raw_os_error(),
1365 )
1366 })?;
1367 if !metadata.is_dir() {
1368 return Err(DirectoryIdentityError::new(
1369 DirectoryIdentityErrorKind::NotDirectory,
1370 None,
1371 ));
1372 }
1373 Ok((metadata.dev(), metadata.ino()))
1374 }
1375
1376 pub(super) fn directory_identity_from_file(
1377 directory: &std::fs::File,
1378 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
1379 let identity = query_directory_identity(directory)?;
1380 Ok(DirectoryFilesystemIdentity::from_unix(
1381 identity.0, identity.1,
1382 ))
1383 }
1384
1385 pub(super) fn capture_regular_file(
1386 path: &Path,
1387 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1388 capture_regular_file_with_hook(path, || {})
1389 }
1390
1391 pub(super) fn open_retained_regular_file(
1392 path: &Path,
1393 ) -> Result<(std::fs::File, RegularFileFilesystemIdentity), DirectoryIdentityError> {
1394 let file = open_regular_file(path)?;
1395 let identity = regular_file_identity_from_file(&file)?;
1396 verify_retained_regular_file_path(&file, &identity, path)?;
1397 Ok((file, identity))
1398 }
1399
1400 pub(super) fn verify_retained_regular_file_path(
1401 file: &std::fs::File,
1402 expected: &RegularFileFilesystemIdentity,
1403 path: &Path,
1404 ) -> Result<(), DirectoryIdentityError> {
1405 if regular_file_identity_from_file(file)? != *expected {
1406 return Err(identity_mismatch());
1407 }
1408 let rebound = open_regular_file(path)?;
1409 if regular_file_identity_from_file(&rebound)? != *expected
1410 || regular_file_identity_from_file(file)? != *expected
1411 {
1412 return Err(identity_mismatch());
1413 }
1414 Ok(())
1415 }
1416
1417 pub(super) fn regular_file_identity_from_file(
1418 file: &std::fs::File,
1419 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1420 let identity = query_regular_file_identity(file)?;
1421 if query_regular_file_identity(file)? != identity {
1422 return Err(identity_mismatch());
1423 }
1424 Ok(RegularFileFilesystemIdentity::from_unix(
1425 identity.0, identity.1,
1426 ))
1427 }
1428
1429 #[cfg(test)]
1430 pub(super) fn capture_regular_file_after_open(
1431 path: &Path,
1432 after_open: impl FnOnce(),
1433 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1434 capture_regular_file_with_hook(path, after_open)
1435 }
1436
1437 fn capture_regular_file_with_hook(
1438 path: &Path,
1439 after_open: impl FnOnce(),
1440 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1441 let file = open_regular_file(path)?;
1442 after_open();
1443 let identity = query_regular_file_identity(&file)?;
1444 let reopened = open_regular_file(path)?;
1445 if query_regular_file_identity(&reopened)? != identity
1446 || query_regular_file_identity(&file)? != identity
1447 {
1448 return Err(identity_mismatch());
1449 }
1450 Ok(RegularFileFilesystemIdentity::from_unix(
1451 identity.0, identity.1,
1452 ))
1453 }
1454
1455 fn open_regular_file(path: &Path) -> Result<std::fs::File, DirectoryIdentityError> {
1456 reject_final_symlink(path)?;
1457 OpenOptions::new()
1458 .read(true)
1459 .custom_flags(libc::O_NOFOLLOW | libc::O_CLOEXEC | libc::O_NONBLOCK)
1460 .open(path)
1461 .map_err(|error| open_regular_file_error(path, &error))
1462 }
1463
1464 fn query_regular_file_identity(
1465 file: &std::fs::File,
1466 ) -> Result<(u64, u64), DirectoryIdentityError> {
1467 let metadata = file.metadata().map_err(|error| {
1468 DirectoryIdentityError::new(
1469 DirectoryIdentityErrorKind::OperatingSystem(
1470 DirectoryIdentityOperation::QueryMetadata,
1471 ),
1472 error.raw_os_error(),
1473 )
1474 })?;
1475 if !metadata.is_file() {
1476 return Err(DirectoryIdentityError::new(
1477 DirectoryIdentityErrorKind::NotRegularFile,
1478 None,
1479 ));
1480 }
1481 if metadata.nlink() != 1 {
1482 return Err(DirectoryIdentityError::new(
1483 DirectoryIdentityErrorKind::MultipleLinks,
1484 None,
1485 ));
1486 }
1487 Ok((metadata.dev(), metadata.ino()))
1488 }
1489
1490 fn reject_final_symlink(path: &Path) -> Result<(), DirectoryIdentityError> {
1491 if std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink()) {
1492 return Err(DirectoryIdentityError::new(
1493 DirectoryIdentityErrorKind::ReparsePoint,
1494 None,
1495 ));
1496 }
1497 Ok(())
1498 }
1499
1500 fn open_error(path: &Path, error: &std::io::Error) -> DirectoryIdentityError {
1501 let os_code = error.raw_os_error();
1502 let kind = match (final_path_is_symlink(path), os_code) {
1503 (true, _) | (_, Some(libc::ELOOP)) => DirectoryIdentityErrorKind::ReparsePoint,
1504 (_, Some(libc::ENOTDIR)) => DirectoryIdentityErrorKind::NotDirectory,
1505 _ => DirectoryIdentityErrorKind::OperatingSystem(
1506 DirectoryIdentityOperation::OpenDirectory,
1507 ),
1508 };
1509 DirectoryIdentityError::new(kind, os_code)
1510 }
1511
1512 fn open_regular_file_error(path: &Path, error: &std::io::Error) -> DirectoryIdentityError {
1513 let os_code = error.raw_os_error();
1514 let kind = if final_path_is_symlink(path) || os_code == Some(libc::ELOOP) {
1515 DirectoryIdentityErrorKind::ReparsePoint
1516 } else {
1517 DirectoryIdentityErrorKind::OperatingSystem(DirectoryIdentityOperation::OpenRegularFile)
1518 };
1519 DirectoryIdentityError::new(kind, os_code)
1520 }
1521
1522 fn final_path_is_symlink(path: &Path) -> bool {
1523 std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink())
1524 }
1525
1526 fn identity_mismatch() -> DirectoryIdentityError {
1527 DirectoryIdentityError::new(DirectoryIdentityErrorKind::IdentityMismatch, None)
1528 }
1529}
1530
1531#[cfg(not(any(unix, windows)))]
1532mod platform {
1533 use std::path::Path;
1534
1535 use super::{
1536 DirectoryFilesystemIdentity, DirectoryIdentityError, DirectoryIdentityErrorKind,
1537 RegularFileFilesystemIdentity,
1538 };
1539
1540 pub(super) fn capture(
1541 _path: &Path,
1542 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
1543 Err(DirectoryIdentityError::new(
1544 DirectoryIdentityErrorKind::UnsupportedPlatform,
1545 None,
1546 ))
1547 }
1548
1549 pub(super) fn open_retained_directory(
1550 _path: &Path,
1551 ) -> Result<(std::fs::File, DirectoryFilesystemIdentity), DirectoryIdentityError> {
1552 Err(DirectoryIdentityError::new(
1553 DirectoryIdentityErrorKind::UnsupportedPlatform,
1554 None,
1555 ))
1556 }
1557
1558 pub(super) fn verify_retained_directory_path(
1559 _directory: &std::fs::File,
1560 _expected: &DirectoryFilesystemIdentity,
1561 _path: &Path,
1562 ) -> Result<(), DirectoryIdentityError> {
1563 Err(DirectoryIdentityError::new(
1564 DirectoryIdentityErrorKind::UnsupportedPlatform,
1565 None,
1566 ))
1567 }
1568
1569 pub(super) fn capture_regular_file(
1570 _path: &Path,
1571 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1572 Err(DirectoryIdentityError::new(
1573 DirectoryIdentityErrorKind::UnsupportedPlatform,
1574 None,
1575 ))
1576 }
1577
1578 pub(super) fn open_retained_regular_file(
1579 _path: &Path,
1580 ) -> Result<(std::fs::File, RegularFileFilesystemIdentity), DirectoryIdentityError> {
1581 Err(DirectoryIdentityError::new(
1582 DirectoryIdentityErrorKind::UnsupportedPlatform,
1583 None,
1584 ))
1585 }
1586
1587 pub(super) fn verify_retained_regular_file_path(
1588 _file: &std::fs::File,
1589 _expected: &RegularFileFilesystemIdentity,
1590 _path: &Path,
1591 ) -> Result<(), DirectoryIdentityError> {
1592 Err(DirectoryIdentityError::new(
1593 DirectoryIdentityErrorKind::UnsupportedPlatform,
1594 None,
1595 ))
1596 }
1597
1598 pub(super) fn regular_file_identity_from_file(
1599 _file: &std::fs::File,
1600 ) -> Result<RegularFileFilesystemIdentity, DirectoryIdentityError> {
1601 Err(DirectoryIdentityError::new(
1602 DirectoryIdentityErrorKind::UnsupportedPlatform,
1603 None,
1604 ))
1605 }
1606
1607 pub(super) fn directory_identity_from_file(
1608 _file: &std::fs::File,
1609 ) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
1610 Err(DirectoryIdentityError::new(
1611 DirectoryIdentityErrorKind::UnsupportedPlatform,
1612 None,
1613 ))
1614 }
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620
1621 #[test]
1622 fn identity_is_stable_across_reopen() {
1623 let temporary = tempfile::tempdir().unwrap();
1624 let first = DirectoryFilesystemIdentity::capture(temporary.path()).unwrap();
1625 let second = DirectoryFilesystemIdentity::capture(temporary.path()).unwrap();
1626
1627 assert_eq!(first, second);
1628 assert!(first.as_str().is_ascii());
1629 assert!(
1630 !first
1631 .as_str()
1632 .contains(&temporary.path().to_string_lossy()[..])
1633 );
1634 let restored = first
1635 .to_string()
1636 .parse::<DirectoryFilesystemIdentity>()
1637 .unwrap();
1638 assert_eq!(restored, first);
1639 verify_directory_identity(temporary.path(), &restored).unwrap();
1640 }
1641
1642 #[test]
1643 fn retained_directory_identity_matches_same_handle_and_safe_clone() {
1644 let temporary = tempfile::tempdir().unwrap();
1645 let retained = RetainedDirectoryIdentity::open(temporary.path()).unwrap();
1646
1647 assert_eq!(
1648 retained.identity(),
1649 &DirectoryFilesystemIdentity::capture(temporary.path()).unwrap()
1650 );
1651 assert!(retained.as_file().metadata().unwrap().is_dir());
1652 assert!(
1653 retained
1654 .try_clone_file()
1655 .unwrap()
1656 .metadata()
1657 .unwrap()
1658 .is_dir()
1659 );
1660 retained.verify_path(temporary.path()).unwrap();
1661 assert!(!format!("{retained:?}").contains(&temporary.path().to_string_lossy()[..]));
1662 }
1663
1664 #[test]
1665 fn directory_identity_can_be_derived_from_a_retained_handle() {
1666 let temporary = tempfile::tempdir().unwrap();
1667 let retained = RetainedDirectoryIdentity::open(temporary.path()).unwrap();
1668
1669 assert_eq!(
1670 directory_identity_from_file(retained.as_file()).unwrap(),
1671 *retained.identity()
1672 );
1673 }
1674
1675 #[test]
1676 fn directory_identity_from_file_rejects_a_regular_file() {
1677 let file = tempfile::NamedTempFile::new().unwrap();
1678 assert_eq!(
1679 directory_identity_from_file(file.as_file())
1680 .unwrap_err()
1681 .kind(),
1682 DirectoryIdentityErrorKind::NotDirectory
1683 );
1684 }
1685
1686 #[test]
1687 fn retained_directory_rejects_relative_and_regular_file_paths() {
1688 assert_eq!(
1689 RetainedDirectoryIdentity::open(Path::new("relative"))
1690 .unwrap_err()
1691 .kind(),
1692 DirectoryIdentityErrorKind::InvalidPath
1693 );
1694
1695 let file = tempfile::NamedTempFile::new().unwrap();
1696 assert_eq!(
1697 RetainedDirectoryIdentity::open(file.path())
1698 .unwrap_err()
1699 .kind(),
1700 DirectoryIdentityErrorKind::NotDirectory
1701 );
1702
1703 let missing = file.path().with_extension("missing");
1704 assert_eq!(
1705 RetainedDirectoryIdentity::open(&missing)
1706 .unwrap_err()
1707 .kind(),
1708 DirectoryIdentityErrorKind::OperatingSystem(DirectoryIdentityOperation::OpenDirectory)
1709 );
1710 }
1711
1712 #[test]
1713 fn retained_directory_verify_path_rejects_another_directory() {
1714 let temporary = tempfile::tempdir().unwrap();
1715 let retained_path = temporary.path().join("retained");
1716 let other_path = temporary.path().join("other");
1717 std::fs::create_dir(&retained_path).unwrap();
1718 std::fs::create_dir(&other_path).unwrap();
1719 let retained = RetainedDirectoryIdentity::open(&retained_path).unwrap();
1720
1721 assert_eq!(
1722 retained.verify_path(&other_path).unwrap_err().kind(),
1723 DirectoryIdentityErrorKind::IdentityMismatch
1724 );
1725 assert_eq!(
1726 retained
1727 .verify_path(Path::new("relative"))
1728 .unwrap_err()
1729 .kind(),
1730 DirectoryIdentityErrorKind::InvalidPath
1731 );
1732 }
1733
1734 #[test]
1735 fn replacement_directory_has_a_different_identity() {
1736 let temporary = tempfile::tempdir().unwrap();
1737 let project = temporary.path().join("project");
1738 let displaced = temporary.path().join("displaced");
1739 std::fs::create_dir(&project).unwrap();
1740 let original = DirectoryFilesystemIdentity::capture(&project).unwrap();
1741
1742 std::fs::rename(&project, &displaced).unwrap();
1743 std::fs::create_dir(&project).unwrap();
1744 let replacement = DirectoryFilesystemIdentity::capture(&project).unwrap();
1745
1746 assert_ne!(original, replacement);
1747 assert_eq!(
1748 verify_directory_identity(&project, &original)
1749 .unwrap_err()
1750 .kind(),
1751 DirectoryIdentityErrorKind::IdentityMismatch
1752 );
1753 }
1754
1755 #[test]
1756 fn relative_and_regular_file_paths_are_rejected() {
1757 assert_eq!(
1758 DirectoryFilesystemIdentity::capture(Path::new("relative"))
1759 .unwrap_err()
1760 .kind(),
1761 DirectoryIdentityErrorKind::InvalidPath
1762 );
1763
1764 let file = tempfile::NamedTempFile::new().unwrap();
1765 assert_eq!(
1766 DirectoryFilesystemIdentity::capture(file.path())
1767 .unwrap_err()
1768 .kind(),
1769 DirectoryIdentityErrorKind::NotDirectory
1770 );
1771 }
1772
1773 #[test]
1774 fn persisted_format_is_strict_and_canonical() {
1775 let valid_windows =
1776 "rustferry-directory-v1:windows:0000000000000001:00112233445566778899aabbccddeeff";
1777 let valid_unix = "rustferry-directory-v1:unix:0000000000000001:0000000000000002";
1778 assert_eq!(
1779 valid_windows
1780 .parse::<DirectoryFilesystemIdentity>()
1781 .unwrap()
1782 .as_str(),
1783 valid_windows
1784 );
1785 assert!(valid_unix.parse::<DirectoryFilesystemIdentity>().is_ok());
1786
1787 for invalid in [
1788 "",
1789 "rustferry-directory-v2:unix:0000000000000001:0000000000000002",
1790 "rustferry-directory-v1:other:0000000000000001:0000000000000002",
1791 "rustferry-directory-v1:unix:1:2",
1792 "rustferry-directory-v1:unix:000000000000000G:0000000000000002",
1793 "rustferry-directory-v1:windows:0000000000000001:00112233445566778899AABBCCDDEEFF",
1794 "rustferry-directory-v1:unix:0000000000000001:0000000000000002:extra",
1795 ] {
1796 let error = invalid.parse::<DirectoryFilesystemIdentity>().unwrap_err();
1797 assert_eq!(error.kind(), DirectoryIdentityErrorKind::InvalidEncoding);
1798 }
1799
1800 let regular_file =
1801 "rustferry-regular-file-v1:windows:0000000000000001:00112233445566778899aabbccddeeff";
1802 assert!(
1803 regular_file
1804 .parse::<RegularFileFilesystemIdentity>()
1805 .is_ok()
1806 );
1807 assert!(regular_file.parse::<DirectoryFilesystemIdentity>().is_err());
1808 assert!(
1809 valid_windows
1810 .parse::<RegularFileFilesystemIdentity>()
1811 .is_err()
1812 );
1813 assert!(
1814 "rustferry-regular-file-v1:unix:0000000000000001:000000000000000G"
1815 .parse::<RegularFileFilesystemIdentity>()
1816 .is_err()
1817 );
1818 }
1819
1820 #[test]
1821 fn regular_file_identity_is_stable_across_reopen() {
1822 let temporary = tempfile::tempdir().unwrap();
1823 let artifact = temporary.path().join("artifact.bin");
1824 std::fs::write(&artifact, b"artifact").unwrap();
1825
1826 let first = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
1827 let second = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
1828 let restored = first
1829 .to_string()
1830 .parse::<RegularFileFilesystemIdentity>()
1831 .unwrap();
1832
1833 assert_eq!(first, second);
1834 assert_eq!(first, restored);
1835 assert!(first.as_str().is_ascii());
1836 assert!(
1837 !first
1838 .as_str()
1839 .contains(&temporary.path().to_string_lossy()[..])
1840 );
1841 verify_regular_file_identity(&artifact, &restored).unwrap();
1842 }
1843
1844 #[test]
1845 fn retained_file_identity_matches_the_path_capture() {
1846 let temporary = tempfile::tempdir().unwrap();
1847 let artifact = temporary.path().join("artifact.bin");
1848 std::fs::write(&artifact, b"artifact").unwrap();
1849 let retained = std::fs::File::open(&artifact).unwrap();
1850
1851 assert_eq!(
1852 regular_file_identity_from_file(&retained).unwrap(),
1853 RegularFileFilesystemIdentity::capture(&artifact).unwrap()
1854 );
1855 }
1856
1857 #[test]
1858 fn retained_file_identity_does_not_reopen_a_replacement_path() {
1859 let temporary = tempfile::tempdir().unwrap();
1860 let artifact = temporary.path().join("artifact.bin");
1861 let displaced = temporary.path().join("displaced.bin");
1862 std::fs::write(&artifact, b"original").unwrap();
1863 let retained = std::fs::File::open(&artifact).unwrap();
1864 let original = regular_file_identity_from_file(&retained).unwrap();
1865
1866 std::fs::rename(&artifact, &displaced).unwrap();
1867 std::fs::write(&artifact, b"replacement").unwrap();
1868
1869 assert_eq!(
1870 regular_file_identity_from_file(&retained).unwrap(),
1871 original
1872 );
1873 assert_ne!(
1874 RegularFileFilesystemIdentity::capture(&artifact).unwrap(),
1875 original
1876 );
1877 }
1878
1879 #[test]
1880 fn retained_file_identity_rejects_a_hardlink_added_after_open() {
1881 let temporary = tempfile::tempdir().unwrap();
1882 let artifact = temporary.path().join("artifact.bin");
1883 let linked = temporary.path().join("linked.bin");
1884 std::fs::write(&artifact, b"artifact").unwrap();
1885 let retained = std::fs::File::open(&artifact).unwrap();
1886
1887 std::fs::hard_link(&artifact, &linked).unwrap();
1888
1889 assert_eq!(
1890 regular_file_identity_from_file(&retained)
1891 .unwrap_err()
1892 .kind(),
1893 DirectoryIdentityErrorKind::MultipleLinks
1894 );
1895 }
1896
1897 #[test]
1898 fn replacement_regular_file_has_a_different_identity() {
1899 let temporary = tempfile::tempdir().unwrap();
1900 let artifact = temporary.path().join("artifact.bin");
1901 let displaced = temporary.path().join("displaced.bin");
1902 std::fs::write(&artifact, b"original").unwrap();
1903 let original = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
1904
1905 std::fs::rename(&artifact, &displaced).unwrap();
1906 std::fs::write(&artifact, b"replacement").unwrap();
1907 let replacement = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
1908
1909 assert_ne!(original, replacement);
1910 assert_eq!(
1911 verify_regular_file_identity(&artifact, &original)
1912 .unwrap_err()
1913 .kind(),
1914 DirectoryIdentityErrorKind::IdentityMismatch
1915 );
1916 }
1917
1918 #[cfg(unix)]
1919 #[test]
1920 fn directory_capture_rejects_after_open_path_replacement() {
1921 let temporary = tempfile::tempdir().unwrap();
1922 let project = temporary.path().join("project");
1923 let displaced = temporary.path().join("displaced");
1924 std::fs::create_dir(&project).unwrap();
1925
1926 let error = platform::capture_after_open(&project, || {
1927 std::fs::rename(&project, &displaced).unwrap();
1928 std::fs::create_dir(&project).unwrap();
1929 })
1930 .unwrap_err();
1931
1932 assert_eq!(error.kind(), DirectoryIdentityErrorKind::IdentityMismatch);
1933 }
1934
1935 #[cfg(unix)]
1936 #[test]
1937 fn retained_directory_open_rejects_initial_path_replacement() {
1938 let temporary = tempfile::tempdir().unwrap();
1939 let project = temporary.path().join("project");
1940 let displaced = temporary.path().join("displaced");
1941 std::fs::create_dir(&project).unwrap();
1942
1943 let error = platform::open_retained_directory_after_open(&project, || {
1944 std::fs::rename(&project, &displaced).unwrap();
1945 std::fs::create_dir(&project).unwrap();
1946 })
1947 .unwrap_err();
1948
1949 assert_eq!(error.kind(), DirectoryIdentityErrorKind::IdentityMismatch);
1950 }
1951
1952 #[cfg(unix)]
1953 #[test]
1954 fn retained_unix_descriptor_detects_but_does_not_block_replacement() {
1955 let temporary = tempfile::tempdir().unwrap();
1956 let project = temporary.path().join("project");
1957 let displaced = temporary.path().join("displaced");
1958 std::fs::create_dir(&project).unwrap();
1959 let retained = RetainedDirectoryIdentity::open(&project).unwrap();
1960 let original = retained.identity().clone();
1961
1962 std::fs::rename(&project, &displaced).unwrap();
1963 std::fs::create_dir(&project).unwrap();
1964
1965 assert!(retained.as_file().metadata().unwrap().is_dir());
1966 assert_eq!(
1967 retained.verify_path(&project).unwrap_err().kind(),
1968 DirectoryIdentityErrorKind::IdentityMismatch
1969 );
1970 assert_eq!(
1971 DirectoryFilesystemIdentity::capture(&displaced).unwrap(),
1972 original
1973 );
1974 }
1975
1976 #[cfg(unix)]
1977 #[test]
1978 fn regular_file_capture_rejects_after_open_path_replacement() {
1979 let temporary = tempfile::tempdir().unwrap();
1980 let artifact = temporary.path().join("artifact.bin");
1981 let displaced = temporary.path().join("displaced.bin");
1982 std::fs::write(&artifact, b"original").unwrap();
1983
1984 let error = platform::capture_regular_file_after_open(&artifact, || {
1985 std::fs::rename(&artifact, &displaced).unwrap();
1986 std::fs::write(&artifact, b"replacement").unwrap();
1987 })
1988 .unwrap_err();
1989
1990 assert_eq!(error.kind(), DirectoryIdentityErrorKind::IdentityMismatch);
1991 }
1992
1993 #[cfg(windows)]
1994 #[test]
1995 fn directory_capture_blocks_or_rejects_after_open_path_replacement() {
1996 use std::cell::Cell;
1997
1998 let temporary = tempfile::tempdir().unwrap();
1999 let project = temporary.path().join("project");
2000 let displaced = temporary.path().join("displaced");
2001 std::fs::create_dir(&project).unwrap();
2002 let replaced = Cell::new(false);
2003
2004 let result = platform::capture_after_open(&project, || {
2005 if std::fs::rename(&project, &displaced).is_ok() {
2006 std::fs::create_dir(&project).unwrap();
2007 replaced.set(true);
2008 }
2009 });
2010
2011 if replaced.get() {
2012 assert_eq!(
2013 result.unwrap_err().kind(),
2014 DirectoryIdentityErrorKind::IdentityMismatch
2015 );
2016 } else {
2017 verify_directory_identity(&project, &result.unwrap()).unwrap();
2018 assert!(!displaced.exists());
2019 }
2020 }
2021
2022 #[cfg(windows)]
2023 #[test]
2024 fn retained_windows_handle_blocks_replacement_until_all_clones_drop() {
2025 let temporary = tempfile::tempdir().unwrap();
2026 let project = temporary.path().join("project");
2027 let displaced = temporary.path().join("displaced");
2028 std::fs::create_dir(&project).unwrap();
2029 let retained = RetainedDirectoryIdentity::open(&project).unwrap();
2030 let original = retained.identity().clone();
2031 let clone = retained.try_clone_file().unwrap();
2032
2033 assert!(std::fs::rename(&project, &displaced).is_err());
2034 assert!(std::fs::remove_dir(&project).is_err());
2035 retained.verify_path(&project).unwrap();
2036 drop(retained);
2037 assert!(std::fs::rename(&project, &displaced).is_err());
2038 drop(clone);
2039
2040 std::fs::rename(&project, &displaced).unwrap();
2041 std::fs::create_dir(&project).unwrap();
2042 assert_eq!(
2043 verify_directory_identity(&project, &original)
2044 .unwrap_err()
2045 .kind(),
2046 DirectoryIdentityErrorKind::IdentityMismatch
2047 );
2048 }
2049
2050 #[cfg(windows)]
2051 #[test]
2052 fn retained_ordinary_parent_flushes_strict_child_namespace() {
2053 let temporary = tempfile::tempdir().unwrap();
2054 let parent_path = temporary.path().join("ordinary-parent");
2055 let displaced = temporary.path().join("displaced-parent");
2056 let child_path = parent_path.join("private-child");
2057 std::fs::create_dir(&parent_path).unwrap();
2058 let parent = RetainedDirectoryIdentity::open(&parent_path).unwrap();
2059 let child =
2060 crate::windows_private_directory::create_private_directory(&child_path).unwrap();
2061
2062 parent.sync_metadata(&parent_path).unwrap();
2063 parent.verify_path(&parent_path).unwrap();
2064 assert!(child_path.is_dir());
2065 assert!(std::fs::rename(&parent_path, &displaced).is_err());
2066
2067 drop(child);
2068 std::fs::remove_dir(&child_path).unwrap();
2069 parent.sync_metadata(&parent_path).unwrap();
2070 drop(parent);
2071
2072 std::fs::rename(&parent_path, &displaced).unwrap();
2073 std::fs::remove_dir(&displaced).unwrap();
2074 }
2075
2076 #[cfg(windows)]
2077 #[test]
2078 fn read_only_directory_handle_flush_fails_closed() {
2079 let temporary = tempfile::tempdir().unwrap();
2080 let parent_path = temporary.path().join("ordinary-parent");
2081 std::fs::create_dir(&parent_path).unwrap();
2082 let (file, identity) =
2083 platform::open_retained_directory_without_write(&parent_path).unwrap();
2084 let error = platform::flush_directory_handle_for_test(&file).unwrap_err();
2085 assert_eq!(
2086 error.kind(),
2087 DirectoryIdentityErrorKind::OperatingSystem(DirectoryIdentityOperation::FlushDirectory)
2088 );
2089 assert_eq!(error.os_code(), Some(5));
2090 platform::verify_retained_directory_path(&file, &identity, &parent_path).unwrap();
2091 assert!(parent_path.is_dir());
2092 }
2093
2094 #[cfg(windows)]
2095 #[test]
2096 fn retained_system_directory_does_not_require_write_access() {
2097 let system_directory = crate::windows_system_root().unwrap().join("System32");
2098 let retained = RetainedDirectoryIdentity::open(&system_directory).unwrap();
2099 retained.verify_path(&system_directory).unwrap();
2100 }
2101
2102 #[cfg(windows)]
2103 #[test]
2104 fn retained_windows_open_blocks_or_rejects_initial_replacement() {
2105 use std::cell::Cell;
2106
2107 let temporary = tempfile::tempdir().unwrap();
2108 let project = temporary.path().join("project");
2109 let displaced = temporary.path().join("displaced");
2110 std::fs::create_dir(&project).unwrap();
2111 let replaced = Cell::new(false);
2112
2113 let result = platform::open_retained_directory_after_open(&project, || {
2114 if std::fs::rename(&project, &displaced).is_ok() {
2115 std::fs::create_dir(&project).unwrap();
2116 replaced.set(true);
2117 }
2118 });
2119
2120 if replaced.get() {
2121 assert_eq!(
2122 result.unwrap_err().kind(),
2123 DirectoryIdentityErrorKind::IdentityMismatch
2124 );
2125 } else {
2126 let (file, identity) = result.unwrap();
2127 assert!(file.metadata().unwrap().is_dir());
2128 assert_eq!(
2129 identity,
2130 DirectoryFilesystemIdentity::capture(&project).unwrap()
2131 );
2132 assert!(!displaced.exists());
2133 }
2134 }
2135
2136 #[cfg(windows)]
2137 #[test]
2138 fn regular_file_capture_blocks_or_rejects_after_open_path_replacement() {
2139 use std::cell::Cell;
2140
2141 let temporary = tempfile::tempdir().unwrap();
2142 let artifact = temporary.path().join("artifact.bin");
2143 let displaced = temporary.path().join("displaced.bin");
2144 std::fs::write(&artifact, b"original").unwrap();
2145 let replaced = Cell::new(false);
2146
2147 let result = platform::capture_regular_file_after_open(&artifact, || {
2148 if std::fs::rename(&artifact, &displaced).is_ok() {
2149 std::fs::write(&artifact, b"replacement").unwrap();
2150 replaced.set(true);
2151 }
2152 });
2153
2154 if replaced.get() {
2155 assert_eq!(
2156 result.unwrap_err().kind(),
2157 DirectoryIdentityErrorKind::IdentityMismatch
2158 );
2159 } else {
2160 verify_regular_file_identity(&artifact, &result.unwrap()).unwrap();
2161 assert!(!displaced.exists());
2162 }
2163 }
2164
2165 #[test]
2166 fn regular_file_identity_rejects_relative_and_directory_paths() {
2167 assert_eq!(
2168 RegularFileFilesystemIdentity::capture(Path::new("relative"))
2169 .unwrap_err()
2170 .kind(),
2171 DirectoryIdentityErrorKind::InvalidPath
2172 );
2173
2174 let directory = tempfile::tempdir().unwrap();
2175 assert_eq!(
2176 RegularFileFilesystemIdentity::capture(directory.path())
2177 .unwrap_err()
2178 .kind(),
2179 DirectoryIdentityErrorKind::NotRegularFile
2180 );
2181 }
2182
2183 #[test]
2184 fn regular_file_identity_rejects_multiple_links() {
2185 let temporary = tempfile::tempdir().unwrap();
2186 let artifact = temporary.path().join("artifact.bin");
2187 let linked = temporary.path().join("linked.bin");
2188 std::fs::write(&artifact, b"artifact").unwrap();
2189 let identity = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
2190 std::fs::hard_link(&artifact, &linked).unwrap();
2191
2192 assert_eq!(
2193 RegularFileFilesystemIdentity::capture(&artifact)
2194 .unwrap_err()
2195 .kind(),
2196 DirectoryIdentityErrorKind::MultipleLinks
2197 );
2198 assert_eq!(
2199 verify_regular_file_identity(&artifact, &identity)
2200 .unwrap_err()
2201 .kind(),
2202 DirectoryIdentityErrorKind::MultipleLinks
2203 );
2204 assert_eq!(
2205 RetainedRegularFileIdentity::open(&artifact)
2206 .unwrap_err()
2207 .kind(),
2208 DirectoryIdentityErrorKind::MultipleLinks
2209 );
2210 }
2211
2212 #[test]
2213 fn retained_regular_file_identity_rejects_mismatched_path() {
2214 let temporary = tempfile::tempdir().unwrap();
2215 let artifact = temporary.path().join("artifact.bin");
2216 let other = temporary.path().join("other.bin");
2217 std::fs::write(&artifact, b"artifact").unwrap();
2218 std::fs::write(&other, b"other").unwrap();
2219 let retained = RetainedRegularFileIdentity::open(&artifact).unwrap();
2220
2221 assert_eq!(
2222 retained.verify_path(&other).unwrap_err().kind(),
2223 DirectoryIdentityErrorKind::IdentityMismatch
2224 );
2225 retained.verify_path(&artifact).unwrap();
2226 }
2227
2228 #[cfg(windows)]
2229 #[test]
2230 fn retained_regular_file_blocks_modification_and_replacement_until_drop() {
2231 let temporary = tempfile::tempdir().unwrap();
2232 let artifact = temporary.path().join("artifact.bin");
2233 let displaced = temporary.path().join("displaced.bin");
2234 std::fs::write(&artifact, b"original").unwrap();
2235 let retained = RetainedRegularFileIdentity::open(&artifact).unwrap();
2236 let expected = retained.identity().clone();
2237
2238 assert!(std::fs::write(&artifact, b"modified").is_err());
2239 assert!(std::fs::rename(&artifact, &displaced).is_err());
2240 assert!(std::fs::remove_file(&artifact).is_err());
2241 retained.verify_path(&artifact).unwrap();
2242 drop(retained);
2243
2244 std::fs::rename(&artifact, &displaced).unwrap();
2245 std::fs::write(&artifact, b"replacement").unwrap();
2246 assert_eq!(
2247 verify_regular_file_identity(&artifact, &expected)
2248 .unwrap_err()
2249 .kind(),
2250 DirectoryIdentityErrorKind::IdentityMismatch
2251 );
2252 }
2253
2254 #[cfg(unix)]
2255 #[test]
2256 fn retained_regular_file_detects_unix_namespace_rebinding() {
2257 let temporary = tempfile::tempdir().unwrap();
2258 let artifact = temporary.path().join("artifact.bin");
2259 let displaced = temporary.path().join("displaced.bin");
2260 std::fs::write(&artifact, b"original").unwrap();
2261 let retained = RetainedRegularFileIdentity::open(&artifact).unwrap();
2262
2263 std::fs::rename(&artifact, &displaced).unwrap();
2264 std::fs::write(&artifact, b"replacement").unwrap();
2265 assert_eq!(
2266 retained.verify_path(&artifact).unwrap_err().kind(),
2267 DirectoryIdentityErrorKind::IdentityMismatch
2268 );
2269 assert_eq!(std::fs::read(&displaced).unwrap(), b"original");
2270 }
2271
2272 #[cfg(windows)]
2273 #[test]
2274 fn retained_regular_file_supports_long_windows_paths() {
2275 let temporary = tempfile::tempdir().unwrap();
2276 let mut parent = temporary.path().to_path_buf();
2277 while parent.as_os_str().len() < 280 {
2278 parent.push("retained-identity");
2279 }
2280 std::fs::create_dir_all(&parent).unwrap();
2281 let artifact = parent.join("artifact.bin");
2282 std::fs::write(&artifact, b"artifact").unwrap();
2283
2284 let retained = RetainedRegularFileIdentity::open(&artifact).unwrap();
2285 retained.verify_path(&artifact).unwrap();
2286 }
2287
2288 #[cfg(unix)]
2289 #[test]
2290 fn final_symbolic_link_is_rejected() {
2291 use std::os::unix::fs::symlink;
2292
2293 let temporary = tempfile::tempdir().unwrap();
2294 let target = temporary.path().join("target");
2295 let link = temporary.path().join("link");
2296 std::fs::create_dir(&target).unwrap();
2297 symlink(&target, &link).unwrap();
2298
2299 assert_eq!(
2300 DirectoryFilesystemIdentity::capture(&link)
2301 .unwrap_err()
2302 .kind(),
2303 DirectoryIdentityErrorKind::ReparsePoint
2304 );
2305 assert_eq!(
2306 RetainedDirectoryIdentity::open(&link).unwrap_err().kind(),
2307 DirectoryIdentityErrorKind::ReparsePoint
2308 );
2309 }
2310
2311 #[cfg(unix)]
2312 #[test]
2313 fn final_file_symbolic_link_is_rejected() {
2314 use std::os::unix::fs::symlink;
2315
2316 let temporary = tempfile::tempdir().unwrap();
2317 let target = temporary.path().join("target.bin");
2318 let link = temporary.path().join("link.bin");
2319 std::fs::write(&target, b"artifact").unwrap();
2320 symlink(&target, &link).unwrap();
2321
2322 assert_eq!(
2323 RegularFileFilesystemIdentity::capture(&link)
2324 .unwrap_err()
2325 .kind(),
2326 DirectoryIdentityErrorKind::ReparsePoint
2327 );
2328 assert_eq!(
2329 RetainedRegularFileIdentity::open(&link).unwrap_err().kind(),
2330 DirectoryIdentityErrorKind::ReparsePoint
2331 );
2332 }
2333
2334 #[cfg(windows)]
2335 #[test]
2336 fn final_directory_reparse_point_is_rejected_when_supported() {
2337 use std::os::windows::fs::symlink_dir;
2338
2339 let temporary = tempfile::tempdir().unwrap();
2340 let target = temporary.path().join("target");
2341 let link = temporary.path().join("link");
2342 std::fs::create_dir(&target).unwrap();
2343 if let Err(error) = symlink_dir(&target, &link) {
2344 if error.kind() == std::io::ErrorKind::PermissionDenied
2345 || error.raw_os_error() == Some(1314)
2346 {
2347 return;
2348 }
2349 panic!("could not create test directory symlink: {error}");
2350 }
2351
2352 assert_eq!(
2353 DirectoryFilesystemIdentity::capture(&link)
2354 .unwrap_err()
2355 .kind(),
2356 DirectoryIdentityErrorKind::ReparsePoint
2357 );
2358 assert_eq!(
2359 RetainedDirectoryIdentity::open(&link).unwrap_err().kind(),
2360 DirectoryIdentityErrorKind::ReparsePoint
2361 );
2362 }
2363
2364 #[cfg(windows)]
2365 #[test]
2366 fn final_file_reparse_point_is_rejected_when_supported() {
2367 use std::os::windows::fs::symlink_file;
2368
2369 let temporary = tempfile::tempdir().unwrap();
2370 let target = temporary.path().join("target.bin");
2371 let link = temporary.path().join("link.bin");
2372 std::fs::write(&target, b"artifact").unwrap();
2373 if let Err(error) = symlink_file(&target, &link) {
2374 if error.kind() == std::io::ErrorKind::PermissionDenied
2375 || error.raw_os_error() == Some(1314)
2376 {
2377 return;
2378 }
2379 panic!("could not create test file symlink: {error}");
2380 }
2381
2382 assert_eq!(
2383 RegularFileFilesystemIdentity::capture(&link)
2384 .unwrap_err()
2385 .kind(),
2386 DirectoryIdentityErrorKind::ReparsePoint
2387 );
2388 assert_eq!(
2389 RetainedRegularFileIdentity::open(&link).unwrap_err().kind(),
2390 DirectoryIdentityErrorKind::ReparsePoint
2391 );
2392 }
2393
2394 #[cfg(windows)]
2395 #[test]
2396 fn exact_regular_file_removal_is_handle_bound_and_preserves_replacement() {
2397 let temporary = tempfile::tempdir().unwrap();
2398 let artifact = temporary.path().join("artifact.bin");
2399 let displaced = temporary.path().join("displaced.bin");
2400 std::fs::write(&artifact, b"original").unwrap();
2401 let expected = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
2402
2403 let removal = open_regular_file_for_exact_removal(&artifact).unwrap();
2404 assert_eq!(removal.identity(), &expected);
2405
2406 if std::fs::rename(&artifact, &displaced).is_ok() {
2407 std::fs::write(&artifact, b"replacement").unwrap();
2408 removal.remove().unwrap();
2409
2410 assert_eq!(std::fs::read(&artifact).unwrap(), b"replacement");
2411 assert!(!displaced.exists());
2412 } else {
2413 assert!(std::fs::write(&artifact, b"replacement").is_err());
2414 removal.remove().unwrap();
2415
2416 assert!(!artifact.exists());
2417 assert!(!displaced.exists());
2418 }
2419 }
2420
2421 #[cfg(windows)]
2422 #[test]
2423 fn exact_regular_file_removal_revalidates_retained_identity() {
2424 let temporary = tempfile::tempdir().unwrap();
2425 let artifact = temporary.path().join("artifact.bin");
2426 let other = temporary.path().join("other.bin");
2427 std::fs::write(&artifact, b"artifact").unwrap();
2428 std::fs::write(&other, b"other").unwrap();
2429
2430 let removal = open_regular_file_for_exact_removal(&artifact).unwrap();
2431 let wrong_identity = RegularFileFilesystemIdentity::capture(&other).unwrap();
2432 let removal = ExactRegularFileRemoval {
2433 file: removal.file,
2434 identity: wrong_identity,
2435 };
2436
2437 assert_eq!(
2438 removal.remove().unwrap_err().kind(),
2439 DirectoryIdentityErrorKind::IdentityMismatch
2440 );
2441 assert_eq!(std::fs::read(&artifact).unwrap(), b"artifact");
2442 assert_eq!(std::fs::read(&other).unwrap(), b"other");
2443 }
2444
2445 #[cfg(windows)]
2446 #[test]
2447 fn exact_regular_file_removal_rejects_hardlinks_before_and_after_open() {
2448 let temporary = tempfile::tempdir().unwrap();
2449 let linked_before = temporary.path().join("linked-before.bin");
2450 let before = temporary.path().join("before.bin");
2451 std::fs::write(&before, b"before").unwrap();
2452 std::fs::hard_link(&before, &linked_before).unwrap();
2453
2454 assert_eq!(
2455 open_regular_file_for_exact_removal(&before)
2456 .unwrap_err()
2457 .kind(),
2458 DirectoryIdentityErrorKind::MultipleLinks
2459 );
2460
2461 let after = temporary.path().join("after.bin");
2462 let linked_after = temporary.path().join("linked-after.bin");
2463 std::fs::write(&after, b"after").unwrap();
2464 let removal = open_regular_file_for_exact_removal(&after).unwrap();
2465
2466 std::fs::hard_link(&after, &linked_after).unwrap();
2467 assert_eq!(
2468 removal.remove().unwrap_err().kind(),
2469 DirectoryIdentityErrorKind::MultipleLinks
2470 );
2471 assert_eq!(std::fs::read(&after).unwrap(), b"after");
2472 assert_eq!(std::fs::read(&linked_after).unwrap(), b"after");
2473 }
2474
2475 #[cfg(windows)]
2476 #[test]
2477 fn exact_regular_file_removal_rejects_reparse_and_directory_targets_when_supported() {
2478 use std::os::windows::fs::symlink_file;
2479
2480 let temporary = tempfile::tempdir().unwrap();
2481 assert_eq!(
2482 open_regular_file_for_exact_removal(temporary.path())
2483 .unwrap_err()
2484 .kind(),
2485 DirectoryIdentityErrorKind::NotRegularFile
2486 );
2487
2488 let target = temporary.path().join("target.bin");
2489 let link = temporary.path().join("link.bin");
2490 std::fs::write(&target, b"artifact").unwrap();
2491 if let Err(error) = symlink_file(&target, &link) {
2492 if error.kind() == std::io::ErrorKind::PermissionDenied
2493 || error.raw_os_error() == Some(1314)
2494 {
2495 return;
2496 }
2497 panic!("could not create test file symlink: {error}");
2498 }
2499
2500 assert_eq!(
2501 open_regular_file_for_exact_removal(&link)
2502 .unwrap_err()
2503 .kind(),
2504 DirectoryIdentityErrorKind::ReparsePoint
2505 );
2506 assert_eq!(std::fs::read(&target).unwrap(), b"artifact");
2507 assert!(link.exists());
2508 }
2509
2510 #[cfg(windows)]
2511 #[test]
2512 fn long_absolute_directory_path_is_supported() {
2513 use std::os::windows::ffi::OsStrExt as _;
2514
2515 let temporary = tempfile::tempdir().unwrap();
2516 let mut directory = temporary.path().to_owned();
2517 let mut segment = 0_u32;
2518 while directory.as_os_str().encode_wide().count() <= 300 {
2519 directory.push(format!("long-project-directory-segment-{segment:02}"));
2520 std::fs::create_dir(&directory).unwrap();
2521 segment += 1;
2522 }
2523
2524 let identity = DirectoryFilesystemIdentity::capture(&directory).unwrap();
2525 verify_directory_identity(&directory, &identity).unwrap();
2526 let retained = RetainedDirectoryIdentity::open(&directory).unwrap();
2527 assert_eq!(retained.identity(), &identity);
2528 retained.verify_path(&directory).unwrap();
2529 retained.sync_metadata(&directory).unwrap();
2530 }
2531
2532 #[cfg(windows)]
2533 #[test]
2534 fn long_absolute_regular_file_path_is_supported() {
2535 use std::os::windows::ffi::OsStrExt as _;
2536
2537 let temporary = tempfile::tempdir().unwrap();
2538 let mut directory = temporary.path().to_owned();
2539 let mut segment = 0_u32;
2540 while directory.as_os_str().encode_wide().count() <= 300 {
2541 directory.push(format!("long-artifact-directory-segment-{segment:02}"));
2542 std::fs::create_dir(&directory).unwrap();
2543 segment += 1;
2544 }
2545 let artifact = directory.join("artifact.bin");
2546 std::fs::write(&artifact, b"artifact").unwrap();
2547
2548 let identity = RegularFileFilesystemIdentity::capture(&artifact).unwrap();
2549 verify_regular_file_identity(&artifact, &identity).unwrap();
2550 }
2551}