1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
//! More ways to rename files.
//!
//! ## Overview
//!
//! The Rust standard library offers [`std::fs::rename`] for renaming files.
//! Sometimes, that's not enough. Consider the example of renaming a file but
//! aborting the operation if something already exists at the destination path.
//! That can be achieved using the Rust standard library but ensuring that the
//! operation is atomic requires platform-specific APIs. Without using
//! platform-specific APIs, a [TOCTTOU] bug can be introduced. This library aims
//! to provide a cross-platform interface to these APIs.
//!
//! [TOCTTOU]: https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
//!
//! ## Examples
//!
//! Renaming a file without the possibility of accidentally overwriting anything
//! can be done using [`rename_exclusive`]. It should be noted that this feature
//! is not supported by all combinations of operating system and file system.
//! `rename_exclusive` will fail if it can't be done atomically.
//!
//! ```no_run
//! use std::io::Result;
//!
//! fn main() -> Result<()> {
//!     renamore::rename_exclusive("old.txt", "new.txt")
//! }
//! ```
//!
//! Alternatively, [`rename_exclusive_fallback`] can be used. This will try to
//! perform the operation atomically, and use a non-atomic fallback if that's
//! not supported. The return value will indicate what happened.
//!
//! ```no_run
//! use std::io::Result;
//!
//! fn main() -> Result<()> {
//!     if renamore::rename_exclusive_fallback("old.txt", "new.txt")? {
//!         // `new.txt` was definitely not overwritten.
//!         println!("The operation was atomic");
//!     } else {
//!         // `new.txt` was probably not overwritten.
//!         println!("The operation was not atomic");
//!     }
//!
//!     Ok(())
//! }
//! ```
//!
//! ## Platform-specific behaviour
//!
//! On Linux, the `renameat2` syscall is used. A wrapper around this syscall is
//! provided by glibc since version 2.28 but not musl (yet?). The existence of
//! the wrapper is checked at build time and a wrapper is provided if one isn't
//! found. In case something goes wrong, there are two features that can be used
//! to bypass this mechanism.
//!
//!  - `always-supported`. Assume that `renameat2` exists.
//!  - `always-fallback`. Assume that `renameat2` doesn't exist.
//!
//! Hopefully using these features shouldn't be necessary. If they do become
//! necessary, then there might be a bug.

use std::path::Path;
use std::io::{Error, ErrorKind, Result};

/// Rename a file without overwriting the destination path if it exists.
///
/// Unlike a combination of [`try_exists`] and [`rename`], this operation is
/// atomic. A potential [TOCTTOU] bug is avoided. There is no possibility of
/// `to` coming into existence at just the wrong moment and being overwritten.
///
/// [`try_exists`]: std::path::Path::try_exists
/// [`rename`]: std::fs::rename
/// [TOCTTOU]: https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use
///
/// # Platform-specific behaviour
///
/// On Linux, this calls `renameat2` with `RENAME_NOREPLACE`. On Darwin (macOS,
/// iOS, watchOS, tvOS), this calls `renamex_np` with `RENAME_EXCL`. On Windows,
/// this calls `MoveFileExW` with no flags. On all other platforms, this returns
/// [`ErrorKind::Unsupported`] unconditionally.
///
/// # Errors
///
/// Performing this operation atomically is not supported on all platforms. If
/// it's not supported but the rename request is otherwise valid, then
/// [`ErrorKind::Unsupported`] will be returned. If the operation is supported
/// but a file at `to` exists, then [`ErrorKind::AlreadyExists`] will be
/// returned.
///
/// [`ErrorKind::Unsupported`]: std::io::ErrorKind::Unsupported
/// [`ErrorKind::AlreadyExists`]: std::io::ErrorKind::AlreadyExists
pub fn rename_exclusive<F: AsRef<Path>, T: AsRef<Path>>(from: F, to: T) -> Result<()> {
    sys::rename_exclusive(from.as_ref(), to.as_ref())
}

/// Determine whether an atomic [`rename_exclusive`] is supported.
///
/// Support for performing this operation atomically depends on whether the
/// necessary functions are available at link-time, and the OS implements the
/// operation for the file system of the given path. If this function returns
/// `Ok(true)`, then a call to `rename_exclusive` at the same path is unlikely
/// to return [`ErrorKind::Unsupported`] if it fails.
///
/// [`ErrorKind::Unsupported`]: std::io::ErrorKind::Unsupported
///
/// # Platform-specific behaviour
///
/// On Linux, this parses `/proc/version` to determine the kernel version and
/// calls `statfs` to determine the file system type. On Darwin (macOS, iOS,
/// watchOS, tvOS), this calls `getattrlist` to determine whether the volume at
/// the path lists `VOL_CAP_INT_RENAME_EXCL` as one of its capabilities. On
/// Windows, this always returns `Ok(true)` even though that may not be
/// technically true. On all other platforms, this always returns `Ok(false)`.
///
/// # Examples
///
/// ```no_run
/// # use std::io::Result;
/// # fn main() -> Result<()> {
/// if !renamore::rename_exclusive_is_atomic(".")? {
///     println!("Warning: atomically renaming without overwriting is not supported!");
/// }
/// # Ok(())
/// # }
/// ```
pub fn rename_exclusive_is_atomic<P: AsRef<Path>>(path: P) -> Result<bool> {
    sys::rename_exclusive_is_atomic(path.as_ref())
}

/// Rename a file without overwriting the destination path if it exists, using a
/// non-atomic fallback if necessary.
///
/// This is similar to [`rename_exclusive`] except that if performing the
/// operation atomically is not supported, then a non-atomic fallback
/// implementation based on [`try_exists`] and [`rename`] will be used.
///
/// [`try_exists`]: std::path::Path::try_exists
/// [`rename`]: std::fs::rename
///
/// # Examples
///
/// ```no_run
/// # fn main() -> std::io::Result<()> {
/// if renamore::rename_exclusive_fallback("old.txt", "new.txt")? {
///     // `new.txt` was definitely not overwritten.
///     println!("The operation was atomic");
/// } else {
///     // `new.txt` was probably not overwritten.
///     println!("The operation was not atomic");
/// }
/// # Ok(())
/// # }
/// ```
pub fn rename_exclusive_fallback<F: AsRef<Path>, T: AsRef<Path>>(from: F, to: T) -> Result<bool> {
    fn inner(from: &Path, to: &Path) -> Result<bool> {
        if let Err(e) = sys::rename_exclusive(from, to) {
            if e.kind() == ErrorKind::Unsupported {
                rename_exclusive_non_atomic(from, to)?;
                return Ok(false);
            }
            Err(e)
        } else {
            Ok(true)
        }
    }
    inner(from.as_ref(), to.as_ref())
}

fn rename_exclusive_non_atomic(from: &Path, to: &Path) -> Result<()> {
    if to.try_exists()? {
        return Err(Error::from(ErrorKind::AlreadyExists));
    }

    std::fs::rename(from, to)
}

#[cfg(all(target_os = "linux", linker))]
mod linux;
#[cfg(all(target_os = "linux", linker))]
use linux as sys;

#[cfg(target_vendor = "apple")]
mod macos;
#[cfg(target_vendor = "apple")]
use macos as sys;

#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
use windows as sys;

#[cfg(not(any(
    all(target_os = "linux", linker),
    target_vendor = "apple",
    target_os = "windows",
)))]
mod sys {
    use std::path::Path;
    use std::io::{Error, ErrorKind, Result};

    pub fn rename_exclusive(_from: &Path, _to: &Path) -> Result<()> {
        Err(Error::from(ErrorKind::Unsupported))
    }

    pub fn rename_exclusive_is_atomic(_path: &Path) -> Result<bool> {
        Ok(false)
    }
}

#[cfg(test)]
mod tests;