Skip to main content

rustferry_core/
filesystem_identity.rs

1//! Stable, handle-bound filesystem identities for durable filesystem references.
2
3use 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/// Filesystem operation that failed while capturing an object identity.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15#[non_exhaustive]
16pub enum DirectoryIdentityOperation {
17    /// Open the directory without following its final path component.
18    OpenDirectory,
19    /// Open a regular file without following its final path component.
20    OpenRegularFile,
21    /// Read directory attributes from the retained handle.
22    QueryAttributes,
23    /// Read regular-file attributes and link count from the retained handle.
24    QueryLinkCount,
25    /// Read filesystem metadata from the retained descriptor.
26    QueryMetadata,
27    /// Duplicate a retained filesystem handle or descriptor.
28    CloneHandle,
29    /// Flush retained directory metadata through the exact handle.
30    FlushDirectory,
31    /// Remove a retained regular file through its exact handle.
32    RemoveRegularFile,
33}
34
35/// Stable reason a filesystem identity operation was rejected.
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37#[non_exhaustive]
38pub enum DirectoryIdentityErrorKind {
39    /// The path is relative, empty, or cannot be passed to the operating system safely.
40    InvalidPath,
41    /// The retained filesystem object is not a directory.
42    NotDirectory,
43    /// The retained filesystem object is not a regular file.
44    NotRegularFile,
45    /// The retained filesystem object is a symbolic link or Windows reparse point.
46    ReparsePoint,
47    /// A regular file has a filesystem link count other than exactly one.
48    MultipleLinks,
49    /// The filesystem did not provide the required persistent identity fields.
50    IdentityUnavailable,
51    /// A filesystem operation failed.
52    OperatingSystem(DirectoryIdentityOperation),
53    /// A persisted identity string is not in the stable canonical format.
54    InvalidEncoding,
55    /// The reopened filesystem object does not have the expected identity.
56    IdentityMismatch,
57    /// The target platform has no supported handle-bound filesystem identity implementation.
58    UnsupportedPlatform,
59}
60
61/// Sanitized failure from filesystem identity capture, parsing, or verification.
62#[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    /// Return the stable failure classification.
74    pub const fn kind(&self) -> DirectoryIdentityErrorKind {
75        self.kind
76    }
77
78    /// Return the platform error code without any path or user-controlled text.
79    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/// Opaque, versioned identity of one directory filesystem object.
126///
127/// The serialized value contains only filesystem identity fields, never the source path. Callers
128/// should persist and compare the complete string rather than interpreting its components.
129#[derive(Clone, Debug, Eq, Hash, PartialEq)]
130pub struct DirectoryFilesystemIdentity(String);
131
132impl DirectoryFilesystemIdentity {
133    /// Capture the exact directory object currently named by an absolute path.
134    ///
135    /// # Errors
136    ///
137    /// Returns a typed error when the path is not absolute, the final object is not a plain
138    /// directory, or the operating system cannot supply a handle-bound persistent identity.
139    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    /// Return the stable ASCII representation for durable storage.
150    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
193/// Retained, policy-neutral handle and identity for one exact directory.
194///
195/// The final path component is opened without following a symbolic link or Windows reparse point.
196/// On Windows the retained handle shares reads and writes but omits delete sharing, preventing the
197/// named directory from being renamed, deleted, or replaced while this value or one of its cloned
198/// handles remains alive. Unix descriptors do not prevent namespace rename or replacement, so
199/// callers must use [`Self::verify_path`] immediately before and after sensitive pathname work.
200pub struct RetainedDirectoryIdentity {
201    file: std::fs::File,
202    identity: DirectoryFilesystemIdentity,
203}
204
205impl RetainedDirectoryIdentity {
206    /// Open and retain the exact plain directory currently named by an absolute path.
207    ///
208    /// The returned file is opened for directory reads and can be safely duplicated for capability
209    /// consumers. Metadata synchronization acquires a separate handle bound to this exact retained
210    /// directory, so read-only installation directories can still be retained and verified.
211    ///
212    /// # Errors
213    ///
214    /// Returns a typed error when the path is not absolute, the final component is a symbolic link,
215    /// reparse point, or non-directory, the platform cannot derive a persistent identity, or the
216    /// pathname changes during initial binding.
217    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    /// Return the canonical identity captured from the retained handle.
229    pub fn identity(&self) -> &DirectoryFilesystemIdentity {
230        &self.identity
231    }
232
233    /// Borrow the exact retained directory file for capability-based consumers.
234    pub fn as_file(&self) -> &std::fs::File {
235        &self.file
236    }
237
238    /// Duplicate the exact retained directory handle or descriptor.
239    ///
240    /// On Windows the duplicate preserves the original no-delete-sharing lifetime. On Unix it
241    /// retains the same directory object but still does not prevent namespace rename.
242    ///
243    /// # Errors
244    ///
245    /// Returns a typed operating-system error when the handle cannot be duplicated.
246    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    /// Require the current absolute pathname to identify the same retained directory object.
258    ///
259    /// Windows no-delete sharing keeps a successfully rebound pathname stable while the retained
260    /// handle lives. Unix verification is an instantaneous identity check only; another process can
261    /// rename or replace the path immediately after this method returns.
262    ///
263    /// # Errors
264    ///
265    /// Returns a typed open/validation error, or [`DirectoryIdentityErrorKind::IdentityMismatch`]
266    /// when the path no longer names this retained object.
267    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    /// Synchronize metadata for the retained Windows directory and verify its current pathname.
278    ///
279    /// This policy-neutral operation requires no owner or DACL shape. It verifies the retained
280    /// handle and current absolute pathname before and after a synchronous Windows metadata flush.
281    /// While the retained no-delete-sharing handle keeps that binding stable, the operation opens
282    /// a write-capable handle to the same verified directory and flushes only that handle. Retain
283    /// this value before creating or publishing a child, then call this method on the unchanged
284    /// parent path to establish the parent namespace durability barrier.
285    ///
286    /// A successful return means Windows reported completion of the flush request. Physical
287    /// hardware can still fail to honor cache-flush commands.
288    ///
289    /// # Errors
290    ///
291    /// Returns a typed failure when `path` is not absolute, no longer binds the retained directory,
292    /// the handle lacks directory-write access, or Windows does not report successful synchronous
293    /// completion. A rejected or unsupported flush is never converted into success.
294    #[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/// Opaque, versioned identity of one single-link regular file.
316///
317/// The serialized value contains only filesystem identity fields, never the source path. Its
318/// distinct prefix prevents a persisted directory identity from being accepted as a file identity.
319#[derive(Clone, Debug, Eq, Hash, PartialEq)]
320pub struct RegularFileFilesystemIdentity(String);
321
322impl RegularFileFilesystemIdentity {
323    /// Capture the exact single-link regular file currently named by an absolute path.
324    ///
325    /// # Errors
326    ///
327    /// Returns a typed error when the path is not absolute, the final object is not a plain
328    /// single-link regular file, or the operating system cannot supply a persistent identity.
329    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    /// Return the stable ASCII representation for durable storage.
340    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/// Retained identity guard for one exact single-link regular file.
384///
385/// On Windows the retained handle denies write and delete sharing, so the named file cannot be
386/// modified, renamed, or replaced while the guard is live. On Unix the descriptor retains the
387/// exact object but cannot prevent namespace rebinding; callers must use [`Self::verify_path`]
388/// immediately before and after sensitive pathname work and must not claim moment-wide binding.
389#[derive(Debug)]
390pub struct RetainedRegularFileIdentity {
391    file: std::fs::File,
392    identity: RegularFileFilesystemIdentity,
393}
394
395impl RetainedRegularFileIdentity {
396    /// Open and retain the exact plain single-link regular file currently named by an absolute path.
397    ///
398    /// # Errors
399    ///
400    /// Returns a typed error when the path is not absolute, the final object is a symbolic link or
401    /// reparse point, the file has multiple links, or a stable filesystem identity is unavailable.
402    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    /// Return the identity captured from the retained handle.
414    pub fn identity(&self) -> &RegularFileFilesystemIdentity {
415        &self.identity
416    }
417
418    /// Require the path to still name the exact retained file.
419    ///
420    /// # Errors
421    ///
422    /// Returns a typed capture error, or [`DirectoryIdentityErrorKind::IdentityMismatch`] when the
423    /// path no longer names the retained exact file.
424    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/// Retained Windows handle for policy-neutral removal of one exact regular file.
436///
437/// Unlike the private-file APIs in [`crate::windows_private_directory`], opening this handle does
438/// not require a protected DACL or a particular owner. The handle denies write and delete sharing,
439/// rejects reparse points and multiple links, and remains the sole authority used by [`Self::remove`].
440#[cfg(windows)]
441#[derive(Debug)]
442pub struct ExactRegularFileRemoval {
443    file: std::fs::File,
444    identity: RegularFileFilesystemIdentity,
445}
446
447#[cfg(windows)]
448impl ExactRegularFileRemoval {
449    /// Return the identity captured from the retained handle.
450    ///
451    /// Callers with a persisted expected identity must compare it before removing the file.
452    pub fn identity(&self) -> &RegularFileFilesystemIdentity {
453        &self.identity
454    }
455
456    /// Revalidate and remove the exact retained single-link regular file.
457    ///
458    /// This operation never reopens or deletes a pathname. On any error, callers must treat
459    /// cleanup as uncertain; the exact handle is consumed and dropped without deleting a path
460    /// that may now name a replacement.
461    ///
462    /// # Errors
463    ///
464    /// Returns a typed validation or operating-system error when the retained object no longer
465    /// satisfies the identity, type, or single-link invariant, or handle-bound removal fails.
466    pub fn remove(self) -> Result<(), DirectoryIdentityError> {
467        platform::remove_exact_regular_file(self)
468    }
469}
470
471/// Capture a single-link regular-file identity from an already-retained file object.
472///
473/// This function performs no pathname lookup and imposes no owner or DACL policy. It validates
474/// the retained object type, rejects Windows reparse points and non-single-link files, and derives
475/// the canonical identity from that same file handle or descriptor.
476///
477/// # Errors
478///
479/// Returns a typed validation or operating-system error when the retained object is not a plain
480/// single-link regular file or the platform cannot supply its persistent identity.
481pub 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
487/// Capture a directory identity from an already-retained file object.
488///
489/// This function performs no pathname lookup and imposes no owner or DACL policy. It validates
490/// that the retained object is a directory, rejects a Windows reparse-point handle, and derives
491/// the canonical identity from that same handle or descriptor.
492///
493/// # Errors
494///
495/// Returns a typed validation or operating-system error when the retained object is not a plain
496/// directory or the platform cannot supply its persistent identity.
497pub fn directory_identity_from_file(
498    file: &std::fs::File,
499) -> Result<DirectoryFilesystemIdentity, DirectoryIdentityError> {
500    platform::directory_identity_from_file(file)
501}
502
503/// Report whether a retained Unix directory no longer has a live filesystem name.
504#[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/// Open an absolute Windows path for policy-neutral exact regular-file removal.
511///
512/// The final path component is opened without following a reparse point. The retained handle
513/// requests read and delete access while denying write and delete sharing, so pathname rename or
514/// replacement is blocked on filesystems that enforce Windows share modes. No owner or DACL policy
515/// is imposed; callers should compare [`ExactRegularFileRemoval::identity`] with their persisted
516/// expected identity before calling [`ExactRegularFileRemoval::remove`].
517///
518/// # Errors
519///
520/// Returns a typed error when the path is not absolute, the final object is not a plain
521/// single-link regular file, or the operating system cannot open and identify the retained object.
522#[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
535/// Reopen an absolute path without following its final component and require exact identity.
536///
537/// # Errors
538///
539/// Returns a typed capture error, or [`DirectoryIdentityErrorKind::IdentityMismatch`] when the
540/// reopened filesystem object differs from `expected`.
541pub 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
555/// Reopen an absolute path without following its final component and require exact file identity.
556///
557/// # Errors
558///
559/// Returns a typed capture error, or [`DirectoryIdentityErrorKind::IdentityMismatch`] when the
560/// reopened regular file differs from `expected`.
561pub 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        // The retained no-delete-sharing handle keeps `wide_path` bound to this exact object while
703        // the write-capable flush handle is opened. Identity checks still fail closed if a
704        // filesystem does not honor classic Windows sharing semantics.
705        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        // SAFETY: `directory` is a directory handle. Normal mode (`flags == 0`) synchronously
724        // requests cached data, metadata, and the backing storage cache; this mode accepts no
725        // optional parameter block.
726        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            // SAFETY: `status` is the exact failure returned by `NtFlushBuffersFileEx`.
737            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        // SAFETY: `file` is the retained exact handle opened with DELETE access and `disposition`
871        // is readable storage of the exact information-class structure for `size` bytes.
872        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        // SAFETY: `path` is NUL-terminated and all optional pointer parameters are null. The flags
944        // open the final component itself and permit classifying directory handles. Omitting
945        // delete sharing blocks replacement on filesystems that enforce classic share semantics;
946        // the caller also reopens and compares identities before returning.
947        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        // SAFETY: successful `CreateFileW` transferred one owned real handle.
964        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        // SAFETY: `path` is NUL-terminated and all optional pointer parameters are null. Opening
981        // the final component itself plus omitting write/delete sharing binds rollback authority
982        // to this exact object and blocks pathname replacement while the handle remains live.
983        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        // SAFETY: successful `CreateFileW` transferred one owned real handle.
1000        Ok(unsafe { File::from_raw_handle(handle) })
1001    }
1002
1003    fn open_retained_regular_file_handle(path: &[u16]) -> Result<File, DirectoryIdentityError> {
1004        // SAFETY: `path` is NUL-terminated and all optional pointer parameters are null. Opening
1005        // the final component itself plus omitting write/delete sharing binds reads to this exact
1006        // object and blocks modification or pathname replacement while the handle remains live.
1007        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        // SAFETY: successful `CreateFileW` transferred one owned real handle.
1024        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        // SAFETY: the retained file owns a live handle and `attributes` is writable storage of the
1044        // exact class-specific structure for the supplied size.
1045        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        // SAFETY: the retained file owns a live handle and `identity` is writable storage of the
1085        // exact class-specific structure for the supplied size.
1086        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        // SAFETY: the retained file owns a live handle and `information` is writable initialized
1105        // storage of the exact structure expected by `GetFileInformationByHandle`.
1106        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        // SAFETY: `file` owns a live retained handle for the duration of the query.
1127        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            // SAFETY: `directory` is live and `path` is a writable PATH_MAX-sized buffer.
1295            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}