1#[cfg(unix)]
4use super::flush_batch::BatchFolder;
5use super::{FileName, FlushBatch, StoreReadLock};
6use std::fs;
7use std::io::{self, Read, Write};
8use std::path::{Path, PathBuf};
9use std::sync::Arc;
10
11#[derive(Debug, thiserror::Error)]
13pub enum FileError {
14 #[error("{operation} {}: {source}", path.display())]
16 Io {
17 operation: &'static str,
19 path: PathBuf,
21 #[source]
23 source: Box<dyn std::error::Error + Send + Sync>,
24 },
25 #[error("sync directory after replacing {}: {source}", path.display())]
27 AfterReplace {
28 path: PathBuf,
30 #[source]
32 source: Box<dyn std::error::Error + Send + Sync>,
33 },
34 #[error("sync directory after removing {}: {source}", path.display())]
36 AfterRemove {
37 path: PathBuf,
39 #[source]
41 source: Box<dyn std::error::Error + Send + Sync>,
42 },
43 #[error("{operation}; removing temporary file failed: {cleanup}")]
45 Cleanup {
46 #[source]
48 operation: Box<FileError>,
49 cleanup: Box<dyn std::error::Error + Send + Sync>,
51 },
52 #[error("{}: the device is locked, and the file is protected until it unlocks", path.display())]
56 DeviceLocked {
57 path: PathBuf,
60 },
61 #[error(transparent)]
64 Shutdown(#[from] crate::task::Shutdown),
65 #[error(transparent)]
68 Bug(#[from] crate::task::Bug),
69}
70
71impl crate::task::CarriesBug for FileError {
72 fn bug(&self) -> Option<&crate::task::Bug> {
73 match self {
74 Self::Bug(bug) => Some(bug),
75 Self::Cleanup { operation, .. } => operation.bug(),
76 _ => None,
77 }
78 }
79}
80
81impl From<crate::task::TaskFailed> for FileError {
82 fn from(failed: crate::task::TaskFailed) -> Self {
83 failed.into_error()
84 }
85}
86
87impl FileError {
88 pub fn installed_new_bytes(&self) -> bool {
90 matches!(self, Self::AfterReplace { .. })
91 }
92
93 pub(crate) fn at(operation: &'static str, path: &Path, source: io::Error) -> Self {
94 Self::Io { operation, path: path.to_owned(), source: Box::new(source) }
95 }
96}
97
98#[derive(Clone, Debug)]
101pub struct AtomicFile {
102 path: PathBuf,
103}
104
105impl AtomicFile {
106 pub(crate) fn new(path: PathBuf) -> Self {
107 Self { path }
108 }
109
110 pub fn read_optional(&self) -> Result<Option<Vec<u8>>, FileError> {
113 read_optional(&self.path)
114 }
115
116 pub fn open_reader(&self) -> Result<super::FileReader, super::ObservationError> {
118 super::FileReader::open(&self.path)
119 }
120
121 pub fn replace(&self, bytes: &[u8]) -> Result<(), FileError> {
131 install(prepare(&self.path, bytes, &own_temporaries(&self.path))?, &self.path)
132 }
133
134 pub fn write_recorded(&self, bytes: &[u8]) -> Result<(), FileError> {
140 let path = self.resolved()?;
141 #[cfg(any(test, feature = "test-utils"))]
142 super::reached::reached(&path);
143 let mut file = owned_options()
144 .create(true)
145 .truncate(true)
146 .open(&path)
147 .map_err(|source| FileError::at("create owned file", &path, source))?;
148 let regular = file
149 .metadata()
150 .map_err(|source| FileError::at("inspect owned file", &path, source))?
151 .is_file();
152 if !regular {
153 return Err(FileError::at(
154 "create owned file",
155 &path,
156 io::Error::new(io::ErrorKind::InvalidInput, "a regular file is required"),
157 ));
158 }
159 file.write_all(bytes)
160 .and_then(|()| sync_file(&file))
161 .map_err(|source| FileError::at("write owned file", &path, source))?;
162 #[cfg(unix)]
163 sync_directory(parent(&path))
164 .map_err(|source| FileError::at("sync owned file directory", &path, source))?;
165 Ok(())
166 }
167
168 pub fn overwrite_at(&self, offset: u64, bytes: &[u8]) -> Result<(), super::ObservationError> {
174 let path = self.resolved()?;
175 #[cfg(any(test, feature = "test-utils"))]
176 super::reached::reached(&path);
177 let file = owned_options()
178 .open(&path)
179 .map_err(|error| super::ObservationError::at("open owned file", &path, error))?;
180 let metadata =
181 file.metadata().map_err(|source| FileError::at("inspect owned file", &path, source))?;
182 if !metadata.is_file()
183 || offset.checked_add(bytes.len() as u64).is_none_or(|end| end > metadata.len())
184 {
185 return Err(FileError::at(
186 "overwrite owned file",
187 &path,
188 io::Error::new(io::ErrorKind::InvalidInput, "a range within a regular file"),
189 )
190 .into());
191 }
192 write_all_at(&file, offset, bytes)
195 .and_then(|()| file.sync_all())
196 .map_err(|source| FileError::at("overwrite owned file", &path, source))?;
197 Ok(())
198 }
199
200 pub fn set_len(&self, length: u64) -> Result<(), super::ObservationError> {
203 let path = self.resolved()?;
204 #[cfg(any(test, feature = "test-utils"))]
205 super::reached::reached(&path);
206 let file = owned_options()
207 .open(&path)
208 .map_err(|error| super::ObservationError::at("open owned file", &path, error))?;
209 let regular =
210 file.metadata().map_err(|source| FileError::at("inspect owned file", &path, source))?;
211 if !regular.is_file() {
212 return Err(FileError::at(
213 "resize owned file",
214 &path,
215 io::Error::new(io::ErrorKind::InvalidInput, "a regular file is required"),
216 )
217 .into());
218 }
219 file.set_len(length)
222 .and_then(|()| file.sync_all())
223 .map_err(|source| FileError::at("resize owned file", &path, source))?;
224 Ok(())
225 }
226
227 fn resolved(&self) -> Result<PathBuf, FileError> {
230 let directory = fs::canonicalize(parent(&self.path))
231 .map_err(|source| FileError::at("resolve parent directory", &self.path, source))?;
232 let name = self.path.file_name().ok_or_else(|| {
233 FileError::at(
234 "create owned file",
235 &self.path,
236 io::Error::new(io::ErrorKind::InvalidInput, "file name required"),
237 )
238 })?;
239 Ok(directory.join(name))
240 }
241
242 pub fn remove(&self) -> Result<(), FileError> {
246 remove(&self.path)
247 }
248
249 pub fn remove_with_temporaries(&self) -> Result<(), FileError> {
254 let prefix = own_temporaries(&self.path);
255 remove_named(parent(&self.path), |name| {
256 let random = name.strip_prefix(prefix.as_bytes());
257 random.is_some_and(|random| random.len() == TEMPORARY_RANDOM)
258 })?;
259 remove(&self.path)
260 }
261
262 #[cfg(any(test, feature = "test-utils"))]
265 pub fn leave_temporary(&self, bytes: &[u8]) -> PathBuf {
266 let temp = prepare(&self.path, bytes, &own_temporaries(&self.path)).unwrap();
267 temp.keep().expect("a temporary file can be kept").1
268 }
269}
270
271pub struct FileWriter {
276 file: Arc<tokio::sync::Mutex<LockedWriter>>,
277 path: PathBuf,
278 bugs: crate::task::BugSinkRef,
280}
281struct LockedWriter {
282 file: fs::File,
283 lock: Option<Arc<StoreReadLock>>,
286}
287
288#[derive(Clone)]
293pub struct WriterFolder {
294 path: PathBuf,
295 lock: Arc<StoreReadLock>,
296 bugs: crate::task::BugSinkRef,
298}
299
300impl WriterFolder {
301 pub(super) fn open(
305 path: &Path,
306 lock: Arc<StoreReadLock>,
307 bugs: crate::task::BugSinkRef,
308 ) -> Result<Self, FileError> {
309 let path = fs::canonicalize(path)
310 .map_err(|source| FileError::at("resolve parent directory", path, source))?;
311 Ok(Self { path, lock, bugs })
312 }
313
314 pub fn create(&self, name: &FileName) -> Result<FileWriter, FileError> {
317 let path = self.path.join(name.as_str());
318 let file = create_new(&path)
319 .map_err(|source| FileError::at("create owned file", &path, source))?;
320 Ok(FileWriter::new(file, path, Some(self.lock.clone()), self.bugs.clone()))
321 }
322
323 pub fn write_whole(
328 &self,
329 name: &FileName,
330 bytes: &[u8],
331 batch: &mut FlushBatch,
332 ) -> Result<(), FileError> {
333 let path = self.path.join(name.as_str());
334 let mut file = create_new(&path)
335 .map_err(|source| FileError::at("create owned file", &path, source))?;
336 file.write_all(bytes)
337 .map_err(|source| FileError::at("append owned file", &path, source))?;
338 sync_file(&file).map_err(|source| FileError::at("sync owned file", &path, source))?;
339 #[cfg(unix)]
340 batch.add(
341 BatchFolder::open(self.path.clone(), Some(self.lock.clone()))
342 .map_err(|source| FileError::at("sync owned file", &path, source))?,
343 );
344 #[cfg(windows)]
345 batch.add();
346 Ok(())
347 }
348}
349
350impl FileWriter {
351 fn new(
352 file: fs::File,
353 path: PathBuf,
354 lock: Option<Arc<StoreReadLock>>,
355 bugs: crate::task::BugSinkRef,
356 ) -> Self {
357 Self { file: Arc::new(tokio::sync::Mutex::new(LockedWriter { file, lock })), path, bugs }
358 }
359
360 pub(super) fn resume(
365 file: &AtomicFile,
366 lock: StoreReadLock,
367 bugs: crate::task::BugSinkRef,
368 length: u64,
369 ) -> Result<Self, FileError> {
370 use std::io::Seek;
371 let path = file.resolved()?;
372 let mut file = owned_options()
373 .open(&path)
374 .map_err(|source| FileError::at("reopen owned file", &path, source))?;
375 let metadata =
376 file.metadata().map_err(|source| FileError::at("inspect owned file", &path, source))?;
377 if !metadata.is_file() || metadata.len() < length {
378 return Err(FileError::at(
379 "reopen owned file",
380 &path,
381 io::Error::new(io::ErrorKind::InvalidInput, "a regular file holding its bytes"),
382 ));
383 }
384 file.set_len(length)
385 .and_then(|()| file.seek(io::SeekFrom::Start(length)).map(|_| ()))
386 .map_err(|source| FileError::at("reopen owned file", &path, source))?;
387 Ok(Self::new(file, path, Some(Arc::new(lock)), bugs))
388 }
389
390 pub async fn write_from<R: tokio::io::AsyncRead + Unpin>(
393 mut self,
394 reader: &mut R,
395 mut written: impl FnMut(&[u8]),
396 batch: &mut FlushBatch,
397 ) -> Result<(), FileError> {
398 use tokio::io::AsyncReadExt;
399 let mut buffer = vec![0; 64 * 1024];
400 loop {
401 let count = reader
402 .read(&mut buffer)
403 .await
404 .map_err(|source| FileError::at("read file source", &self.path, source))?;
405 if count == 0 {
406 break;
407 }
408 self.append(&buffer[..count]).await?;
409 written(&buffer[..count]);
410 }
411 self.finish_into(batch).await
412 }
413
414 pub async fn append(&mut self, bytes: &[u8]) -> Result<(), FileError> {
416 let bytes = bytes.to_vec();
417 self.run("append owned file", move |writer, _| writer.file.write_all(&bytes)).await
418 }
419
420 pub async fn finish(self) -> Result<(), FileError> {
423 self.run("sync owned file", |writer, path| {
424 sync_file(&writer.file)?;
425 #[cfg(unix)]
426 sync_directory(parent(path))?;
427 #[cfg(not(unix))]
428 let _ = path;
429 Ok(())
430 })
431 .await
432 }
433
434 pub async fn sync(&mut self) -> Result<(), FileError> {
438 self.run("sync owned file", |writer, path| {
439 sync_file(&writer.file)?;
440 #[cfg(unix)]
441 sync_directory(parent(path))?;
442 #[cfg(not(unix))]
443 let _ = path;
444 Ok(())
445 })
446 .await
447 }
448
449 pub async fn finish_into(self, batch: &mut FlushBatch) -> Result<(), FileError> {
452 #[cfg(unix)]
453 {
454 let folder = self
455 .run("sync owned file", |writer, path| {
456 sync_file(&writer.file)?;
457 BatchFolder::open(parent(path).to_owned(), writer.lock.take())
458 })
459 .await?;
460 batch.add(folder);
461 }
462 #[cfg(windows)]
464 {
465 self.run("sync owned file", |writer, _| {
466 sync_file(&writer.file)?;
467 drop(writer.lock.take());
468 Ok(())
469 })
470 .await?;
471 batch.add();
472 }
473 Ok(())
474 }
475
476 async fn run<T: Send + 'static>(
477 &self,
478 operation: &'static str,
479 run: impl FnOnce(&mut LockedWriter, &Path) -> io::Result<T> + Send + 'static,
480 ) -> Result<T, FileError> {
481 let mut file = self.file.clone().lock_owned().await;
484 let path = self.path.clone();
485 crate::task::blocking_completing(operation, self.bugs.clone(), move || {
486 run(&mut file, &path).map_err(|source| FileError::at(operation, &path, source))
487 })
488 .await?
489 }
490}
491
492fn write_all_at(file: &fs::File, offset: u64, bytes: &[u8]) -> io::Result<()> {
494 #[cfg(unix)]
495 {
496 use std::os::unix::fs::FileExt;
497 file.write_all_at(bytes, offset)
498 }
499 #[cfg(windows)]
500 {
501 use std::os::windows::fs::FileExt;
502 let (mut offset, mut bytes) = (offset, bytes);
503 while !bytes.is_empty() {
504 match file.seek_write(bytes, offset) {
505 Ok(0) => return Err(io::ErrorKind::WriteZero.into()),
506 Ok(count) => {
507 bytes = &bytes[count..];
508 offset += count as u64;
509 }
510 Err(error) if error.kind() == io::ErrorKind::Interrupted => {}
511 Err(error) => return Err(error),
512 }
513 }
514 Ok(())
515 }
516}
517
518pub(super) fn sync_file(file: &fs::File) -> io::Result<()> {
522 #[cfg(unix)]
523 {
524 rustix::fs::fsync(file).map_err(Into::into)
525 }
526 #[cfg(windows)]
527 {
528 file.sync_all()
529 }
530}
531
532pub(super) fn exists(operation: &'static str, path: &Path) -> Result<bool, FileError> {
535 match fs::symlink_metadata(path) {
536 Ok(_) => Ok(true),
537 Err(error) if error.kind() == io::ErrorKind::NotFound => Ok(false),
538 Err(source) => Err(FileError::at(operation, path, source)),
539 }
540}
541
542pub(crate) fn remove(path: &Path) -> Result<(), FileError> {
543 match fs::remove_file(path) {
544 Ok(()) => {}
545 Err(error) if error.kind() == io::ErrorKind::NotFound => {
546 tracing::debug!(path = %path.display(), "file is already absent");
547 }
548 Err(source) => return Err(FileError::at("remove file", path, source)),
549 }
550 #[cfg(unix)]
551 match sync_directory(parent(path)) {
552 Ok(()) => {}
553 Err(error) if error.kind() == io::ErrorKind::NotFound => {
554 tracing::debug!(path = %path.display(), "file's parent is absent");
555 }
556 Err(source) => {
557 return Err(FileError::AfterRemove { path: path.to_owned(), source: Box::new(source) })
558 }
559 }
560 Ok(())
561}
562
563pub(crate) fn remove_empty_folder(path: &Path) -> Result<(), FileError> {
568 match fs::remove_dir(path) {
569 Ok(()) => {}
570 Err(error) if error.kind() == io::ErrorKind::NotFound => {
571 tracing::debug!(path = %path.display(), "folder is already absent");
572 }
573 Err(error) if error.kind() == io::ErrorKind::DirectoryNotEmpty => {
574 tracing::debug!(path = %path.display(), "leaving a folder that holds files");
575 return Ok(());
576 }
577 Err(source) => return Err(FileError::at("remove folder", path, source)),
578 }
579 #[cfg(unix)]
581 match sync_directory(parent(path)) {
582 Ok(()) => {}
583 Err(error) if error.kind() == io::ErrorKind::NotFound => {}
584 Err(source) => {
585 return Err(FileError::AfterRemove { path: path.to_owned(), source: Box::new(source) })
586 }
587 }
588 Ok(())
589}
590
591pub(crate) fn read_optional(path: &Path) -> Result<Option<Vec<u8>>, FileError> {
592 let result = open_reader(path).and_then(|mut file| {
593 let mut bytes = Vec::new();
594 file.read_to_end(&mut bytes)?;
595 Ok(bytes)
596 });
597 match result {
598 Ok(bytes) => Ok(Some(bytes)),
599 Err(source) if source.kind() == io::ErrorKind::NotFound => {
600 tracing::debug!(path = %path.display(), "file is absent");
601 Ok(None)
602 }
603 Err(source) => Err(FileError::at("read file", path, source)),
604 }
605}
606
607#[cfg(any(test, feature = "test-utils"))]
610pub fn read_existing(path: &Path) -> Result<Vec<u8>, FileError> {
611 read_optional(path)?
612 .ok_or_else(|| FileError::at("read file", path, io::ErrorKind::NotFound.into()))
613}
614
615pub(crate) fn read_json<T: serde::de::DeserializeOwned>(
618 path: &Path,
619 operation: &'static str,
620) -> Result<Option<T>, FileError> {
621 read_optional(path)?
622 .map(|bytes| {
623 serde_json::from_slice(&bytes).map_err(|error| {
624 FileError::at(operation, path, io::Error::new(io::ErrorKind::InvalidData, error))
625 })
626 })
627 .transpose()
628}
629
630fn open_reader(path: &Path) -> io::Result<fs::File> {
631 let mut options = fs::OpenOptions::new();
632 options.read(true);
633 #[cfg(windows)]
634 {
635 use std::os::windows::fs::OpenOptionsExt;
636 use windows_sys::Win32::Storage::FileSystem::{
637 FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
638 };
639 options.share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE);
640 }
641 options.open(path)
642}
643
644pub fn replace(path: &Path, bytes: &[u8]) -> Result<(), FileError> {
650 install(prepare(path, bytes, TEMPORARY)?, path)
651}
652
653pub(super) fn remove_temporaries(folder: &Path) -> Result<(), FileError> {
657 remove_named(folder, |name| name.starts_with(TEMPORARY.as_bytes()))
658}
659
660fn remove_named(folder: &Path, named: impl Fn(&[u8]) -> bool) -> Result<(), FileError> {
663 let entries = match fs::read_dir(folder) {
664 Ok(entries) => entries,
665 Err(error) if error.kind() == io::ErrorKind::NotFound => {
666 tracing::debug!(path = %folder.display(), "folder is absent");
667 return Ok(());
668 }
669 Err(source) => return Err(FileError::at("list folder", folder, source)),
670 };
671 for entry in entries {
672 let entry = entry.map_err(|source| FileError::at("list folder", folder, source))?;
673 if named(entry.file_name().as_encoded_bytes()) {
674 remove(&entry.path())?;
675 }
676 }
677 Ok(())
678}
679
680const TEMPORARY: &str = ".coven-write-";
682
683const TEMPORARY_RANDOM: usize = 6;
686
687fn own_temporaries(path: &Path) -> String {
692 let name = path.file_name().expect("an owned file has a name").to_string_lossy();
693 format!("{TEMPORARY}{name}-")
694}
695
696fn install(temp: tempfile::NamedTempFile, path: &Path) -> Result<(), FileError> {
699 #[cfg(unix)]
700 {
701 match temp.persist(path) {
702 Ok(file) => drop(file),
703 Err(error) => {
704 return Err(cleanup(
705 error.file,
706 FileError::at("rename temporary file", path, error.error),
707 ));
708 }
709 }
710 sync_directory(parent(path)).map_err(|source| FileError::AfterReplace {
711 path: path.to_owned(),
712 source: Box::new(source),
713 })
714 }
715 #[cfg(windows)]
716 {
717 let mut temp = temp;
718 if let Err(source) = windows_rename(temp.as_file(), path) {
719 return Err(cleanup(temp, FileError::at("POSIX rename temporary file", path, source)));
720 }
721 temp.disable_cleanup(true);
724 Ok(())
725 }
726}
727
728fn prepare(path: &Path, bytes: &[u8], prefix: &str) -> Result<tempfile::NamedTempFile, FileError> {
729 let mut temp = temporary(path, prefix)?;
730 if let Err(source) = temp.write_all(bytes).and_then(|()| temp.as_file().sync_all()) {
731 return Err(cleanup(temp, FileError::at("write and sync temporary file", path, source)));
732 }
733 Ok(temp)
734}
735
736fn temporary(path: &Path, prefix: &str) -> Result<tempfile::NamedTempFile, FileError> {
737 let directory = fs::canonicalize(parent(path))
740 .map_err(|source| FileError::at("resolve parent directory", path, source))?;
741 let mut builder = tempfile::Builder::new();
742 builder.prefix(prefix).rand_bytes(TEMPORARY_RANDOM);
743 #[cfg(unix)]
744 let temp = builder.tempfile_in(directory);
745 #[cfg(windows)]
746 let temp = builder.make_in(directory, create_new);
747 temp.map_err(|source| FileError::at("create temporary file", path, source))
748}
749
750pub(super) fn create_new(path: &Path) -> io::Result<fs::File> {
751 owned_options().create_new(true).open(path)
752}
753
754fn owned_options() -> fs::OpenOptions {
756 let mut options = fs::OpenOptions::new();
757 options.write(true);
758 #[cfg(windows)]
759 {
760 use std::os::windows::fs::OpenOptionsExt;
761 use windows_sys::Win32::Storage::FileSystem::{
762 DELETE, FILE_ATTRIBUTE_NORMAL, FILE_FLAG_WRITE_THROUGH, FILE_GENERIC_READ,
763 FILE_GENERIC_WRITE, FILE_SHARE_DELETE, FILE_SHARE_READ, FILE_SHARE_WRITE,
764 };
765 options
768 .access_mode(DELETE | FILE_GENERIC_READ | FILE_GENERIC_WRITE)
769 .share_mode(FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE)
770 .attributes(FILE_ATTRIBUTE_NORMAL)
771 .custom_flags(FILE_FLAG_WRITE_THROUGH);
772 }
773 options
774}
775
776fn cleanup(temp: tempfile::NamedTempFile, operation: FileError) -> FileError {
777 match temp.close() {
778 Ok(()) => operation,
779 Err(cleanup) => {
780 FileError::Cleanup { operation: Box::new(operation), cleanup: Box::new(cleanup) }
781 }
782 }
783}
784
785pub(super) fn parent(path: &Path) -> &Path {
786 match path.parent() {
787 Some(parent) if !parent.as_os_str().is_empty() => parent,
788 _ => Path::new("."),
789 }
790}
791
792#[cfg(unix)]
795pub(crate) fn sync_directory(path: &Path) -> io::Result<()> {
796 fs::File::open(path)?.sync_all()
797}
798
799#[cfg(windows)]
800#[expect(
801 clippy::map_err_ignore,
802 reason = "a rename buffer past u32 bytes means the path is too long"
803)]
804fn windows_rename(file: &fs::File, to: &Path) -> io::Result<()> {
805 use std::mem::{offset_of, size_of, size_of_val, MaybeUninit};
806 use std::os::windows::ffi::OsStrExt;
807 use std::os::windows::io::AsRawHandle;
808 use windows_sys::Win32::Storage::FileSystem::{
809 FileRenameInfoEx, SetFileInformationByHandle, FILE_RENAME_INFO,
810 };
811 use windows_sys::Win32::System::WindowsProgramming::{
812 FILE_RENAME_FLAG_POSIX_SEMANTICS, FILE_RENAME_FLAG_REPLACE_IF_EXISTS,
813 };
814
815 let name = to
816 .file_name()
817 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "file name required"))?;
818 let destination = fs::canonicalize(parent(to))?.join(name);
819 let name: Vec<_> = destination.as_os_str().encode_wide().chain(Some(0)).collect();
820 if name[..name.len() - 1].contains(&0) {
821 return Err(io::Error::new(io::ErrorKind::InvalidInput, "NUL in file name"));
822 }
823 let too_long = || io::Error::new(io::ErrorKind::InvalidInput, "rename path is too long");
824 let buffer_size = size_of::<FILE_RENAME_INFO>()
825 .checked_add(size_of_val(name.as_slice()))
826 .ok_or_else(too_long)?;
827 let buffer_size = u32::try_from(buffer_size).map_err(|_| too_long())?;
828 let mut buffer = vec![
831 MaybeUninit::<FILE_RENAME_INFO>::zeroed();
832 (buffer_size as usize).div_ceil(size_of::<FILE_RENAME_INFO>())
833 ];
834 let info = buffer.as_mut_ptr().cast::<FILE_RENAME_INFO>();
835 let result = unsafe {
841 (*info).Anonymous.Flags =
842 FILE_RENAME_FLAG_POSIX_SEMANTICS | FILE_RENAME_FLAG_REPLACE_IF_EXISTS;
843 (*info).FileNameLength = (size_of_val(name.as_slice()) - size_of::<u16>()) as u32;
844 std::ptr::copy_nonoverlapping(
845 name.as_ptr(),
846 info.cast::<u8>().add(offset_of!(FILE_RENAME_INFO, FileName)).cast::<u16>(),
847 name.len(),
848 );
849 SetFileInformationByHandle(file.as_raw_handle(), FileRenameInfoEx, info.cast(), buffer_size)
850 };
851 if result == 0 {
852 Err(io::Error::last_os_error())
855 } else {
856 Ok(())
857 }
858}
859
860#[cfg(unix)]
864pub(crate) fn rename_new(from: &Path, to: &Path) -> io::Result<()> {
865 use rustix::fs::{renameat_with, RenameFlags, CWD};
866 renameat_with(CWD, from, CWD, to, RenameFlags::NOREPLACE).map_err(Into::into)
867}
868
869#[cfg(windows)]
870pub(crate) fn rename_new(from: &Path, to: &Path) -> io::Result<()> {
871 use windows_sys::Win32::Storage::FileSystem::{MoveFileExW, MOVEFILE_WRITE_THROUGH};
872
873 let from = verbatim(from)?;
876 let to = verbatim(to)?;
877 if unsafe { MoveFileExW(from.as_ptr(), to.as_ptr(), MOVEFILE_WRITE_THROUGH) } == 0 {
880 Err(io::Error::last_os_error())
881 } else {
882 Ok(())
883 }
884}
885
886#[cfg(windows)]
894fn verbatim(path: &Path) -> io::Result<Vec<u16>> {
895 use std::os::windows::ffi::OsStrExt;
896 use std::path::{Component, Prefix};
897
898 let absolute = std::path::absolute(path)?;
899 let Some(Component::Prefix(prefix)) = absolute.components().next() else {
900 return Err(io::Error::new(
901 io::ErrorKind::InvalidInput,
902 "an absolute path without a drive or share",
903 ));
904 };
905 let units = absolute.as_os_str().encode_wide();
906 let units: Vec<u16> = match prefix.kind() {
907 Prefix::Disk(_) => r"\\?\".encode_utf16().chain(units).collect(),
908 Prefix::UNC(..) => r"\\?\UNC".encode_utf16().chain(units.skip(1)).collect(),
910 Prefix::Verbatim(_)
911 | Prefix::VerbatimUNC(..)
912 | Prefix::VerbatimDisk(_)
913 | Prefix::DeviceNS(_) => units.collect(),
914 };
915 Ok(units.into_iter().chain(Some(0)).collect())
916}
917
918pub(super) fn sync_publication(path: &Path) -> Result<(), FileError> {
919 #[cfg(unix)]
920 sync_directory(parent(path)).map_err(|source| FileError::AfterReplace {
921 path: path.to_owned(),
922 source: Box::new(source),
923 })?;
924 #[cfg(windows)]
926 let _ = path;
927 Ok(())
928}
929
930#[cfg(test)]
931#[path = "atomic_file_tests.rs"]
932mod tests;