Skip to main content

graphforge_filesystem/
platform.rs

1//! Platform-specific retained-directory, identity, and atomic-file operations.
2
3use std::ffi::OsStr;
4use std::fs::File;
5use std::io;
6use std::path::Path;
7
8#[cfg(any(unix, windows))]
9use super::file_identity;
10use super::{FileIdentity, FileSpaceUsage, ReplaceFileError, StableDirectory};
11#[cfg(windows)]
12use super::{create_private_directory, path_identity, windows};
13#[cfg(unix)]
14use super::{verify_regular_metadata, verify_space_usage_metadata};
15
16#[cfg(unix)]
17pub(super) fn stable_open_directory(path: &Path) -> io::Result<File> {
18    use rustix::fs::{Mode, OFlags};
19    rustix::fs::open(
20        path,
21        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
22        Mode::empty(),
23    )
24    .map(File::from)
25    .map_err(io::Error::from)
26}
27
28#[cfg(unix)]
29pub(super) fn visit_regular_files_platform(
30    directory: &StableDirectory,
31    remaining: &mut usize,
32    visit: &mut impl FnMut(&File) -> io::Result<()>,
33) -> io::Result<()> {
34    use std::os::unix::ffi::OsStrExt as _;
35
36    use rustix::fs::{AtFlags, Dir, FileType};
37
38    let mut entries = Dir::read_from(&directory.file).map_err(io::Error::from)?;
39    while let Some(entry) = entries.read() {
40        let entry = entry.map_err(io::Error::from)?;
41        let raw_name = entry.file_name().to_bytes();
42        if matches!(raw_name, b"." | b"..") {
43            continue;
44        }
45        if *remaining == 0 {
46            return Err(io::Error::other(
47                "descriptor-relative traversal exceeds entry bound",
48            ));
49        }
50        *remaining -= 1;
51        let name = OsStr::from_bytes(raw_name);
52        let mut file_type = entry.file_type();
53        if file_type == FileType::Unknown {
54            let stat = rustix::fs::statat(&directory.file, name, AtFlags::SYMLINK_NOFOLLOW)
55                .map_err(io::Error::from)?;
56            file_type = FileType::from_raw_mode(stat.st_mode);
57        }
58        if file_type.is_file() {
59            let file = directory.open_child_file(name)?;
60            visit(&file)?;
61        } else if file_type.is_dir() {
62            let child = directory.open_child_directory(name)?;
63            visit_regular_files_platform(&child, remaining, visit)?;
64        }
65    }
66    Ok(())
67}
68
69#[cfg(not(unix))]
70pub(super) fn visit_regular_files_platform(
71    _directory: &StableDirectory,
72    _remaining: &mut usize,
73    _visit: &mut impl FnMut(&File) -> io::Result<()>,
74) -> io::Result<()> {
75    Err(io::Error::new(
76        io::ErrorKind::Unsupported,
77        "descriptor-relative directory traversal is unsupported",
78    ))
79}
80
81#[cfg(unix)]
82pub(super) fn stable_open_child_directory(
83    parent: &File,
84    _path: &Path,
85    name: &OsStr,
86) -> io::Result<File> {
87    use rustix::fs::{Mode, OFlags};
88    rustix::fs::openat(
89        parent,
90        name,
91        OFlags::RDONLY | OFlags::DIRECTORY | OFlags::NOFOLLOW | OFlags::CLOEXEC,
92        Mode::empty(),
93    )
94    .map(File::from)
95    .map_err(io::Error::from)
96}
97
98#[cfg(unix)]
99pub(super) fn stable_create_child_directory(
100    parent: &File,
101    _path: &Path,
102    name: &OsStr,
103) -> io::Result<()> {
104    use rustix::fs::Mode;
105    match rustix::fs::mkdirat(parent, name, Mode::from_bits_truncate(0o700)) {
106        Ok(()) | Err(rustix::io::Errno::EXIST) => Ok(()),
107        Err(error) => Err(io::Error::from(error)),
108    }
109}
110
111#[cfg(unix)]
112pub(super) fn stable_open_child_file(
113    parent: &File,
114    _path: &Path,
115    name: &OsStr,
116    create_new: bool,
117) -> io::Result<File> {
118    use rustix::fs::{Mode, OFlags};
119    let flags = if create_new {
120        OFlags::RDWR | OFlags::CREATE | OFlags::EXCL
121    } else {
122        OFlags::RDONLY
123    } | OFlags::NOFOLLOW
124        | OFlags::NONBLOCK
125        | OFlags::CLOEXEC;
126    rustix::fs::openat(parent, name, flags, Mode::from_bits_truncate(0o600))
127        .map(File::from)
128        .map_err(io::Error::from)
129}
130
131#[cfg(unix)]
132pub(super) fn stable_open_publishing_child_file(
133    parent: &File,
134    _path: &Path,
135    name: &OsStr,
136) -> io::Result<File> {
137    use rustix::fs::{Mode, OFlags};
138    rustix::fs::openat(
139        parent,
140        name,
141        OFlags::RDWR | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
142        Mode::empty(),
143    )
144    .map(File::from)
145    .map_err(io::Error::from)
146}
147
148#[cfg(unix)]
149pub(super) fn stable_open_replaceable_child_file(
150    parent: &File,
151    path: &Path,
152    name: &OsStr,
153) -> io::Result<File> {
154    stable_open_child_file(parent, path, name, true)
155}
156
157#[cfg(unix)]
158pub(super) fn stable_open_or_create_child_file(
159    parent: &File,
160    path: &Path,
161    name: &OsStr,
162) -> io::Result<File> {
163    match stable_open_child_file(parent, path, name, true) {
164        Ok(file) => Ok(file),
165        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
166            use rustix::fs::{Mode, OFlags};
167            rustix::fs::openat(
168                parent,
169                name,
170                OFlags::RDWR | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
171                Mode::empty(),
172            )
173            .map(File::from)
174            .map_err(io::Error::from)
175        }
176        Err(error) => Err(error),
177    }
178}
179
180#[cfg(unix)]
181pub(super) fn stable_child_names(
182    parent: &File,
183    path: &Path,
184) -> io::Result<Vec<std::ffi::OsString>> {
185    stable_child_names_bounded(parent, path, usize::MAX)
186}
187
188#[cfg(unix)]
189pub(super) fn stable_child_names_bounded(
190    parent: &File,
191    _path: &Path,
192    limit: usize,
193) -> io::Result<Vec<std::ffi::OsString>> {
194    use std::os::unix::ffi::OsStrExt as _;
195    let directory = rustix::fs::Dir::read_from(parent).map_err(io::Error::from)?;
196    let names = directory
197        .map(|entry| {
198            let entry = entry.map_err(io::Error::from)?;
199            let name = OsStr::from_bytes(entry.file_name().to_bytes());
200            Ok(name.to_os_string())
201        })
202        .filter(|entry| {
203            !matches!(entry, Ok(name) if name == OsStr::new(".") || name == OsStr::new(".."))
204        })
205        .take(limit.saturating_add(1))
206        .collect::<io::Result<Vec<_>>>()?;
207    if names.len() > limit {
208        return Err(io::Error::new(
209            io::ErrorKind::InvalidData,
210            "stable directory child count exceeds bound",
211        ));
212    }
213    Ok(names)
214}
215
216#[cfg(unix)]
217pub(super) fn stable_link_child(
218    source: &File,
219    _source_path: &Path,
220    source_name: &OsStr,
221    destination: &File,
222    _destination_path: &Path,
223    destination_name: &OsStr,
224) -> io::Result<()> {
225    rustix::fs::linkat(
226        source,
227        source_name,
228        destination,
229        destination_name,
230        rustix::fs::AtFlags::empty(),
231    )
232    .map_err(io::Error::from)
233}
234
235#[cfg(unix)]
236pub(super) fn stable_unlink_child_if_identity(
237    parent: &File,
238    _path: &Path,
239    name: &OsStr,
240    expected: FileIdentity,
241) -> io::Result<()> {
242    use rustix::fs::{AtFlags, Mode, OFlags};
243    let opened = rustix::fs::openat(
244        parent,
245        name,
246        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
247        Mode::empty(),
248    )
249    .map(File::from)
250    .map_err(io::Error::from)?;
251    if !opened.metadata()?.is_file() {
252        return Err(io::Error::other("child is not a regular file"));
253    }
254    if file_identity(&opened)? != expected {
255        return Err(io::Error::other("child identity changed before unlink"));
256    }
257    let named =
258        rustix::fs::statat(parent, name, AtFlags::SYMLINK_NOFOLLOW).map_err(io::Error::from)?;
259    // `dev_t` is an opaque device bit pattern. It is unsigned on Linux and
260    // signed on Darwin, so preserving the bits requires a target-dependent
261    // no-op/sign cast that Clippy cannot express portably without allowances.
262    #[allow(clippy::cast_sign_loss, clippy::unnecessary_cast)]
263    let volume_serial = named.st_dev as u64;
264    let named_identity = FileIdentity {
265        volume_serial,
266        file_id: u128::from(named.st_ino).to_le_bytes(),
267    };
268    if named_identity != expected {
269        return Err(io::Error::other("child identity changed before unlink"));
270    }
271    rustix::fs::unlinkat(parent, name, AtFlags::empty()).map_err(io::Error::from)
272}
273
274#[cfg(unix)]
275pub(super) fn stable_remove_child_directory_if_identity(
276    parent: &File,
277    _path: &Path,
278    name: &OsStr,
279    expected: FileIdentity,
280) -> io::Result<()> {
281    use rustix::fs::{AtFlags, Mode, OFlags};
282    let opened = rustix::fs::openat(
283        parent,
284        name,
285        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::DIRECTORY | OFlags::CLOEXEC,
286        Mode::empty(),
287    )
288    .map(File::from)
289    .map_err(io::Error::from)?;
290    if file_identity(&opened)? != expected {
291        return Err(io::Error::other(
292            "child directory identity changed before removal",
293        ));
294    }
295    let named =
296        rustix::fs::statat(parent, name, AtFlags::SYMLINK_NOFOLLOW).map_err(io::Error::from)?;
297    #[allow(clippy::cast_sign_loss, clippy::unnecessary_cast)]
298    let volume_serial = named.st_dev as u64;
299    let named_identity = FileIdentity {
300        volume_serial,
301        file_id: u128::from(named.st_ino).to_le_bytes(),
302    };
303    if named_identity != expected {
304        return Err(io::Error::other(
305            "child directory identity changed before removal",
306        ));
307    }
308    rustix::fs::unlinkat(parent, name, AtFlags::REMOVEDIR).map_err(io::Error::from)
309}
310
311#[cfg(windows)]
312pub(super) fn stable_open_directory(path: &Path) -> io::Result<File> {
313    use std::os::windows::fs::OpenOptionsExt as _;
314    const FILE_SHARE_READ: u32 = 0x0000_0001;
315    const FILE_SHARE_WRITE: u32 = 0x0000_0002;
316    const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
317    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
318    std::fs::OpenOptions::new()
319        .read(true)
320        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
321        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
322        .open(path)
323}
324
325#[cfg(windows)]
326pub(super) fn stable_open_directory_for_sync(path: &Path) -> io::Result<File> {
327    use std::os::windows::fs::OpenOptionsExt as _;
328    const FILE_SHARE_READ: u32 = 0x0000_0001;
329    const FILE_SHARE_WRITE: u32 = 0x0000_0002;
330    const FILE_FLAG_BACKUP_SEMANTICS: u32 = 0x0200_0000;
331    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
332    std::fs::OpenOptions::new()
333        .write(true)
334        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
335        .custom_flags(FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OPEN_REPARSE_POINT)
336        .open(path)
337}
338
339#[cfg(windows)]
340pub(super) fn stable_open_child_directory(
341    _parent: &File,
342    path: &Path,
343    _name: &OsStr,
344) -> io::Result<File> {
345    stable_open_directory(path)
346}
347
348#[cfg(windows)]
349pub(super) fn stable_create_child_directory(
350    _parent: &File,
351    path: &Path,
352    _name: &OsStr,
353) -> io::Result<()> {
354    match create_private_directory(path) {
355        Ok(()) => Ok(()),
356        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => Ok(()),
357        Err(error) => Err(error),
358    }
359}
360
361#[cfg(windows)]
362pub(super) fn stable_open_child_file(
363    _parent: &File,
364    path: &Path,
365    _name: &OsStr,
366    create_new: bool,
367) -> io::Result<File> {
368    use std::os::windows::fs::OpenOptionsExt as _;
369    const FILE_SHARE_READ: u32 = 0x0000_0001;
370    const FILE_SHARE_WRITE: u32 = 0x0000_0002;
371    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
372    let mut options = std::fs::OpenOptions::new();
373    options
374        .read(true)
375        .write(create_new)
376        .create_new(create_new)
377        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
378        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT);
379    options.open(path)
380}
381
382#[cfg(windows)]
383pub(super) fn stable_open_publishing_child_file(
384    _parent: &File,
385    path: &Path,
386    _name: &OsStr,
387) -> io::Result<File> {
388    use std::os::windows::fs::OpenOptionsExt as _;
389    const FILE_SHARE_READ: u32 = 0x0000_0001;
390    const FILE_SHARE_WRITE: u32 = 0x0000_0002;
391    const FILE_SHARE_DELETE: u32 = 0x0000_0004;
392    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
393    std::fs::OpenOptions::new()
394        .read(true)
395        .write(true)
396        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
397        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
398        .open(path)
399}
400
401#[cfg(windows)]
402pub(super) fn stable_open_replaceable_child_file(
403    _parent: &File,
404    path: &Path,
405    _name: &OsStr,
406) -> io::Result<File> {
407    use std::os::windows::fs::OpenOptionsExt as _;
408    const FILE_SHARE_READ: u32 = 0x0000_0001;
409    const FILE_SHARE_WRITE: u32 = 0x0000_0002;
410    const FILE_SHARE_DELETE: u32 = 0x0000_0004;
411    const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
412    std::fs::OpenOptions::new()
413        .read(true)
414        .write(true)
415        .create_new(true)
416        .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
417        .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
418        .open(path)
419}
420
421#[cfg(windows)]
422pub(super) fn stable_open_or_create_child_file(
423    parent: &File,
424    path: &Path,
425    name: &OsStr,
426) -> io::Result<File> {
427    match stable_open_child_file(parent, path, name, true) {
428        Ok(file) => Ok(file),
429        Err(error) if error.kind() == io::ErrorKind::AlreadyExists => {
430            use std::os::windows::fs::OpenOptionsExt as _;
431            const FILE_SHARE_READ: u32 = 0x0000_0001;
432            const FILE_SHARE_WRITE: u32 = 0x0000_0002;
433            const FILE_FLAG_OPEN_REPARSE_POINT: u32 = 0x0020_0000;
434            std::fs::OpenOptions::new()
435                .read(true)
436                .write(true)
437                .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE)
438                .custom_flags(FILE_FLAG_OPEN_REPARSE_POINT)
439                .open(path)
440        }
441        Err(error) => Err(error),
442    }
443}
444
445#[cfg(windows)]
446pub(super) fn stable_child_names(
447    parent: &File,
448    path: &Path,
449) -> io::Result<Vec<std::ffi::OsString>> {
450    stable_child_names_bounded(parent, path, usize::MAX)
451}
452
453#[cfg(windows)]
454pub(super) fn stable_child_names_bounded(
455    _parent: &File,
456    path: &Path,
457    limit: usize,
458) -> io::Result<Vec<std::ffi::OsString>> {
459    let names = std::fs::read_dir(path)?
460        .map(|entry| entry.map(|entry| entry.file_name()))
461        .take(limit.saturating_add(1))
462        .collect::<io::Result<Vec<_>>>()?;
463    if names.len() > limit {
464        return Err(io::Error::new(
465            io::ErrorKind::InvalidData,
466            "stable directory child count exceeds bound",
467        ));
468    }
469    Ok(names)
470}
471
472#[cfg(windows)]
473pub(super) fn stable_link_child(
474    _source: &File,
475    source_path: &Path,
476    source_name: &OsStr,
477    _destination: &File,
478    destination_path: &Path,
479    destination_name: &OsStr,
480) -> io::Result<()> {
481    std::fs::hard_link(
482        source_path.join(source_name),
483        destination_path.join(destination_name),
484    )
485}
486
487#[cfg(windows)]
488pub(super) fn stable_unlink_child_if_identity(
489    _parent: &File,
490    path: &Path,
491    name: &OsStr,
492    expected: FileIdentity,
493) -> io::Result<()> {
494    let child = path.join(name);
495    windows::delete_file_by_handle(&child, expected)
496}
497
498#[cfg(windows)]
499pub(super) fn stable_remove_child_directory_if_identity(
500    _parent: &File,
501    path: &Path,
502    name: &OsStr,
503    expected: FileIdentity,
504) -> io::Result<()> {
505    let child = path.join(name);
506    let retained = stable_open_directory(&child)?;
507    if file_identity(&retained)? != expected || path_identity(&child)? != expected {
508        return Err(io::Error::other(
509            "child directory identity changed before removal",
510        ));
511    }
512    drop(retained);
513    std::fs::remove_dir(child)
514}
515
516#[cfg(all(not(unix), not(windows)))]
517pub(super) fn stable_open_directory(_path: &Path) -> io::Result<File> {
518    Err(io::Error::new(
519        io::ErrorKind::Unsupported,
520        "stable directories unsupported",
521    ))
522}
523
524#[cfg(all(not(unix), not(windows)))]
525pub(super) fn stable_open_child_directory(
526    _parent: &File,
527    _path: &Path,
528    _name: &OsStr,
529) -> io::Result<File> {
530    stable_open_directory(Path::new(""))
531}
532
533#[cfg(all(not(unix), not(windows)))]
534pub(super) fn stable_open_or_create_child_file(
535    _parent: &File,
536    _path: &Path,
537    _name: &OsStr,
538) -> io::Result<File> {
539    stable_open_directory(Path::new(""))
540}
541
542#[cfg(all(not(unix), not(windows)))]
543pub(super) fn stable_open_replaceable_child_file(
544    _parent: &File,
545    _path: &Path,
546    _name: &OsStr,
547) -> io::Result<File> {
548    stable_open_directory(Path::new(""))
549}
550
551#[cfg(all(not(unix), not(windows)))]
552pub(super) fn stable_child_names(
553    _parent: &File,
554    _path: &Path,
555) -> io::Result<Vec<std::ffi::OsString>> {
556    Err(io::Error::new(
557        io::ErrorKind::Unsupported,
558        "stable directories unsupported",
559    ))
560}
561
562#[cfg(all(not(unix), not(windows)))]
563pub(super) fn stable_child_names_bounded(
564    parent: &File,
565    path: &Path,
566    _limit: usize,
567) -> io::Result<Vec<std::ffi::OsString>> {
568    stable_child_names(parent, path)
569}
570
571#[cfg(all(not(unix), not(windows)))]
572pub(super) fn stable_link_child(
573    _source: &File,
574    _source_path: &Path,
575    _source_name: &OsStr,
576    _destination: &File,
577    _destination_path: &Path,
578    _destination_name: &OsStr,
579) -> io::Result<()> {
580    Err(io::Error::new(
581        io::ErrorKind::Unsupported,
582        "stable directories unsupported",
583    ))
584}
585
586#[cfg(all(not(unix), not(windows)))]
587pub(super) fn stable_unlink_child_if_identity(
588    _parent: &File,
589    _path: &Path,
590    _name: &OsStr,
591    _expected: FileIdentity,
592) -> io::Result<()> {
593    Err(io::Error::new(
594        io::ErrorKind::Unsupported,
595        "stable directories unsupported",
596    ))
597}
598
599#[cfg(all(not(unix), not(windows)))]
600pub(super) fn stable_remove_child_directory_if_identity(
601    _parent: &File,
602    _path: &Path,
603    _name: &OsStr,
604    _expected: FileIdentity,
605) -> io::Result<()> {
606    Err(io::Error::new(
607        io::ErrorKind::Unsupported,
608        "stable directories unsupported",
609    ))
610}
611
612#[cfg(all(not(unix), not(windows)))]
613pub(super) fn stable_create_child_directory(
614    _parent: &File,
615    _path: &Path,
616    _name: &OsStr,
617) -> io::Result<()> {
618    Err(io::Error::new(
619        io::ErrorKind::Unsupported,
620        "stable directories unsupported",
621    ))
622}
623
624#[cfg(all(not(unix), not(windows)))]
625pub(super) fn stable_open_child_file(
626    _parent: &File,
627    _path: &Path,
628    _name: &OsStr,
629    _create_new: bool,
630) -> io::Result<File> {
631    stable_open_directory(Path::new(""))
632}
633
634#[cfg(all(not(unix), not(windows)))]
635pub(super) fn stable_open_publishing_child_file(
636    _parent: &File,
637    _path: &Path,
638    _name: &OsStr,
639) -> io::Result<File> {
640    Err(io::Error::new(
641        io::ErrorKind::Unsupported,
642        "publication descriptors unsupported",
643    ))
644}
645
646#[cfg(any(target_vendor = "apple", target_os = "linux", target_os = "redox"))]
647pub(super) fn rename_no_replace_platform(source: &Path, destination: &Path) -> io::Result<()> {
648    rustix::fs::renameat_with(
649        rustix::fs::CWD,
650        source,
651        rustix::fs::CWD,
652        destination,
653        rustix::fs::RenameFlags::NOREPLACE,
654    )?;
655    Ok(())
656}
657
658#[cfg(windows)]
659pub(super) fn rename_no_replace_platform(source: &Path, destination: &Path) -> io::Result<()> {
660    windows::rename_no_replace(source, destination)
661}
662
663#[cfg(not(any(
664    target_vendor = "apple",
665    target_os = "linux",
666    target_os = "redox",
667    windows
668)))]
669pub(super) fn rename_no_replace_platform(_source: &Path, _destination: &Path) -> io::Result<()> {
670    Err(io::Error::new(
671        io::ErrorKind::Unsupported,
672        "atomic no-replace rename is unsupported",
673    ))
674}
675
676/// Whether metadata denotes a symlink or native reparse point.
677#[cfg(windows)]
678#[must_use]
679pub fn is_link_or_reparse(metadata: &std::fs::Metadata) -> bool {
680    use std::os::windows::fs::MetadataExt as _;
681    const FILE_ATTRIBUTE_REPARSE_POINT: u32 = 0x0000_0400;
682    metadata.file_type().is_symlink()
683        || (metadata.file_attributes() & FILE_ATTRIBUTE_REPARSE_POINT) != 0
684}
685
686/// Whether metadata denotes a symlink or native reparse point.
687#[cfg(not(windows))]
688#[must_use]
689pub fn is_link_or_reparse(metadata: &std::fs::Metadata) -> bool {
690    metadata.file_type().is_symlink()
691}
692
693#[cfg(unix)]
694pub(super) fn link_count(metadata: &std::fs::Metadata) -> u64 {
695    use std::os::unix::fs::MetadataExt as _;
696    metadata.nlink()
697}
698
699#[cfg(windows)]
700pub(super) fn link_count(metadata: &std::fs::Metadata) -> u64 {
701    let _ = metadata;
702    1
703}
704
705#[cfg(all(not(unix), not(windows)))]
706pub(super) fn link_count(_metadata: &std::fs::Metadata) -> u64 {
707    0
708}
709
710#[cfg(unix)]
711pub(super) fn replace_file_platform(
712    directory: &File,
713    source_name: &OsStr,
714    target_name: &OsStr,
715    expected_source: Option<FileIdentity>,
716    expected_target: Option<FileIdentity>,
717) -> Result<(), ReplaceFileError> {
718    use rustix::fs::{AtFlags, Mode, OFlags, openat, renameat, statat};
719
720    let source = openat(
721        directory,
722        source_name,
723        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
724        Mode::empty(),
725    )
726    .map(File::from)
727    .map_err(io::Error::from)
728    .map_err(ReplaceFileError::NotReplaced)?;
729    let target = openat(
730        directory,
731        target_name,
732        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
733        Mode::empty(),
734    )
735    .map(File::from)
736    .map_err(io::Error::from)
737    .map_err(ReplaceFileError::NotReplaced)?;
738    verify_regular_metadata(&source.metadata().map_err(ReplaceFileError::NotReplaced)?)
739        .map_err(ReplaceFileError::NotReplaced)?;
740    let target_metadata = target.metadata().map_err(ReplaceFileError::NotReplaced)?;
741    if let Some(expected) = expected_target {
742        verify_space_usage_metadata(&target_metadata).map_err(ReplaceFileError::NotReplaced)?;
743        if unix_identity(&target).map_err(ReplaceFileError::NotReplaced)? != expected {
744            return Err(ReplaceFileError::NotReplaced(io::Error::other(
745                "rename target differs from the authenticated expected identity",
746            )));
747        }
748    } else {
749        verify_regular_metadata(&target_metadata).map_err(ReplaceFileError::NotReplaced)?;
750    }
751    let source_identity = unix_identity(&source).map_err(ReplaceFileError::NotReplaced)?;
752    if expected_source.is_some_and(|expected| expected != source_identity) {
753        return Err(ReplaceFileError::NotReplaced(io::Error::other(
754            "rename source differs from the retained expected identity",
755        )));
756    }
757    renameat(directory, source_name, directory, target_name)
758        .map_err(io::Error::from)
759        .map_err(ReplaceFileError::NotReplaced)?;
760    let replaced = openat(
761        directory,
762        target_name,
763        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
764        Mode::empty(),
765    )
766    .map(File::from)
767    .map_err(io::Error::from)
768    .map_err(ReplaceFileError::StateUnknown)?;
769    verify_regular_metadata(
770        &replaced
771            .metadata()
772            .map_err(ReplaceFileError::StateUnknown)?,
773    )
774    .map_err(ReplaceFileError::StateUnknown)?;
775    if unix_identity(&replaced).map_err(ReplaceFileError::StateUnknown)? != source_identity
776        || statat(directory, source_name, AtFlags::SYMLINK_NOFOLLOW).is_ok()
777    {
778        return Err(ReplaceFileError::StateUnknown(io::Error::other(
779            "replacement success state did not reconcile",
780        )));
781    }
782    Ok(())
783}
784
785#[cfg(unix)]
786pub(super) fn replace_file_from_platform(
787    source_directory: &File,
788    target_directory: &File,
789    source_name: &OsStr,
790    target_name: &OsStr,
791    expected_source: Option<FileIdentity>,
792    expected_target: Option<FileIdentity>,
793) -> Result<(), ReplaceFileError> {
794    use rustix::fs::{AtFlags, Mode, OFlags, openat, renameat, statat};
795
796    let source = openat(
797        source_directory,
798        source_name,
799        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
800        Mode::empty(),
801    )
802    .map(File::from)
803    .map_err(io::Error::from)
804    .map_err(ReplaceFileError::NotReplaced)?;
805    let target = openat(
806        target_directory,
807        target_name,
808        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
809        Mode::empty(),
810    )
811    .map(File::from)
812    .map_err(io::Error::from)
813    .map_err(ReplaceFileError::NotReplaced)?;
814    verify_regular_metadata(&source.metadata().map_err(ReplaceFileError::NotReplaced)?)
815        .map_err(ReplaceFileError::NotReplaced)?;
816    let target_metadata = target.metadata().map_err(ReplaceFileError::NotReplaced)?;
817    verify_space_usage_metadata(&target_metadata).map_err(ReplaceFileError::NotReplaced)?;
818    let source_identity = unix_identity(&source).map_err(ReplaceFileError::NotReplaced)?;
819    let target_identity = unix_identity(&target).map_err(ReplaceFileError::NotReplaced)?;
820    if expected_source.is_some_and(|expected| expected != source_identity)
821        || expected_target.is_some_and(|expected| expected != target_identity)
822    {
823        return Err(ReplaceFileError::NotReplaced(io::Error::other(
824            "cross-directory replacement identity changed",
825        )));
826    }
827    renameat(source_directory, source_name, target_directory, target_name)
828        .map_err(io::Error::from)
829        .map_err(ReplaceFileError::NotReplaced)?;
830    let replaced = openat(
831        target_directory,
832        target_name,
833        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
834        Mode::empty(),
835    )
836    .map(File::from)
837    .map_err(io::Error::from)
838    .map_err(ReplaceFileError::StateUnknown)?;
839    if unix_identity(&replaced).map_err(ReplaceFileError::StateUnknown)? != source_identity
840        || statat(source_directory, source_name, AtFlags::SYMLINK_NOFOLLOW).is_ok()
841    {
842        return Err(ReplaceFileError::StateUnknown(io::Error::other(
843            "cross-directory replacement success state did not reconcile",
844        )));
845    }
846    Ok(())
847}
848
849#[cfg(unix)]
850fn unix_identity(file: &File) -> io::Result<FileIdentity> {
851    use std::os::unix::fs::MetadataExt as _;
852    let metadata = file.metadata()?;
853    Ok(FileIdentity {
854        volume_serial: metadata.dev(),
855        file_id: u128::from(metadata.ino()).to_le_bytes(),
856    })
857}
858
859#[cfg(unix)]
860pub(super) fn file_identity_platform(file: &File) -> io::Result<FileIdentity> {
861    unix_identity(file)
862}
863
864#[cfg(unix)]
865pub(super) fn file_space_usage_platform(file: &File) -> io::Result<FileSpaceUsage> {
866    use std::os::unix::fs::MetadataExt as _;
867
868    let metadata = file.metadata()?;
869    verify_space_usage_metadata(&metadata)?;
870    let allocated_bytes = metadata
871        .blocks()
872        .checked_mul(512)
873        .ok_or_else(|| io::Error::other("allocated file byte count overflowed u64"))?;
874    Ok(FileSpaceUsage {
875        logical_bytes: metadata.len(),
876        allocated_bytes,
877    })
878}
879
880#[cfg(unix)]
881pub(super) fn path_identity_platform(path: &Path) -> io::Result<FileIdentity> {
882    use std::os::unix::fs::MetadataExt as _;
883    let metadata = std::fs::symlink_metadata(path)?;
884    Ok(FileIdentity {
885        volume_serial: metadata.dev(),
886        file_id: u128::from(metadata.ino()).to_le_bytes(),
887    })
888}
889
890#[cfg(unix)]
891pub(super) fn file_link_count_platform(file: &File) -> io::Result<u64> {
892    use std::os::unix::fs::MetadataExt as _;
893    Ok(file.metadata()?.nlink())
894}
895
896#[cfg(unix)]
897pub(super) fn path_link_count_platform(path: &Path) -> io::Result<u64> {
898    use std::os::unix::fs::MetadataExt as _;
899    Ok(std::fs::symlink_metadata(path)?.nlink())
900}
901
902#[cfg(unix)]
903pub(super) fn create_private_directory_platform(path: &Path) -> io::Result<()> {
904    use std::os::unix::fs::DirBuilderExt as _;
905
906    let mut builder = std::fs::DirBuilder::new();
907    builder.mode(0o700).create(path)
908}
909
910#[cfg(windows)]
911pub(super) fn file_identity_platform(file: &File) -> io::Result<FileIdentity> {
912    windows::file_identity(file)
913}
914
915#[cfg(windows)]
916pub(super) fn file_space_usage_platform(file: &File) -> io::Result<FileSpaceUsage> {
917    windows::file_space_usage(file)
918}
919
920#[cfg(windows)]
921pub(super) fn path_identity_platform(path: &Path) -> io::Result<FileIdentity> {
922    windows::identity(path)
923}
924
925#[cfg(windows)]
926pub(super) fn file_link_count_platform(file: &File) -> io::Result<u64> {
927    windows::link_count(file)
928}
929
930#[cfg(windows)]
931pub(super) fn path_link_count_platform(path: &Path) -> io::Result<u64> {
932    windows::path_link_count(path)
933}
934
935#[cfg(unix)]
936pub(super) fn install_new_file_platform(
937    directory: &File,
938    source_name: &OsStr,
939    target_name: &OsStr,
940    expected_source: Option<FileIdentity>,
941) -> io::Result<()> {
942    use rustix::fs::{Mode, OFlags, RenameFlags, openat, renameat_with};
943
944    let source = openat(
945        directory,
946        source_name,
947        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
948        Mode::empty(),
949    )
950    .map(File::from)
951    .map_err(io::Error::from)?;
952    verify_regular_metadata(&source.metadata()?)?;
953    let source_identity = unix_identity(&source)?;
954    if expected_source.is_some_and(|expected| expected != source_identity) {
955        return Err(io::Error::other(
956            "rename source differs from the retained expected identity",
957        ));
958    }
959    renameat_with(
960        directory,
961        source_name,
962        directory,
963        target_name,
964        RenameFlags::NOREPLACE,
965    )
966    .map_err(io::Error::from)?;
967    let installed = openat(
968        directory,
969        target_name,
970        OFlags::RDONLY | OFlags::NOFOLLOW | OFlags::NONBLOCK | OFlags::CLOEXEC,
971        Mode::empty(),
972    )
973    .map(File::from)
974    .map_err(io::Error::from)?;
975    verify_regular_metadata(&installed.metadata()?)?;
976    if unix_identity(&installed)? != source_identity {
977        return Err(io::Error::other("atomic creation state did not reconcile"));
978    }
979    Ok(())
980}
981
982#[cfg(windows)]
983pub(super) fn replace_file_platform(
984    directory: &File,
985    source_name: &OsStr,
986    target_name: &OsStr,
987    expected_source: Option<FileIdentity>,
988    expected_target: Option<FileIdentity>,
989) -> Result<(), ReplaceFileError> {
990    windows::replace_file(
991        directory,
992        source_name,
993        target_name,
994        expected_source,
995        expected_target,
996    )
997}
998
999#[cfg(windows)]
1000pub(super) fn replace_file_from_platform(
1001    source_directory: &File,
1002    target_directory: &File,
1003    source_name: &OsStr,
1004    target_name: &OsStr,
1005    expected_source: Option<FileIdentity>,
1006    expected_target: Option<FileIdentity>,
1007) -> Result<(), ReplaceFileError> {
1008    windows::replace_file_from(
1009        source_directory,
1010        target_directory,
1011        source_name,
1012        target_name,
1013        expected_source,
1014        expected_target,
1015    )
1016}
1017
1018#[cfg(windows)]
1019pub(super) fn install_new_file_platform(
1020    directory: &File,
1021    source_name: &OsStr,
1022    target_name: &OsStr,
1023    expected_source: Option<FileIdentity>,
1024) -> io::Result<()> {
1025    // The native handle-scoped rename is the race-free no-replace authority;
1026    // identity reconciliation supplies a stable AlreadyExists class.
1027    windows::install_new_file(directory, source_name, target_name, expected_source)
1028}
1029
1030#[cfg(windows)]
1031pub(super) fn create_private_directory_platform(path: &Path) -> io::Result<()> {
1032    windows::create_private_directory(path)
1033}
1034
1035#[cfg(all(not(unix), not(windows)))]
1036pub(super) fn replace_file_platform(
1037    _directory: &File,
1038    _source_name: &OsStr,
1039    _target_name: &OsStr,
1040    _expected_source: Option<FileIdentity>,
1041    _expected_target: Option<FileIdentity>,
1042) -> Result<(), ReplaceFileError> {
1043    Err(ReplaceFileError::NotReplaced(io::Error::new(
1044        io::ErrorKind::Unsupported,
1045        "atomic replacement is unsupported on this platform",
1046    )))
1047}
1048
1049#[cfg(all(not(unix), not(windows)))]
1050pub(super) fn replace_file_from_platform(
1051    _source_directory: &File,
1052    _target_directory: &File,
1053    _source_name: &OsStr,
1054    _target_name: &OsStr,
1055    _expected_source: Option<FileIdentity>,
1056    _expected_target: Option<FileIdentity>,
1057) -> Result<(), ReplaceFileError> {
1058    Err(ReplaceFileError::NotReplaced(io::Error::new(
1059        io::ErrorKind::Unsupported,
1060        "cross-directory atomic replacement is unsupported on this platform",
1061    )))
1062}
1063
1064#[cfg(all(not(unix), not(windows)))]
1065pub(super) fn install_new_file_platform(
1066    _directory: &File,
1067    _source_name: &OsStr,
1068    _target_name: &OsStr,
1069    _expected_source: Option<FileIdentity>,
1070) -> io::Result<()> {
1071    Err(io::Error::new(
1072        io::ErrorKind::Unsupported,
1073        "atomic creation is unsupported on this platform",
1074    ))
1075}
1076
1077#[cfg(all(not(unix), not(windows)))]
1078pub(super) fn create_private_directory_platform(_path: &Path) -> io::Result<()> {
1079    Err(io::Error::new(
1080        io::ErrorKind::Unsupported,
1081        "private directory creation is unsupported on this platform",
1082    ))
1083}
1084
1085#[cfg(all(not(unix), not(windows)))]
1086pub(super) fn file_identity_platform(_file: &File) -> io::Result<FileIdentity> {
1087    Err(io::Error::new(
1088        io::ErrorKind::Unsupported,
1089        "identity unsupported",
1090    ))
1091}
1092
1093#[cfg(all(not(unix), not(windows)))]
1094pub(super) fn file_space_usage_platform(_file: &File) -> io::Result<FileSpaceUsage> {
1095    Err(io::Error::new(
1096        io::ErrorKind::Unsupported,
1097        "allocated-byte measurement is unsupported on this platform",
1098    ))
1099}
1100
1101#[cfg(all(not(unix), not(windows)))]
1102pub(super) fn path_identity_platform(_path: &Path) -> io::Result<FileIdentity> {
1103    Err(io::Error::new(
1104        io::ErrorKind::Unsupported,
1105        "identity unsupported",
1106    ))
1107}
1108
1109#[cfg(all(not(unix), not(windows)))]
1110pub(super) fn file_link_count_platform(_file: &File) -> io::Result<u64> {
1111    Err(io::Error::new(
1112        io::ErrorKind::Unsupported,
1113        "link count unsupported",
1114    ))
1115}
1116
1117#[cfg(all(not(unix), not(windows)))]
1118pub(super) fn path_link_count_platform(_path: &Path) -> io::Result<u64> {
1119    Err(io::Error::new(
1120        io::ErrorKind::Unsupported,
1121        "link count unsupported",
1122    ))
1123}
1124
1125// Unix metadata already contains the native identity from this observation.
1126#[cfg(unix)]
1127pub(super) fn metadata_identity(metadata: &std::fs::Metadata) -> FileIdentity {
1128    use std::os::unix::fs::MetadataExt as _;
1129    FileIdentity {
1130        volume_serial: metadata.dev(),
1131        file_id: u128::from(metadata.ino()).to_le_bytes(),
1132    }
1133}