Skip to main content

coven_foundation/files/
atomic_file.rs

1//! Durable file creation, atomic replacement and removal.
2
3#[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/// A file operation's cause, including whether replacement already happened.
12#[derive(Debug, thiserror::Error)]
13pub enum FileError {
14    /// An operation failed without replacing the target.
15    #[error("{operation} {}: {source}", path.display())]
16    Io {
17        /// The operation that failed.
18        operation: &'static str,
19        /// The affected path.
20        path: PathBuf,
21        /// The operating system's error.
22        #[source]
23        source: Box<dyn std::error::Error + Send + Sync>,
24    },
25    /// The new bytes are visible, but syncing the directory failed.
26    #[error("sync directory after replacing {}: {source}", path.display())]
27    AfterReplace {
28        /// The replaced file.
29        path: PathBuf,
30        /// The operating system's error.
31        #[source]
32        source: Box<dyn std::error::Error + Send + Sync>,
33    },
34    /// The file is absent, but syncing its directory failed.
35    #[error("sync directory after removing {}: {source}", path.display())]
36    AfterRemove {
37        /// The removed file.
38        path: PathBuf,
39        /// The operating system's error.
40        #[source]
41        source: Box<dyn std::error::Error + Send + Sync>,
42    },
43    /// Removing an unpublished temporary file also failed.
44    #[error("{operation}; removing temporary file failed: {cleanup}")]
45    Cleanup {
46        /// The original failure.
47        #[source]
48        operation: Box<FileError>,
49        /// The error removing the temporary file.
50        cleanup: Box<dyn std::error::Error + Send + Sync>,
51    },
52    /// The device is locked, and the file's protection class keeps it from
53    /// being made, opened or given its class until the device unlocks
54    /// (api.md E1). The app waits for the unlock and retries.
55    #[error("{}: the device is locked, and the file is protected until it unlocks", path.display())]
56    DeviceLocked {
57        /// The protected file, or the file that couldn't be made in a
58        /// protected folder.
59        path: PathBuf,
60    },
61    /// The operation's runtime is shutting down and dropped it unstarted
62    /// (E15).
63    #[error(transparent)]
64    Shutdown(#[from] crate::task::Shutdown),
65    /// The operation panicked on the blocking pool: a bug, which the caller
66    /// reports (E15).
67    #[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    /// Whether readers already see the replacement despite a durability error.
89    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/// One named file capability. It reads, creates, replaces and removes bytes
99/// without exposing a path. Replacement atomically installs a complete version.
100#[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    /// Read the complete file. Only a missing file is `None`; other errors
111    /// retain their operating-system cause.
112    pub fn read_optional(&self) -> Result<Option<Vec<u8>>, FileError> {
113        read_optional(&self.path)
114    }
115
116    /// Open this owned file for checked scans and positioned reads.
117    pub fn open_reader(&self) -> Result<super::FileReader, super::ObservationError> {
118        super::FileReader::open(&self.path)
119    }
120
121    /// Write and sync a temporary sibling, replace the target, then sync its
122    /// directory. The parent must already exist. Concurrent writers each
123    /// install one complete version; a failed pre-rename write leaves it alone.
124    ///
125    /// Windows uses POSIX replacement on a write-through file handle, which
126    /// flushes the rename's metadata on NTFS. This requires `FileRenameInfoEx`
127    /// and a filesystem supporting POSIX replacement. Windows 10 before 1607
128    /// lacks that API; unsupported systems return the OS error without falling
129    /// back to a rename that can fail while readers hold old versions open.
130    pub fn replace(&self, bytes: &[u8]) -> Result<(), FileError> {
131        install(prepare(&self.path, bytes, &own_temporaries(&self.path))?, &self.path)
132    }
133
134    /// Write and sync complete bytes at this name, then flush its directory
135    /// and the drive's cache, as a batch of this one file, with no temporary
136    /// sibling. The caller records the name as its own
137    /// first, so bytes a crash interrupts stay recorded for removal. Anything
138    /// but a regular file there is refused before a byte is written.
139    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    /// Write `bytes` over this existing file's bytes from `offset`, which
169    /// they must lie within, and hand them to the drive. Readers can see
170    /// some of the old bytes and some of the new while it writes, and a
171    /// crash can leave them so: only bytes that check themselves, as a
172    /// cache's authenticated chunks do, are written this way.
173    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        // No directory entry changes, so `sync_all`, which flushes an Apple
193        // drive's cache with `F_FULLFSYNC`, is the whole barrier.
194        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    /// Give this existing file `length` bytes, cutting it short or adding
201    /// zeros at its end, and hand the change to the drive.
202    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        // No directory entry changes, so `sync_all`, which flushes an Apple
220        // drive's cache with `F_FULLFSYNC`, is the whole barrier.
221        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    /// The path under its canonical parent, which also supplies Windows'
228    /// extended-length prefix.
229    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    /// Remove the owned file; an absent file is success. On Unix, sync the
243    /// directory even on a retry after an earlier directory-sync failure.
244    /// A missing parent is already absent and needs no durability barrier.
245    pub fn remove(&self) -> Result<(), FileError> {
246        remove(&self.path)
247    }
248
249    /// Remove the owned file and every temporary sibling a write of it left,
250    /// as a crash or a failed removal does: each holds a whole version of its
251    /// bytes. The caller holds what guards this file's writes, so none of
252    /// them is a write in flight.
253    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    /// Leave a temporary sibling holding `bytes`, as a crash between writing
263    /// it and renaming it does, and say where.
264    #[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
271/// An unpublished file's open writer. Its OS handle and path stay private.
272/// Dropping it leaves the named bytes for the caller's recorded cleanup.
273/// The shared store lock lasts until its handle and any cancelled I/O finish,
274/// and, for a file finished into a batch, until the batch's flush ends.
275pub struct FileWriter {
276    file: Arc<tokio::sync::Mutex<LockedWriter>>,
277    path: PathBuf,
278    /// Where a panic in a write its caller stopped waiting for goes (E15).
279    bugs: crate::task::BugSinkRef,
280}
281struct LockedWriter {
282    file: fs::File,
283    // Close the handle before allowing the store directory to be deleted.
284    // Finishing into a batch moves it to the batch, which outlives the handle.
285    lock: Option<Arc<StoreReadLock>>,
286}
287
288/// One area's folder, held for a run of files made in it: the store's read
289/// lock, taken once, and the folder's resolved path, shared by every writer
290/// made here. The lock lasts until the last of those writers, and any batch
291/// one finished into, lets go of it.
292#[derive(Clone)]
293pub struct WriterFolder {
294    path: PathBuf,
295    lock: Arc<StoreReadLock>,
296    /// Where a panic in a write its caller stopped waiting for goes (E15).
297    bugs: crate::task::BugSinkRef,
298}
299
300impl WriterFolder {
301    /// Hold `lock`, the store's read lock, with the folder at `path`
302    /// resolved. A panic in a write whose caller stopped waiting goes to
303    /// `bugs` (E15).
304    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    /// Create `name` here for asynchronous streaming without replacing
315    /// anything.
316    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    /// Create `name` here holding all of `bytes` and count it in `batch`, as
324    /// [`FileWriter::write_from`] does for a source it reads, but in this one
325    /// call: it blocks on the file's I/O, so a caller on an async runtime
326    /// runs it on the blocking pool.
327    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    /// Reopen `file`, an unfinished file of a writer that stopped, for
361    /// appending after its first `length` bytes, which the caller recorded
362    /// as synced: whatever lies past them is cut off. A file shorter than
363    /// `length` is refused, since the bytes it lost are not appended again.
364    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    /// Read once in 64 KiB chunks, reporting each chunk after its disk write,
391    /// then finish the file into `batch`.
392    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    /// Append one bounded buffer, completing its disk write before returning.
415    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    /// Sync all appended bytes, then flush their directory and the drive's
421    /// cache, as a batch of this one file, before publishing metadata.
422    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    /// Sync all appended bytes and flush their directory and the drive's
435    /// cache, keeping the writer open: what was appended before is durable
436    /// when this returns.
437    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    /// Sync all appended bytes and count the file in `batch`, whose flush
450    /// makes it durable before the commit that records it.
451    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        // Windows' batch flush opens no folder, so needs no lock.
463        #[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        // Acquire before spawning so later calls wait for cancelled work too.
482        // The owned guard carries the OS handle and store lock into the task.
483        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
492/// Write all of `bytes` at `offset` with the platform's positioned writes.
493fn 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
518/// Hand a file's bytes to the drive. Unix's `fsync` can leave them in an
519/// Apple drive's cache until a flush of their directory with `F_FULLFSYNC`;
520/// Windows' `FlushFileBuffers` also flushes the drive's cache.
521pub(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
532/// Inspect the directory entry itself, including dangling symlinks.
533/// Only a missing entry is false; all other failures retain the caller's operation.
534pub(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
563/// Remove the folder at `path` if nothing is in it, then sync the folder
564/// that held it: a folder holding anything stays. An absent one is gone,
565/// and its parent is synced still, as for a retry after an earlier sync
566/// failed.
567pub(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    // Windows writes a folder's entries through with the file handle's flag.
580    #[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/// The complete file at `path`, which must exist, for a test reading back
608/// bytes it saved with [`replace`].
609#[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
615/// The JSON record at `path`, or `None` when it is absent. Bytes that don't
616/// decode are invalid data, reported as `operation`'s failure.
617pub(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
644/// Write and sync a temporary sibling, atomically replace `path`, then sync
645/// its directory. The parent must exist. Concurrent readers see whole versions.
646/// A pre-replacement failure leaves the target unchanged; `FileError::AfterReplace`
647/// reports that new bytes are visible but directory syncing failed.
648/// Windows requires the POSIX replacement support described by [`AtomicFile::replace`].
649pub fn replace(path: &Path, bytes: &[u8]) -> Result<(), FileError> {
650    install(prepare(path, bytes, TEMPORARY)?, path)
651}
652
653/// Remove every temporary sibling in `folder`, the top of a store's
654/// directory: what a crash or a failed removal left of its writes. The caller
655/// holds the store's writer or bootstrap lock, so none is a write in flight.
656pub(super) fn remove_temporaries(folder: &Path) -> Result<(), FileError> {
657    remove_named(folder, |name| name.starts_with(TEMPORARY.as_bytes()))
658}
659
660/// Remove each entry of `folder` whose name `named` picks. A folder that is
661/// absent holds none.
662fn 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
680/// How every temporary sibling's name starts.
681const TEMPORARY: &str = ".coven-write-";
682
683/// The random letters ending a temporary sibling's name, so each writer
684/// has its own.
685const TEMPORARY_RANDOM: usize = 6;
686
687/// How the names of an owned file's temporary siblings start: they carry
688/// its name, so forgetting it finds them. An app's own path, written by
689/// [`replace`], may already fill the 255 bytes a name holds, so it carries
690/// none.
691fn 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
696/// Replace `path` with the prepared temporary sibling `temp`, whose failure
697/// to install also removes it.
698fn 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        // The source no longer exists. Disarm removal without issuing another
722        // filesystem operation against a possibly reused name.
723        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    // Canonicalization also supplies Windows' extended-length prefix. The
738    // directory already exists, so there is no guess about a relative parent.
739    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
754/// Write access to a file coven owns, durable on every platform.
755fn 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        // Write-through also flushes creation and rename metadata on NTFS.
766        // Do not mark bytes destined for durable storage as TEMPORARY.
767        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/// Sync a directory and, on Apple systems, flush the drive's cache with
793/// `F_FULLFSYNC`, as std's `sync_all` does there.
794#[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    // Allocate in units of the ABI type for its alignment, leaving enough room
829    // for its variable-length UTF-16 name, NUL and trailing struct padding.
830    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    // SAFETY: the allocation is aligned for FILE_RENAME_INFO and contains its
836    // header and the whole name. RootDirectory is null (the name is absolute),
837    // and FileNameLength excludes the NUL.
838    // Both the buffer and the DELETE-access, write-through handle stay alive
839    // for the synchronous call. No Rust reference covers the variable tail.
840    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        // No classic-rename fallback: it cannot preserve replacement while
853        // old versions are open. Keep the OS cause on unsupported systems too.
854        Err(io::Error::last_os_error())
855    } else {
856        Ok(())
857    }
858}
859
860/// Rename a file or directory without replacing any existing destination.
861/// Callers use resolved siblings and handle directory durability themselves.
862/// Unsupported filesystems return their native error; there is no fallback.
863#[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    // Both paths have a resolved parent on the same filesystem. Refuse any
874    // existing destination, whether publishing a file or a store directory.
875    let from = verbatim(from)?;
876    let to = verbatim(to)?;
877    // SAFETY: both paths are live, NUL-terminated UTF-16 buffers. They have
878    // the same parent; no replacement or cross-volume copy is requested.
879    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/// `path` as a NUL-terminated `\\?\` path for a Win32 call. Given a plain
887/// path, such a call parses it itself and stops at 260 characters; a `\\?\`
888/// path goes to the file system as given, at any length, as std passes its
889/// own calls once a path nears that limit. The path is first made absolute
890/// as Win32 would read it (`GetFullPathNameW`), which refuses a NUL; then
891/// `C:\…` becomes `\\?\C:\…` and `\\server\share\…` becomes
892/// `\\?\UNC\server\share\…`. A `\\?\` or `\\.\` path stays as it is.
893#[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        // `\\server` gives one of its backslashes to `\\?\UNC\`.
909        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    // MoveFileExW uses WRITE_THROUGH; Unix needs the explicit parent barrier.
925    #[cfg(windows)]
926    let _ = path;
927    Ok(())
928}
929
930#[cfg(test)]
931#[path = "atomic_file_tests.rs"]
932mod tests;