1#[cfg(unix)]
3extern crate rustix;
4extern crate tempfile;
5
6use std::convert::AsRef;
7use std::error::Error as ErrorTrait;
8use std::fmt;
9use std::fs;
10use std::io;
11use std::path;
12
13pub use OverwriteBehavior::{AllowOverwrite, DisallowOverwrite};
14
15#[derive(Clone, Copy)]
17pub enum OverwriteBehavior {
18    AllowOverwrite,
20
21    DisallowOverwrite,
24}
25
26#[derive(Debug)]
28pub enum Error<E> {
29    Internal(io::Error),
32    User(E),
34}
35
36impl From<Error<io::Error>> for io::Error {
38    fn from(e: Error<io::Error>) -> Self {
39        match e {
40            Error::Internal(x) => x,
41            Error::User(x) => x,
42        }
43    }
44}
45
46impl<E: fmt::Display> fmt::Display for Error<E> {
47    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
48        match *self {
49            Error::Internal(ref e) => e.fmt(f),
50            Error::User(ref e) => e.fmt(f),
51        }
52    }
53}
54
55impl<E: ErrorTrait> ErrorTrait for Error<E> {
56    fn cause(&self) -> Option<&dyn ErrorTrait> {
57        match *self {
58            Error::Internal(ref e) => Some(e),
59            Error::User(ref e) => Some(e),
60        }
61    }
62}
63
64fn safe_parent(p: &path::Path) -> Option<&path::Path> {
65    match p.parent() {
66        None => None,
67        Some(x) if x.as_os_str().is_empty() => Some(path::Path::new(".")),
68        x => x,
69    }
70}
71
72pub struct AtomicFile {
74    path: path::PathBuf,
76    overwrite: OverwriteBehavior,
77    tmpdir: path::PathBuf,
79}
80
81impl AtomicFile {
82    pub fn new<P>(path: P, overwrite: OverwriteBehavior) -> Self
91    where
92        P: AsRef<path::Path>,
93    {
94        let p = path.as_ref();
95        AtomicFile::new_with_tmpdir(
96            p,
97            overwrite,
98            safe_parent(p).unwrap_or_else(|| path::Path::new(".")),
99        )
100    }
101
102    pub fn new_with_tmpdir<P, Q>(path: P, overwrite: OverwriteBehavior, tmpdir: Q) -> Self
106    where
107        P: AsRef<path::Path>,
108        Q: AsRef<path::Path>,
109    {
110        AtomicFile {
111            path: path.as_ref().to_path_buf(),
112            overwrite,
113            tmpdir: tmpdir.as_ref().to_path_buf(),
114        }
115    }
116
117    fn commit(&self, tmppath: &path::Path) -> io::Result<()> {
119        match self.overwrite {
120            AllowOverwrite => replace_atomic(tmppath, self.path()),
121            DisallowOverwrite => move_atomic(tmppath, self.path()),
122        }
123    }
124
125    pub fn path(&self) -> &path::Path {
127        &self.path
128    }
129
130    pub fn write<T, E, F>(&self, f: F) -> Result<T, Error<E>>
135    where
136        F: FnOnce(&mut fs::File) -> Result<T, E>,
137    {
138        let mut options = fs::OpenOptions::new();
139        options.write(true).create(true).truncate(true);
141        self.write_with_options(f, options)
142    }
143
144    pub fn write_with_options<T, E, F>(&self, f: F, options: fs::OpenOptions) -> Result<T, Error<E>>
152    where
153        F: FnOnce(&mut fs::File) -> Result<T, E>,
154    {
155        let tmpdir = tempfile::Builder::new()
156            .prefix(".atomicwrite")
157            .tempdir_in(&self.tmpdir)
158            .map_err(Error::Internal)?;
159
160        let tmppath = tmpdir.path().join("tmpfile.tmp");
161        let rv = {
162            let mut tmpfile = options.open(&tmppath).map_err(Error::Internal)?;
163            let r = f(&mut tmpfile).map_err(Error::User)?;
164            tmpfile.sync_all().map_err(Error::Internal)?;
165            r
166        };
167        self.commit(&tmppath).map_err(Error::Internal)?;
168        Ok(rv)
169    }
170}
171
172#[cfg(unix)]
173mod imp {
174    use super::safe_parent;
175
176    use rustix::fs::AtFlags;
177    use std::{fs, io, path};
178
179    pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
180        let src_parent_path = safe_parent(src).unwrap();
181        let dst_parent_path = safe_parent(dst).unwrap();
182        let src_child_path = src.file_name().unwrap();
183        let dst_child_path = dst.file_name().unwrap();
184
185        let src_parent = fs::File::open(src_parent_path)?;
188        let dst_parent;
189        let dst_parent = if src_parent_path == dst_parent_path {
190            &src_parent
191        } else {
192            dst_parent = fs::File::open(dst_parent_path)?;
193            &dst_parent
194        };
195
196        rustix::fs::renameat(&src_parent, src_child_path, dst_parent, dst_child_path)?;
198
199        src_parent.sync_all()?;
201        if src_parent_path != dst_parent_path {
202            dst_parent.sync_all()?;
203        }
204
205        Ok(())
206    }
207
208    pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
209        let src_parent_path = safe_parent(src).unwrap();
210        let dst_parent_path = safe_parent(dst).unwrap();
211        let src_child_path = src.file_name().unwrap();
212        let dst_child_path = dst.file_name().unwrap();
213
214        let src_parent = fs::File::open(src_parent_path)?;
217        let dst_parent;
218        let dst_parent = if src_parent_path == dst_parent_path {
219            &src_parent
220        } else {
221            dst_parent = fs::File::open(dst_parent_path)?;
222            &dst_parent
223        };
224
225        #[cfg(any(target_os = "android", target_os = "linux"))]
228        {
229            use rustix::fs::RenameFlags;
230            use std::sync::atomic::AtomicBool;
231            use std::sync::atomic::Ordering::Relaxed;
232
233            static NO_RENAMEAT2: AtomicBool = AtomicBool::new(false);
234            if !NO_RENAMEAT2.load(Relaxed) {
235                match rustix::fs::renameat_with(
236                    &src_parent,
237                    src_child_path,
238                    dst_parent,
239                    dst_child_path,
240                    RenameFlags::NOREPLACE,
241                ) {
242                    Ok(()) => {
243                        src_parent.sync_all()?;
246                        if src_parent_path != dst_parent_path {
247                            dst_parent.sync_all()?;
248                        }
249                        return Ok(());
250                    }
251                    Err(rustix::io::Errno::INVAL) | Err(rustix::io::Errno::NOSYS) => {
252                        NO_RENAMEAT2.store(true, Relaxed);
266                    }
267                    Err(e) => return Err(e.into()),
268                }
269            }
270        }
271
272        rustix::fs::linkat(
274            &src_parent,
275            src_child_path,
276            dst_parent,
277            dst_child_path,
278            AtFlags::empty(),
279        )?;
280        rustix::fs::unlinkat(&src_parent, src_child_path, AtFlags::empty())?;
281
282        src_parent.sync_all()?;
284        if src_parent_path != dst_parent_path {
285            dst_parent.sync_all()?;
286        }
287
288        Ok(())
289    }
290}
291
292#[cfg(windows)]
293mod imp {
294    extern crate windows_sys;
295
296    use std::ffi::OsStr;
297    use std::os::windows::ffi::OsStrExt;
298    use std::{io, path};
299
300    macro_rules! call {
301        ($e: expr) => {
302            if $e != 0 {
303                Ok(())
304            } else {
305                Err(io::Error::last_os_error())
306            }
307        };
308    }
309
310    fn path_to_windows_str<T: AsRef<OsStr>>(x: T) -> Vec<u16> {
311        x.as_ref().encode_wide().chain(Some(0)).collect()
312    }
313
314    pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
315        call!(unsafe {
316            windows_sys::Win32::Storage::FileSystem::MoveFileExW(
317                path_to_windows_str(src).as_ptr(),
318                path_to_windows_str(dst).as_ptr(),
319                windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH
320                    | windows_sys::Win32::Storage::FileSystem::MOVEFILE_REPLACE_EXISTING,
321            )
322        })
323    }
324
325    pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
326        call!(unsafe {
327            windows_sys::Win32::Storage::FileSystem::MoveFileExW(
328                path_to_windows_str(src).as_ptr(),
329                path_to_windows_str(dst).as_ptr(),
330                windows_sys::Win32::Storage::FileSystem::MOVEFILE_WRITE_THROUGH,
331            )
332        })
333    }
334}
335
336pub fn replace_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
340    imp::replace_atomic(src, dst)
341}
342
343pub fn move_atomic(src: &path::Path, dst: &path::Path) -> io::Result<()> {
347    imp::move_atomic(src, dst)
348}