Skip to main content

graphforge_filesystem/
lib.rs

1//! Audited native filesystem primitives used by GraphForge's durability
2//! protocol.
3//!
4//! Cache-release I/O and platform operations have private owners; common
5//! capability types and public entrypoints remain at the crate root.
6
7#![deny(unsafe_code)]
8
9use std::ffi::{OsStr, OsString};
10use std::fs::File;
11use std::io;
12use std::path::{Path, PathBuf};
13
14mod cache_io;
15mod platform;
16#[cfg(windows)]
17#[allow(unsafe_code)]
18mod windows;
19#[cfg(windows)]
20mod windows_cas;
21
22pub use cache_io::DEFAULT_CACHE_RELEASE_WINDOW_BYTES;
23pub use cache_io::DurableFileCacheWriter;
24pub use cache_io::FileCacheReleaseEvidence;
25pub use cache_io::FileCacheReleaseOutcome;
26pub use cache_io::FileCacheReleaseTracker;
27pub use cache_io::FileCacheReleasingReader;
28pub use cache_io::cache_release_window_for_streams;
29pub use cache_io::release_file_cache;
30pub use cache_io::validate_cache_release_operation_windows;
31use platform::create_private_directory_platform;
32use platform::file_identity_platform;
33use platform::file_link_count_platform;
34use platform::file_space_usage_platform;
35use platform::install_new_file_platform;
36pub use platform::is_link_or_reparse;
37use platform::link_count;
38use platform::path_identity_platform;
39use platform::path_link_count_platform;
40use platform::rename_no_replace_platform;
41use platform::stable_child_names;
42use platform::stable_child_names_bounded;
43use platform::stable_create_child_directory;
44use platform::stable_link_child;
45use platform::stable_open_child_directory;
46use platform::stable_open_child_file;
47use platform::stable_open_directory;
48#[cfg(windows)]
49use platform::stable_open_directory_for_sync;
50use platform::stable_open_or_create_child_file;
51use platform::stable_open_publishing_child_file;
52use platform::stable_open_replaceable_child_file;
53use platform::stable_remove_child_directory_if_identity;
54use platform::stable_unlink_child_if_identity;
55use platform::visit_regular_files_platform;
56use platform::{replace_file_from_platform, replace_file_platform};
57#[cfg(windows)]
58pub use windows_cas::{WindowsCasWriter, WindowsLegacyCasAdopter, WindowsSealedCasFile};
59
60/// Stable filesystem identity suitable for Windows and Unix filesystems.
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct FileIdentity {
63    /// Native volume/device identity.
64    pub volume_serial: u64,
65    /// Full native file identity (128-bit on Windows; zero-extended inode on Unix).
66    pub file_id: [u8; 16],
67}
68
69/// Logical and physically allocated byte counts for one retained file handle.
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub struct FileSpaceUsage {
72    /// Logical end-of-file length visible to readers.
73    pub logical_bytes: u64,
74    /// Physical filesystem allocation charged to the file.
75    pub allocated_bytes: u64,
76}
77
78/// Stage of a failed retained-directory validation. Policy owners may preserve
79/// their own diagnostics without duplicating native identity checks.
80#[derive(Debug, Clone, Copy, PartialEq, Eq)]
81pub enum DirectoryValidationStage {
82    /// Named path metadata could not be read.
83    NamedMetadata,
84    /// Retained handle metadata could not be read.
85    RetainedMetadata,
86    /// Named path identity could not be read.
87    NamedIdentity,
88    /// Retained handle identity could not be read.
89    RetainedIdentity,
90    /// A path/handle is not an ordinary directory or its identity changed.
91    IdentityChanged,
92}
93
94/// Native validation failure with a typed stage and original I/O diagnostic.
95#[derive(Debug)]
96pub struct DirectoryValidationError {
97    stage: DirectoryValidationStage,
98    source: io::Error,
99}
100
101impl DirectoryValidationError {
102    fn new(stage: DirectoryValidationStage, source: io::Error) -> Self {
103        Self { stage, source }
104    }
105
106    /// Return the failing native operation for a caller's policy mapping.
107    #[must_use]
108    pub fn stage(&self) -> DirectoryValidationStage {
109        self.stage
110    }
111
112    /// Preserve the existing native I/O error and kind for generic consumers.
113    #[must_use]
114    pub fn into_io_error(self) -> io::Error {
115        self.source
116    }
117}
118
119impl std::fmt::Display for DirectoryValidationError {
120    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
121        std::fmt::Display::fmt(&self.source, formatter)
122    }
123}
124
125impl std::error::Error for DirectoryValidationError {
126    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
127        Some(&self.source)
128    }
129}
130
131/// Directory handle opened with this crate's native no-follow and sharing policy.
132/// Private construction prevents an arbitrary `File` from claiming that policy.
133#[derive(Debug)]
134pub struct OpenedDirectoryHandle {
135    file: File,
136}
137
138impl OpenedDirectoryHandle {
139    /// Borrow the native handle for metadata, volume queries, or cooperative locks.
140    #[must_use]
141    pub fn as_file(&self) -> &File {
142        &self.file
143    }
144
145    /// Transfer the raw handle to a caller that owns its validation policy.
146    #[must_use]
147    pub fn into_file(self) -> File {
148        self.file
149    }
150}
151
152/// Retained directory capability whose children are opened without following
153/// links or reparse points.
154#[derive(Debug)]
155pub struct StableDirectory {
156    path: PathBuf,
157    file: File,
158    identity: FileIdentity,
159}
160
161/// Single-owner capability for one exclusively created, unpublished child.
162///
163/// The guard follows the owned inode across an atomic rename and removes any
164/// still-uncommitted name on explicit cleanup or drop. Callers must retain the
165/// guard until every higher-level publication invariant, including parent
166/// directory synchronization and manifest/receipt inclusion, is complete.
167#[derive(Debug)]
168pub struct UnpublishedArtifactGuard {
169    directory: StableDirectory,
170    candidate_names: Vec<OsString>,
171    identity: Option<FileIdentity>,
172    file: Option<File>,
173    published: bool,
174    parent_synced: bool,
175    armed: bool,
176}
177
178impl UnpublishedArtifactGuard {
179    /// Return the immutable identity captured during descriptor setup.
180    ///
181    /// # Errors
182    /// Returns an error before descriptor identity has been initialized.
183    pub fn identity(&self) -> io::Result<FileIdentity> {
184        self.identity
185            .ok_or_else(|| io::Error::other("unpublished artifact identity is not initialized"))
186    }
187
188    /// Re-read the retained descriptor identity and run a setup check before
189    /// the descriptor is transferred to a writer.
190    ///
191    /// # Errors
192    /// Returns an error when identity observation, `setup_check`, or the
193    /// identity comparison fails.
194    pub fn verify_identity_with(
195        &mut self,
196        setup_check: impl FnOnce() -> io::Result<()>,
197    ) -> io::Result<FileIdentity> {
198        let file = self
199            .file
200            .as_ref()
201            .ok_or_else(|| io::Error::other("unpublished artifact descriptor was transferred"))?;
202        let observed = file_identity(file)?;
203        self.identity = Some(observed);
204        setup_check()?;
205        Ok(observed)
206    }
207
208    /// Borrow the private original candidate name for durability-owner validation.
209    pub fn temporary_name(&self) -> io::Result<&OsStr> {
210        self.candidate_names
211            .first()
212            .map(OsString::as_os_str)
213            .ok_or_else(|| io::Error::other("unpublished artifact has no temporary name"))
214    }
215
216    /// Open one sibling through the retained no-follow parent capability.
217    ///
218    /// # Errors
219    /// Returns an error when the child is absent, special, linked, or the
220    /// retained directory authority changed.
221    pub fn open_sibling(&self, name: &OsStr) -> io::Result<File> {
222        self.directory.open_child_file(name)
223    }
224
225    /// Transfer the retained data descriptor into its durable writer.
226    ///
227    /// # Errors
228    /// Returns an error if identity observation fails or the descriptor was
229    /// already transferred.
230    pub fn take_file(&mut self) -> io::Result<File> {
231        if self.identity.is_none() {
232            let file = self
233                .file
234                .as_ref()
235                .ok_or_else(|| io::Error::other("unpublished artifact file already transferred"))?;
236            self.identity = Some(file_identity(file)?);
237        }
238        self.file
239            .take()
240            .ok_or_else(|| io::Error::other("unpublished artifact file already transferred"))
241    }
242
243    /// Transfer exclusive unpublished cleanup to a new owner of this same file.
244    /// A published guard cannot relinquish its semantic commit obligations.
245    pub fn transfer_unpublished_owner(
246        self,
247        file: &File,
248    ) -> io::Result<(StableDirectory, OsString, FileIdentity)> {
249        let directory = self.directory.try_clone()?;
250        self.transfer_unpublished_owner_in(directory, file)
251    }
252
253    /// Transfer an already-owned retained parent without duplicating its handle.
254    pub fn transfer_unpublished_owner_in(
255        mut self,
256        directory: StableDirectory,
257        file: &File,
258    ) -> io::Result<(StableDirectory, OsString, FileIdentity)> {
259        if self.published || self.file.is_some() || self.candidate_names.len() != 1 {
260            return Err(io::Error::other(
261                "unpublished owner transfer invariants are incomplete",
262            ));
263        }
264        let expected = self.identity()?;
265        self.directory.revalidate_named()?;
266        directory.revalidate_named()?;
267        let name = self.candidate_names[0].clone();
268        let named = directory.open_child_file(&name)?;
269        if directory.identity() != self.directory.identity()
270            || file_identity(file)? != expected
271            || file_identity(&named)? != expected
272            || file_link_count(file)? != 1
273        {
274            return Err(io::Error::other(
275                "unpublished owner transfer identity changed",
276            ));
277        }
278        self.armed = false;
279        Ok((directory, name, expected))
280    }
281
282    /// Atomically install `target` while retaining cleanup ownership.
283    ///
284    /// # Errors
285    /// Returns an error when identity-safe installation fails.
286    pub fn install_child(&mut self, target: &OsStr) -> io::Result<()> {
287        validate_child_name(target)?;
288        let temporary = self
289            .candidate_names
290            .first()
291            .ok_or_else(|| io::Error::other("unpublished artifact has no temporary name"))?
292            .clone();
293        if !self.candidate_names.iter().any(|name| name == target) {
294            self.candidate_names.push(target.to_owned());
295        }
296        self.directory
297            .install_child(&temporary, self.identity()?, target)?;
298        self.published = true;
299        self.parent_synced = false;
300        Ok(())
301    }
302
303    /// Synchronize the retained parent directory while remaining armed.
304    ///
305    /// # Errors
306    /// Returns an error when the directory durability barrier fails.
307    pub fn sync_parent(&mut self) -> io::Result<()> {
308        self.sync_parent_with(StableDirectory::sync)
309    }
310
311    /// Acknowledge through the caller's durability owner, then mark this guard.
312    pub fn sync_parent_with(
313        &mut self,
314        acknowledge: impl FnOnce(&StableDirectory) -> io::Result<()>,
315    ) -> io::Result<()> {
316        acknowledge(&self.directory)?;
317        self.parent_synced = true;
318        Ok(())
319    }
320
321    /// Disarm cleanup after every publication invariant is complete.
322    ///
323    /// # Errors
324    /// Returns an error if the descriptor is still held by the guard or the
325    /// atomic publication and parent barrier have not both completed.
326    pub fn commit(mut self) -> io::Result<()> {
327        if self.file.is_some() || !self.published || !self.parent_synced {
328            return Err(io::Error::other(
329                "unpublished artifact commit invariants are incomplete",
330            ));
331        }
332        self.armed = false;
333        Ok(())
334    }
335
336    /// Remove every possible uncommitted name and synchronize the parent.
337    ///
338    /// Names that are absent or no longer identify the exclusively created
339    /// inode are left untouched.
340    ///
341    /// # Errors
342    /// Returns sanitized cleanup failure context after attempting every unlink
343    /// and the final parent-directory barrier.
344    pub fn cleanup(&mut self) -> io::Result<()> {
345        if !self.armed {
346            return Ok(());
347        }
348        let mut cleanup = Ok(());
349        let identity = match self.identity {
350            Some(identity) => Some(identity),
351            None => match self
352                .file
353                .as_ref()
354                .ok_or_else(|| io::Error::other("unpublished artifact descriptor is unavailable"))
355                .and_then(file_identity)
356            {
357                Ok(identity) => {
358                    self.identity = Some(identity);
359                    Some(identity)
360                }
361                Err(error) => {
362                    cleanup = append_sanitized_cleanup(
363                        cleanup,
364                        &error,
365                        "unpublished artifact identity recovery failed",
366                    );
367                    None
368                }
369            },
370        };
371        self.file.take();
372        for name in &self.candidate_names {
373            let Some(identity) = identity else {
374                break;
375            };
376            let removed = self.directory.open_child_file(name).and_then(|file| {
377                if file_identity(&file)? == identity {
378                    drop(file);
379                    self.directory.unlink_child_if_identity(name, identity)?;
380                }
381                Ok(())
382            });
383            match removed {
384                Ok(()) => {}
385                Err(error) if error.kind() == io::ErrorKind::NotFound => {}
386                Err(error) => {
387                    cleanup = append_sanitized_cleanup(
388                        cleanup,
389                        &error,
390                        "unpublished artifact unlink failed",
391                    );
392                }
393            }
394        }
395        cleanup = match self.directory.sync() {
396            Ok(()) => cleanup,
397            Err(error) => append_sanitized_cleanup(
398                cleanup,
399                &error,
400                "unpublished artifact directory synchronization failed",
401            ),
402        };
403        self.armed = false;
404        cleanup
405    }
406
407    /// Perform full identity-safe cleanup and its parent barrier, then run one
408    /// caller-supplied finalization check.
409    ///
410    /// # Errors
411    /// Returns sanitized cleanup or finalization context after both have been
412    /// attempted.
413    pub fn cleanup_checked(
414        &mut self,
415        finalization_check: impl FnOnce() -> io::Result<()>,
416    ) -> io::Result<()> {
417        let cleanup = self.cleanup();
418        match finalization_check() {
419            Ok(()) => cleanup,
420            Err(error) => append_sanitized_cleanup(
421                cleanup,
422                &error,
423                "unpublished artifact cleanup finalization failed",
424            ),
425        }
426    }
427}
428
429impl Drop for UnpublishedArtifactGuard {
430    fn drop(&mut self) {
431        let _ = self.cleanup();
432    }
433}
434
435fn append_sanitized_cleanup(
436    primary: io::Result<()>,
437    cleanup: &io::Error,
438    context: &'static str,
439) -> io::Result<()> {
440    let cleanup = io::Error::new(cleanup.kind(), context);
441    match primary {
442        Ok(()) => Err(cleanup),
443        Err(primary) => Err(io::Error::new(
444            primary.kind(),
445            format!("{primary}; {cleanup}"),
446        )),
447    }
448}
449
450impl StableDirectory {
451    /// Duplicate this retained directory capability without resolving its path again.
452    ///
453    /// # Errors
454    /// Returns an error if the retained directory identity changed or the OS
455    /// cannot duplicate its handle.
456    pub fn try_clone(&self) -> io::Result<Self> {
457        self.revalidate_named()?;
458        let clone = Self {
459            path: self.path.clone(),
460            file: self.file.try_clone()?,
461            identity: self.identity,
462        };
463        clone.revalidate_named()?;
464        Ok(clone)
465    }
466
467    /// Acquire a cooperative shared lock on this retained Unix directory inode.
468    ///
469    /// Windows directory handles cannot be byte-range locked; callers use a
470    /// retained regular coordination file there instead.
471    #[cfg(unix)]
472    pub fn lock_shared(&self) -> io::Result<()> {
473        <File as fs4::FileExt>::lock_shared(&self.file)
474    }
475
476    /// Acquire a cooperative exclusive lock on this retained Unix directory inode.
477    #[cfg(unix)]
478    pub fn lock_exclusive(&self) -> io::Result<()> {
479        <File as fs4::FileExt>::lock(&self.file)
480    }
481
482    /// Try to acquire a cooperative exclusive lock on this retained Unix directory inode.
483    #[cfg(unix)]
484    pub fn try_lock_exclusive(&self) -> io::Result<bool> {
485        match <File as fs4::FileExt>::try_lock(&self.file) {
486            Ok(()) => Ok(true),
487            Err(fs4::TryLockError::WouldBlock) => Ok(false),
488            Err(fs4::TryLockError::Error(error)) => Err(error),
489        }
490    }
491
492    /// Release this retained Unix directory inode's cooperative lock.
493    #[cfg(unix)]
494    pub fn unlock(&self) -> io::Result<()> {
495        <File as fs4::FileExt>::unlock(&self.file)
496    }
497
498    /// Open and retain a real directory at `path`.
499    pub fn open(path: &Path) -> io::Result<Self> {
500        let file = stable_open_directory(path)?;
501        let identity = file_identity(&file)?;
502        let directory = Self {
503            path: path.to_path_buf(),
504            file,
505            identity,
506        };
507        directory.revalidate_named()?;
508        Ok(directory)
509    }
510
511    /// Adopt an already retained handle and expected identity, validating both
512    /// the named path and the handle before issuing a directory capability.
513    pub fn from_retained_handle(
514        path: PathBuf,
515        handle: OpenedDirectoryHandle,
516        identity: FileIdentity,
517    ) -> Result<Self, DirectoryValidationError> {
518        let directory = Self {
519            path,
520            file: handle.file,
521            identity,
522        };
523        directory.revalidate_named_detailed()?;
524        Ok(directory)
525    }
526
527    /// Borrow the retained handle for native volume queries and cooperative locks.
528    /// Callers must revalidate around namespace-sensitive operations.
529    #[must_use]
530    pub fn as_file(&self) -> &File {
531        &self.file
532    }
533
534    /// Return the named path associated with this capability.
535    #[must_use]
536    pub fn path(&self) -> &Path {
537        &self.path
538    }
539
540    /// Transfer the policy-proven native handle for adoption by another capability.
541    #[must_use]
542    pub fn into_handle(self) -> OpenedDirectoryHandle {
543        OpenedDirectoryHandle { file: self.file }
544    }
545
546    /// Require that the named path and retained handle still identify this
547    /// ordinary directory, returning a typed failure stage for policy adapters.
548    pub fn revalidate_named_detailed(&self) -> Result<(), DirectoryValidationError> {
549        use DirectoryValidationStage as Stage;
550        let named = std::fs::symlink_metadata(&self.path)
551            .map_err(|error| DirectoryValidationError::new(Stage::NamedMetadata, error))?;
552        let retained = self
553            .file
554            .metadata()
555            .map_err(|error| DirectoryValidationError::new(Stage::RetainedMetadata, error))?;
556        if !named.is_dir() || is_link_or_reparse(&named) || !retained.is_dir() {
557            return Err(DirectoryValidationError::new(
558                Stage::IdentityChanged,
559                io::Error::other("stable directory path is linked or special"),
560            ));
561        }
562        #[cfg(unix)]
563        let named_identity = platform::metadata_identity(&named);
564        #[cfg(not(unix))]
565        let named_identity = path_identity(&self.path)
566            .map_err(|error| DirectoryValidationError::new(Stage::NamedIdentity, error))?;
567        if named_identity != self.identity {
568            return Err(DirectoryValidationError::new(
569                Stage::IdentityChanged,
570                io::Error::other("stable directory identity changed"),
571            ));
572        }
573        #[cfg(unix)]
574        let retained_identity = platform::metadata_identity(&retained);
575        #[cfg(not(unix))]
576        let retained_identity = file_identity(&self.file)
577            .map_err(|error| DirectoryValidationError::new(Stage::RetainedIdentity, error))?;
578        if retained_identity != self.identity {
579            return Err(DirectoryValidationError::new(
580                Stage::IdentityChanged,
581                io::Error::other("stable directory identity changed"),
582            ));
583        }
584        Ok(())
585    }
586
587    /// Require that the named path still identifies this retained directory.
588    pub fn revalidate_named(&self) -> io::Result<()> {
589        self.revalidate_named_detailed()
590            .map_err(DirectoryValidationError::into_io_error)
591    }
592
593    /// Open one real child directory relative to this capability.
594    pub fn open_child_directory(&self, name: &OsStr) -> io::Result<Self> {
595        validate_child_name(name)?;
596        self.revalidate_named()?;
597        let path = self.path.join(name);
598        let file = stable_open_child_directory(&self.file, &path, name)?;
599        let child = Self {
600            identity: file_identity(&file)?,
601            path,
602            file,
603        };
604        child.revalidate_named()?;
605        self.revalidate_named()?;
606        Ok(child)
607    }
608
609    /// Create one child directory if absent, then retain it.
610    pub fn create_child_directory(&self, name: &OsStr) -> io::Result<Self> {
611        validate_child_name(name)?;
612        self.revalidate_named()?;
613        stable_create_child_directory(&self.file, &self.path.join(name), name)?;
614        self.open_child_directory(name)
615    }
616
617    /// Open one regular child without following links or reparse points.
618    pub fn open_child_file(&self, name: &OsStr) -> io::Result<File> {
619        validate_child_name(name)?;
620        self.revalidate_named()?;
621        let path = self.path.join(name);
622        let file = stable_open_child_file(&self.file, &path, name, false)?;
623        validate_stable_child_file(&file, &path)?;
624        self.revalidate_named()?;
625        Ok(file)
626    }
627
628    /// Revalidate a retained regular child against its current named path.
629    ///
630    /// Performs the same fresh directory and child observations as opening
631    /// the child, without replacing the retained handle or its seek position.
632    pub fn revalidate_child_file(&self, name: &OsStr, file: &File) -> io::Result<()> {
633        validate_child_name(name)?;
634        self.revalidate_named()?;
635        validate_stable_child_file(file, &self.path.join(name))?;
636        self.revalidate_named()
637    }
638
639    /// Reopen an already-admitted private publication source with write access
640    /// for its file barrier and Windows delete sharing for its native rename.
641    /// Ordinary immutable readers retain their existing anti-delete sharing.
642    pub fn open_publishing_child_file(
643        &self,
644        name: &OsStr,
645        expected: FileIdentity,
646    ) -> io::Result<File> {
647        validate_child_name(name)?;
648        self.revalidate_named()?;
649        let path = self.path.join(name);
650        let file = stable_open_publishing_child_file(&self.file, &path, name)?;
651        validate_stable_child_file(&file, &path)?;
652        if file_identity(&file)? != expected || file_link_count(&file)? != 1 {
653            return Err(io::Error::other(
654                "publication source identity or link count changed",
655            ));
656        }
657        self.revalidate_named()?;
658        Ok(file)
659    }
660
661    /// Visit regular descendants through retained, no-follow directory handles.
662    ///
663    /// Every child is opened relative to its retained parent and revalidated
664    /// before it reaches `visit`. Links and non-regular objects are skipped.
665    ///
666    /// # Errors
667    /// Returns an error if traversal exceeds `remaining`, a retained identity
668    /// changes, directory enumeration fails, or `visit` fails. Targets without
669    /// descriptor-relative directory enumeration return `Unsupported`.
670    pub fn visit_regular_files(
671        &self,
672        remaining: &mut usize,
673        visit: &mut impl FnMut(&File) -> io::Result<()>,
674    ) -> io::Result<()> {
675        self.revalidate_named()?;
676        visit_regular_files_platform(self, remaining, visit)?;
677        self.revalidate_named()
678    }
679
680    /// Create one new regular child without following links or reparse points.
681    pub fn create_child_file(&self, name: &OsStr) -> io::Result<File> {
682        validate_child_name(name)?;
683        self.revalidate_named()?;
684        let path = self.path.join(name);
685        let file = stable_open_child_file(&self.file, &path, name, true)?;
686        validate_stable_child_file(&file, &path)?;
687        self.revalidate_named()?;
688        Ok(file)
689    }
690
691    /// Create a Windows CAS child with exclusive data-write authority.
692    ///
693    /// Readers may coexist, but no second writer can be admitted. The handle
694    /// retains the native authority needed for the irreversible seal.
695    #[cfg(windows)]
696    pub fn create_cas_child_file(&self, name: &OsStr) -> io::Result<WindowsCasWriter> {
697        validate_child_name(name)?;
698        self.revalidate_named()?;
699        let path = self.path.join(name);
700        let file = windows::create_cas_writer(&path)?;
701        validate_stable_child_file(&file, &path)?;
702        self.revalidate_named()?;
703        Ok(WindowsCasWriter {
704            identity: file_identity(&file)?,
705            file,
706        })
707    }
708
709    /// Convert an exact Windows CAS writer into an identity-matched sealed reader.
710    #[cfg(windows)]
711    pub fn seal_cas_child_file(
712        &self,
713        name: &OsStr,
714        writer: WindowsCasWriter,
715    ) -> io::Result<WindowsSealedCasFile> {
716        validate_child_name(name)?;
717        self.revalidate_named()?;
718        let path = self.path.join(name);
719        let expected = writer.identity;
720        if file_identity(&writer.file)? != expected || path_identity(&path)? != expected {
721            return Err(io::Error::other(
722                "CAS writer identity changed before sealing",
723            ));
724        }
725        windows::seal_cas_writer(&writer.file)?;
726        if file_identity(&writer.file)? != expected || path_identity(&path)? != expected {
727            return Err(io::Error::other(
728                "CAS writer identity changed while sealing",
729            ));
730        }
731        let bridge = windows::open_cas_bridge(&path)?;
732        if file_identity(&bridge)? != expected {
733            return Err(io::Error::other(
734                "CAS bridge identity changed while sealing",
735            ));
736        }
737        drop(writer.file);
738        let reader = windows::open_sealed_cas_reader(&path)?;
739        validate_stable_child_file(&reader, &path)?;
740        if file_identity(&reader)? != expected || path_identity(&path)? != expected {
741            return Err(io::Error::other(
742                "CAS identity changed while reopening sealed reader",
743            ));
744        }
745        drop(bridge);
746        self.revalidate_named()?;
747        Ok(WindowsSealedCasFile(reader))
748    }
749
750    /// Open a canonically sealed Windows CAS child while excluding writers.
751    #[cfg(windows)]
752    pub fn open_cas_child_file(&self, name: &OsStr) -> io::Result<WindowsSealedCasFile> {
753        validate_child_name(name)?;
754        self.revalidate_named()?;
755        let path = self.path.join(name);
756        let reader = windows::open_sealed_cas_reader(&path)?;
757        validate_stable_child_file(&reader, &path)?;
758        if !reader.metadata()?.permissions().readonly()
759            || !windows::has_canonical_cas_dacl(&reader)?
760        {
761            return Err(io::Error::new(
762                io::ErrorKind::PermissionDenied,
763                "CAS child is not canonically sealed",
764            ));
765        }
766        self.revalidate_named()?;
767        Ok(WindowsSealedCasFile(reader))
768    }
769
770    /// Retain an exclusive metadata handle for authenticating a legacy sealed CAS child.
771    #[cfg(windows)]
772    pub fn open_legacy_cas_child_for_adoption(
773        &self,
774        name: &OsStr,
775    ) -> io::Result<WindowsLegacyCasAdopter> {
776        validate_child_name(name)?;
777        self.revalidate_named()?;
778        let path = self.path.join(name);
779        let file = windows::open_legacy_cas_adopter(&path)?;
780        validate_stable_child_file(&file, &path)?;
781        let identity = file_identity(&file)?;
782        if path_identity(&path)? != identity {
783            return Err(io::Error::other("legacy CAS identity changed during open"));
784        }
785        self.revalidate_named()?;
786        Ok(WindowsLegacyCasAdopter { file, identity })
787    }
788
789    /// Canonically seal a retained legacy CAS child after caller authentication.
790    #[cfg(windows)]
791    pub fn adopt_legacy_cas_child(
792        &self,
793        name: &OsStr,
794        adopter: WindowsLegacyCasAdopter,
795    ) -> io::Result<WindowsSealedCasFile> {
796        validate_child_name(name)?;
797        self.revalidate_named()?;
798        let path = self.path.join(name);
799        windows::set_canonical_cas_dacl(&adopter.file)?;
800        let bridge = windows::open_cas_bridge(&path)?;
801        if file_identity(&bridge)? != adopter.identity {
802            return Err(io::Error::other("legacy CAS bridge identity changed"));
803        }
804        drop(adopter.file);
805        let reader = windows::open_sealed_cas_reader(&path)?;
806        if file_identity(&reader)? != adopter.identity || path_identity(&path)? != adopter.identity
807        {
808            return Err(io::Error::other(
809                "legacy CAS identity changed during adoption",
810            ));
811        }
812        drop(bridge);
813        self.revalidate_named()?;
814        Ok(WindowsSealedCasFile(reader))
815    }
816
817    /// Create a new regular child whose retained handle permits an atomic
818    /// namespace replacement while it remains open.
819    pub fn create_replaceable_child_file(&self, name: &OsStr) -> io::Result<File> {
820        validate_child_name(name)?;
821        self.revalidate_named()?;
822        let path = self.path.join(name);
823        let file = stable_open_replaceable_child_file(&self.file, &path, name)?;
824        validate_stable_child_file(&file, &path)?;
825        self.revalidate_named()?;
826        Ok(file)
827    }
828
829    /// Exclusively create one replaceable child and immediately bind cleanup
830    /// ownership to its retained descriptor identity.
831    ///
832    /// # Errors
833    /// Returns an error when creation, identity capture, or directory-capability
834    /// cloning fails. Setup failure removes the exact created inode when safe.
835    pub fn create_unpublished_replaceable_child(
836        &self,
837        name: &OsStr,
838    ) -> io::Result<UnpublishedArtifactGuard> {
839        // Clone the parent authority before creating the child. From the
840        // instant exclusive creation succeeds, no fallible setup remains
841        // outside the armed guard.
842        let directory = self.try_clone()?;
843        let file = self.create_replaceable_child_file(name)?;
844        Ok(UnpublishedArtifactGuard {
845            directory,
846            candidate_names: vec![name.to_owned()],
847            identity: None,
848            file: Some(file),
849            published: false,
850            parent_synced: false,
851            armed: true,
852        })
853    }
854
855    /// Open an existing regular child for read/write, or create it once.
856    pub fn open_or_create_child_file(&self, name: &OsStr) -> io::Result<File> {
857        validate_child_name(name)?;
858        self.revalidate_named()?;
859        let path = self.path.join(name);
860        let file = stable_open_or_create_child_file(&self.file, &path, name)?;
861        validate_stable_child_file(&file, &path)?;
862        self.revalidate_named()?;
863        Ok(file)
864    }
865
866    /// Enumerate child names while retaining this directory capability.
867    pub fn child_names(&self) -> io::Result<Vec<std::ffi::OsString>> {
868        self.revalidate_named()?;
869        stable_child_names(&self.file, &self.path)
870    }
871
872    /// Enumerate no more than `limit` child names from this retained directory.
873    /// Returns `InvalidData` instead of materializing an attacker-sized sibling
874    /// inventory when the bound is exceeded.
875    pub fn child_names_bounded(&self, limit: usize) -> io::Result<Vec<std::ffi::OsString>> {
876        self.revalidate_named()?;
877        stable_child_names_bounded(&self.file, &self.path, limit)
878    }
879
880    /// Create a hard link between retained source and destination directories.
881    pub fn link_child_into(
882        &self,
883        source_name: &OsStr,
884        source: &File,
885        expected_source: FileIdentity,
886        destination: &Self,
887        destination_name: &OsStr,
888    ) -> io::Result<(File, FileIdentity)> {
889        validate_child_name(source_name)?;
890        validate_child_name(destination_name)?;
891        self.revalidate_named()?;
892        destination.revalidate_named()?;
893        validate_stable_child_file(source, &self.path.join(source_name))?;
894        if file_identity(source)? != expected_source {
895            return Err(io::Error::other("hard-link source identity changed"));
896        }
897        stable_link_child(
898            &self.file,
899            &self.path,
900            source_name,
901            &destination.file,
902            &destination.path,
903            destination_name,
904        )?;
905        self.revalidate_named()?;
906        destination.revalidate_named()?;
907        let installed = destination.open_child_file(destination_name)?;
908        let installed_identity = file_identity(&installed)?;
909        if installed_identity != expected_source {
910            return Err(io::Error::other("hard-link destination identity mismatch"));
911        }
912        Ok((installed, installed_identity))
913    }
914
915    /// Remove a child under the caller's held cooperative exclusive lifecycle
916    /// guard, only while its current named identity matches `expected`.
917    pub fn unlink_child_if_identity(&self, name: &OsStr, expected: FileIdentity) -> io::Result<()> {
918        validate_child_name(name)?;
919        self.revalidate_named()?;
920        stable_unlink_child_if_identity(&self.file, &self.path, name, expected)?;
921        self.revalidate_named()
922    }
923
924    /// Remove one empty child directory only while its retained and named
925    /// identities still match. Callers must first authenticate and empty the
926    /// directory through the returned child capability.
927    pub fn remove_child_directory_if_identity(
928        &self,
929        name: &OsStr,
930        expected: FileIdentity,
931    ) -> io::Result<()> {
932        validate_child_name(name)?;
933        self.revalidate_named()?;
934        stable_remove_child_directory_if_identity(&self.file, &self.path, name, expected)?;
935        self.revalidate_named()
936    }
937
938    /// Atomically publish a retained temporary child as `target` within this
939    /// retained directory. Cooperative publishers must serialize the target.
940    pub fn replace_child(
941        &self,
942        temporary: &OsStr,
943        expected_temporary: FileIdentity,
944        target: &OsStr,
945    ) -> io::Result<()> {
946        self.replace_child_typed(temporary, expected_temporary, target)
947            .map_err(|error| io::Error::other(error.to_string()))
948    }
949
950    /// Replace one child while retaining native visibility uncertainty.
951    pub fn replace_child_typed(
952        &self,
953        temporary: &OsStr,
954        expected_temporary: FileIdentity,
955        target: &OsStr,
956    ) -> Result<(), ReplaceFileError> {
957        let prepare = || -> io::Result<bool> {
958            validate_child_name(temporary)?;
959            validate_child_name(target)?;
960            self.revalidate_named()?;
961            let temporary_file = self.open_child_file(temporary)?;
962            if file_identity(&temporary_file)? != expected_temporary
963                || file_link_count(&temporary_file)? != 1
964            {
965                return Err(io::Error::other(
966                    "atomic temporary child identity or link count changed",
967                ));
968            }
969            match self.open_child_file(target) {
970                Ok(_) => Ok(true),
971                Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
972                Err(error) => Err(error),
973            }
974        };
975        if prepare().map_err(ReplaceFileError::NotReplaced)? {
976            replace_file_platform(
977                &self.file,
978                temporary,
979                target,
980                Some(expected_temporary),
981                None,
982            )?;
983        } else {
984            install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))
985                .map_err(ReplaceFileError::StateUnknown)?;
986        }
987        self.validate_replaced_child(target, expected_temporary)
988            .map_err(ReplaceFileError::StateUnknown)
989    }
990
991    fn validate_replaced_child(&self, target: &OsStr, expected: FileIdentity) -> io::Result<()> {
992        self.revalidate_named()?;
993        let installed = self.open_child_file(target)?;
994        if file_identity(&installed)? != expected || file_link_count(&installed)? != 1 {
995            return Err(io::Error::other(
996                "replacement target identity or link count changed",
997            ));
998        }
999        Ok(())
1000    }
1001
1002    /// Atomically replace an authenticated prior child, which may share its inode
1003    /// with immutable payloads or active snapshots. The source must be private.
1004    /// Callers must authenticate the prior contents and serialize publishers;
1005    /// this operation checks identity and never writes to the prior inode.
1006    pub fn replace_authenticated_child(
1007        &self,
1008        temporary: &OsStr,
1009        expected_temporary: FileIdentity,
1010        target: &OsStr,
1011        expected_target: FileIdentity,
1012    ) -> io::Result<()> {
1013        self.replace_authenticated_child_typed(
1014            temporary,
1015            expected_temporary,
1016            target,
1017            expected_target,
1018        )
1019        .map_err(|error| io::Error::other(error.to_string()))
1020    }
1021
1022    /// Replace an authenticated prior child without losing native error state.
1023    pub fn replace_authenticated_child_typed(
1024        &self,
1025        temporary: &OsStr,
1026        expected_temporary: FileIdentity,
1027        target: &OsStr,
1028        expected_target: FileIdentity,
1029    ) -> Result<(), ReplaceFileError> {
1030        validate_child_name(temporary).map_err(ReplaceFileError::NotReplaced)?;
1031        validate_child_name(target).map_err(ReplaceFileError::NotReplaced)?;
1032        self.revalidate_named()
1033            .map_err(ReplaceFileError::NotReplaced)?;
1034        replace_file_platform(
1035            &self.file,
1036            temporary,
1037            target,
1038            Some(expected_temporary),
1039            Some(expected_target),
1040        )?;
1041        self.validate_replaced_child(target, expected_temporary)
1042            .map_err(ReplaceFileError::StateUnknown)
1043    }
1044
1045    /// Atomically move an authenticated temporary from another retained
1046    /// directory over this directory's exact authenticated target.
1047    ///
1048    /// The temporary must already be durably sealed: this renames it without
1049    /// write access, so it neither flushes nor needs to open its payload for
1050    /// writing. On Windows the caller must not retain its own handle to the
1051    /// temporary, and a retained reader of the target that denies delete
1052    /// sharing makes the replacement fail with nothing replaced.
1053    #[doc(hidden)]
1054    pub fn replace_authenticated_child_from(
1055        &self,
1056        source_directory: &Self,
1057        temporary: &OsStr,
1058        expected_temporary: FileIdentity,
1059        target: &OsStr,
1060        expected_target: FileIdentity,
1061    ) -> Result<(), ReplaceFileError> {
1062        validate_child_name(temporary).map_err(ReplaceFileError::NotReplaced)?;
1063        validate_child_name(target).map_err(ReplaceFileError::NotReplaced)?;
1064        source_directory
1065            .revalidate_named()
1066            .map_err(ReplaceFileError::NotReplaced)?;
1067        self.revalidate_named()
1068            .map_err(ReplaceFileError::NotReplaced)?;
1069        replace_file_from_platform(
1070            &source_directory.file,
1071            &self.file,
1072            temporary,
1073            target,
1074            Some(expected_temporary),
1075            Some(expected_target),
1076        )?;
1077        source_directory
1078            .revalidate_named()
1079            .map_err(ReplaceFileError::StateUnknown)?;
1080        self.validate_replaced_child(target, expected_temporary)
1081            .map_err(ReplaceFileError::StateUnknown)
1082    }
1083
1084    /// Atomically install a retained temporary child without replacing an
1085    /// existing target. This is the creation authority for durable control
1086    /// records whose first publication must never overwrite competing state.
1087    ///
1088    /// # Errors
1089    /// Returns an I/O error when either name is invalid, the retained source
1090    /// identity changed, the target already exists, or durable installation
1091    /// and identity revalidation fail.
1092    pub fn install_child(
1093        &self,
1094        temporary: &OsStr,
1095        expected_temporary: FileIdentity,
1096        target: &OsStr,
1097    ) -> io::Result<()> {
1098        validate_child_name(temporary)?;
1099        validate_child_name(target)?;
1100        self.revalidate_named()?;
1101        let temporary_file = self.open_child_file(temporary)?;
1102        if file_identity(&temporary_file)? != expected_temporary
1103            || file_link_count(&temporary_file)? != 1
1104        {
1105            return Err(io::Error::other(
1106                "atomic temporary child identity or link count changed",
1107            ));
1108        }
1109        drop(temporary_file);
1110        install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))?;
1111        self.revalidate_named()?;
1112        self.open_child_file(target).map(|_| ())
1113    }
1114
1115    /// Preserve whether create-only publication definitely did not install or
1116    /// reached a native visibility boundary whose result needs reconciliation.
1117    pub fn install_child_typed(
1118        &self,
1119        temporary: &OsStr,
1120        expected_temporary: FileIdentity,
1121        target: &OsStr,
1122    ) -> Result<(), ReplaceFileError> {
1123        let prepare = || -> io::Result<()> {
1124            validate_child_name(temporary)?;
1125            validate_child_name(target)?;
1126            self.revalidate_named()?;
1127            let file = self.open_child_file(temporary)?;
1128            if file_identity(&file)? != expected_temporary || file_link_count(&file)? != 1 {
1129                return Err(io::Error::other(
1130                    "atomic temporary child identity or link count changed",
1131                ));
1132            }
1133            Ok(())
1134        };
1135        prepare().map_err(ReplaceFileError::NotReplaced)?;
1136        install_new_file_platform(&self.file, temporary, target, Some(expected_temporary))
1137            .map_err(|error| {
1138                if error.kind() == io::ErrorKind::AlreadyExists {
1139                    ReplaceFileError::NotReplaced(error)
1140                } else {
1141                    ReplaceFileError::StateUnknown(error)
1142                }
1143            })?;
1144        self.validate_replaced_child(target, expected_temporary)
1145            .map_err(ReplaceFileError::StateUnknown)
1146    }
1147
1148    /// Flush this retained directory capability.
1149    pub fn sync(&self) -> io::Result<()> {
1150        self.revalidate_named()?;
1151        #[cfg(windows)]
1152        {
1153            let directory = stable_open_directory_for_sync(&self.path)?;
1154            if file_identity(&directory)? != self.identity {
1155                return Err(io::Error::other(
1156                    "stable directory identity changed before sync",
1157                ));
1158            }
1159            directory.observed_sync_all()?;
1160            self.revalidate_named()
1161        }
1162        #[cfg(not(windows))]
1163        {
1164            self.file.observed_sync_all()?;
1165            self.revalidate_named()
1166        }
1167    }
1168
1169    /// Return the retained native identity.
1170    #[must_use]
1171    pub fn identity(&self) -> FileIdentity {
1172        self.identity
1173    }
1174}
1175
1176/// Open a directory handle using the platform's no-follow and sharing policy.
1177/// The caller must validate directory kind and retained/named identity before
1178/// treating it as authority; prefer `StableDirectory::open` for a capability.
1179pub fn open_directory_handle(path: &Path) -> io::Result<OpenedDirectoryHandle> {
1180    stable_open_directory(path).map(|file| OpenedDirectoryHandle { file })
1181}
1182
1183fn validate_child_name(name: &OsStr) -> io::Result<()> {
1184    let path = Path::new(name);
1185    if name.is_empty()
1186        || path.is_absolute()
1187        || path.components().count() != 1
1188        || matches!(name.to_str(), Some("." | ".."))
1189    {
1190        return Err(io::Error::new(
1191            io::ErrorKind::InvalidInput,
1192            "invalid child name",
1193        ));
1194    }
1195    Ok(())
1196}
1197
1198fn validate_stable_child_file(file: &File, path: &Path) -> io::Result<()> {
1199    let metadata = file.metadata()?;
1200    let named = std::fs::symlink_metadata(path)?;
1201    if !metadata.is_file()
1202        || !named.is_file()
1203        || is_link_or_reparse(&named)
1204        || file_identity(file)? != path_identity(path)?
1205    {
1206        return Err(io::Error::other(
1207            "stable child is linked, special, or substituted",
1208        ));
1209    }
1210    Ok(())
1211}
1212
1213/// Native Windows volume facts needed by the durability admission policy.
1214#[cfg(windows)]
1215#[derive(Debug, Clone, PartialEq, Eq)]
1216pub struct WindowsVolumeInformation {
1217    /// Filesystem name reported by the mounted volume (`NTFS`, `ReFS`, ...).
1218    pub filesystem_name: String,
1219    /// Whether the volume reports the read-only filesystem flag.
1220    pub read_only: bool,
1221    /// Whether Windows classifies the volume root as a fixed local drive.
1222    pub fixed: bool,
1223}
1224
1225/// Create a durability-probe directory that is private to the current user.
1226///
1227/// Unix uses mode `0700`. Windows installs a protected DACL that grants full
1228/// access only to the owner, LocalSystem, and local administrators.
1229pub fn create_private_directory(path: &Path) -> io::Result<()> {
1230    create_private_directory_platform(path)
1231}
1232
1233/// Return the stable native volume/file identity of an open handle.
1234pub fn file_identity(file: &File) -> io::Result<FileIdentity> {
1235    file_identity_platform(file)
1236}
1237
1238/// Return logical and physically allocated bytes for a retained regular-file handle.
1239///
1240/// The descriptor is the sole authority: this function never resolves or reopens a
1241/// pathname. Unsupported platforms and native values that cannot be represented
1242/// safely fail closed.
1243pub fn file_space_usage(file: &File) -> io::Result<FileSpaceUsage> {
1244    file_space_usage_platform(file)
1245}
1246
1247/// Return the stable native volume/file identity of a non-followed path.
1248pub fn path_identity(path: &Path) -> io::Result<FileIdentity> {
1249    path_identity_platform(path)
1250}
1251
1252/// Return the native hard-link count of an open file handle.
1253pub fn file_link_count(file: &File) -> io::Result<u64> {
1254    file_link_count_platform(file)
1255}
1256
1257/// Return the native hard-link count of a non-followed path.
1258pub fn path_link_count(path: &Path) -> io::Result<u64> {
1259    path_link_count_platform(path)
1260}
1261
1262/// Query Windows volume facts from the native mount root containing `path`.
1263///
1264/// This accepts canonical extended-length paths such as `\\?\C:\...` and
1265/// follows mount-point boundaries through `GetVolumePathNameW`.
1266#[cfg(windows)]
1267pub fn windows_volume_information(path: &Path) -> io::Result<WindowsVolumeInformation> {
1268    windows::volume_information(path)
1269}
1270
1271/// Failure classification for an attempted atomic replacement.
1272#[derive(Debug)]
1273pub enum ReplaceFileError {
1274    /// The operating system rejected the operation and the open source handle
1275    /// plus both named identities were verified unchanged.
1276    NotReplaced(io::Error),
1277    /// The operating system reported failure after it may have moved or
1278    /// modified one of the named files. The caller must reconcile from
1279    /// authoritative persisted state.
1280    StateUnknown(io::Error),
1281}
1282
1283impl std::fmt::Display for ReplaceFileError {
1284    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1285        match self {
1286            Self::NotReplaced(error) => write!(formatter, "file was not replaced: {error}"),
1287            Self::StateUnknown(error) => {
1288                write!(
1289                    formatter,
1290                    "replacement state requires reconciliation: {error}"
1291                )
1292            }
1293        }
1294    }
1295}
1296
1297impl std::error::Error for ReplaceFileError {
1298    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
1299        Some(match self {
1300            Self::NotReplaced(error) | Self::StateUnknown(error) => error,
1301        })
1302    }
1303}
1304
1305/// Classify an OS-reported failed replacement from reconciled identities.
1306///
1307/// This is public for the deterministic fault oracle; publication callers use
1308/// [`replace_file`] directly.
1309#[doc(hidden)]
1310#[must_use]
1311pub fn classify_failed_replacement(
1312    error: io::Error,
1313    source_before: FileIdentity,
1314    target_before: FileIdentity,
1315    source_after: Option<FileIdentity>,
1316    target_after: Option<FileIdentity>,
1317) -> ReplaceFileError {
1318    if source_after == Some(source_before) && target_after == Some(target_before) {
1319        ReplaceFileError::NotReplaced(error)
1320    } else {
1321        ReplaceFileError::StateUnknown(error)
1322    }
1323}
1324
1325/// Atomically replace an existing regular file with another regular file in
1326/// the same directory.
1327///
1328/// Source contents must already be written. On Windows the implementation
1329/// reopens and flushes the source through a write-through handle before issuing
1330/// the NTFS namespace rename through that same handle. On POSIX the caller
1331/// remains responsible for the containing-directory durability barrier.
1332pub fn replace_file(
1333    directory: &File,
1334    source_name: &OsStr,
1335    target_name: &OsStr,
1336) -> Result<(), ReplaceFileError> {
1337    verify_single_component(source_name).map_err(ReplaceFileError::NotReplaced)?;
1338    verify_single_component(target_name).map_err(ReplaceFileError::NotReplaced)?;
1339    replace_file_platform(directory, source_name, target_name, None, None)
1340}
1341
1342/// Atomically install a new regular file without replacing an existing entry.
1343///
1344/// Windows uses the same flushed write-through source handle for the NTFS
1345/// namespace rename. POSIX callers remain responsible for directory `fsync`.
1346pub fn install_new_file(
1347    directory: &File,
1348    source_name: &OsStr,
1349    target_name: &OsStr,
1350) -> io::Result<()> {
1351    verify_single_component(source_name)?;
1352    verify_single_component(target_name)?;
1353    install_new_file_platform(directory, source_name, target_name, None)
1354}
1355
1356/// Atomically move a file or directory without replacing any existing destination.
1357///
1358/// This never copies across volumes. The caller owns source admission and the
1359/// containing-directory durability barrier after a successful namespace change.
1360pub fn rename_no_replace(source: &Path, destination: &Path) -> io::Result<()> {
1361    rename_no_replace_platform(source, destination)
1362}
1363
1364fn verify_single_component(name: &OsStr) -> io::Result<()> {
1365    let mut components = Path::new(name).components();
1366    if !matches!(components.next(), Some(std::path::Component::Normal(_)))
1367        || components.next().is_some()
1368    {
1369        return Err(io::Error::new(
1370            io::ErrorKind::InvalidInput,
1371            "filesystem operation requires one plain name",
1372        ));
1373    }
1374    Ok(())
1375}
1376
1377fn verify_regular_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
1378    if is_link_or_reparse(metadata) || !metadata.is_file() || link_count(metadata) != 1 {
1379        return Err(io::Error::other(
1380            "replacement path is not a regular non-link file",
1381        ));
1382    }
1383    Ok(())
1384}
1385
1386fn verify_space_usage_metadata(metadata: &std::fs::Metadata) -> io::Result<()> {
1387    if is_link_or_reparse(metadata) || !metadata.is_file() {
1388        return Err(io::Error::other(
1389            "space usage handle is not a regular non-reparse file",
1390        ));
1391    }
1392    Ok(())
1393}
1394
1395#[cfg(test)]
1396mod tests;
1397
1398/// Optional, process-wide diagnostic work counters.
1399pub mod observation;
1400pub use observation::ObservedSync;