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