1use 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 #[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#[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#[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 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#[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}