tempfile/file/imp/
unix.rs

1use std::ffi::OsStr;
2use std::fs::{self, File, OpenOptions};
3use std::io;
4
5use crate::util;
6use std::path::Path;
7
8#[cfg(not(target_os = "redox"))]
9use {
10    rustix::fs::{rename, unlink},
11    std::fs::hard_link,
12};
13
14pub fn create_named(
15    path: &Path,
16    open_options: &mut OpenOptions,
17    #[cfg_attr(target_os = "wasi", allow(unused))] permissions: Option<&std::fs::Permissions>,
18) -> io::Result<File> {
19    open_options.read(true).write(true).create_new(true);
20
21    #[cfg(not(target_os = "wasi"))]
22    {
23        use std::os::unix::fs::{OpenOptionsExt, PermissionsExt};
24        open_options.mode(permissions.map(|p| p.mode()).unwrap_or(0o600));
25    }
26
27    open_options.open(path)
28}
29
30fn create_unlinked(path: &Path) -> io::Result<File> {
31    let tmp;
32    // shadow this to decrease the lifetime. It can't live longer than `tmp`.
33    let mut path = path;
34    if !path.is_absolute() {
35        let cur_dir = std::env::current_dir()?;
36        tmp = cur_dir.join(path);
37        path = &tmp;
38    }
39
40    let f = create_named(path, &mut OpenOptions::new(), None)?;
41    // don't care whether the path has already been unlinked,
42    // but perhaps there are some IO error conditions we should send up?
43    let _ = fs::remove_file(path);
44    Ok(f)
45}
46
47#[cfg(target_os = "linux")]
48pub fn create(dir: &Path) -> io::Result<File> {
49    use rustix::{fs::OFlags, io::Errno};
50    use std::os::unix::fs::OpenOptionsExt;
51    OpenOptions::new()
52        .read(true)
53        .write(true)
54        .custom_flags(OFlags::TMPFILE.bits() as i32) // do not mix with `create_new(true)`
55        .open(dir)
56        .or_else(|e| {
57            match Errno::from_io_error(&e) {
58                // These are the three "not supported" error codes for O_TMPFILE.
59                Some(Errno::OPNOTSUPP) | Some(Errno::ISDIR) | Some(Errno::NOENT) => {
60                    create_unix(dir)
61                }
62                _ => Err(e),
63            }
64        })
65}
66
67#[cfg(not(target_os = "linux"))]
68pub fn create(dir: &Path) -> io::Result<File> {
69    create_unix(dir)
70}
71
72fn create_unix(dir: &Path) -> io::Result<File> {
73    util::create_helper(
74        dir,
75        OsStr::new(".tmp"),
76        OsStr::new(""),
77        crate::NUM_RAND_CHARS,
78        |path| create_unlinked(&path),
79    )
80}
81
82#[cfg(any(not(target_os = "wasi"), feature = "nightly"))]
83pub fn reopen(file: &File, path: &Path) -> io::Result<File> {
84    #[cfg(not(target_os = "wasi"))]
85    use std::os::unix::fs::MetadataExt;
86    #[cfg(target_os = "wasi")]
87    use std::os::wasi::fs::MetadataExt;
88
89    let new_file = OpenOptions::new().read(true).write(true).open(path)?;
90    let old_meta = file.metadata()?;
91    let new_meta = new_file.metadata()?;
92    if old_meta.dev() != new_meta.dev() || old_meta.ino() != new_meta.ino() {
93        return Err(io::Error::new(
94            io::ErrorKind::NotFound,
95            "original tempfile has been replaced",
96        ));
97    }
98    Ok(new_file)
99}
100
101#[cfg(all(target_os = "wasi", not(feature = "nightly")))]
102pub fn reopen(_file: &File, _path: &Path) -> io::Result<File> {
103    return Err(io::Error::new(
104        io::ErrorKind::Other,
105        "this operation is supported on WASI only on nightly Rust (with `nightly` feature enabled)",
106    ));
107}
108
109#[cfg(not(target_os = "redox"))]
110pub fn persist(old_path: &Path, new_path: &Path, overwrite: bool) -> io::Result<()> {
111    if overwrite {
112        rename(old_path, new_path)?;
113    } else {
114        // On Linux and apple operating systems, use `renameat_with` to avoid overwriting an
115        // existing name, if the kernel and the filesystem support it.
116        #[cfg(any(
117            target_os = "android",
118            target_os = "linux",
119            target_os = "macos",
120            target_os = "ios",
121            target_os = "tvos",
122            target_os = "visionos",
123            target_os = "watchos",
124        ))]
125        {
126            use rustix::fs::{renameat_with, RenameFlags, CWD};
127            use rustix::io::Errno;
128            use std::sync::atomic::{AtomicBool, Ordering::Relaxed};
129
130            static NOSYS: AtomicBool = AtomicBool::new(false);
131            if !NOSYS.load(Relaxed) {
132                match renameat_with(CWD, old_path, CWD, new_path, RenameFlags::NOREPLACE) {
133                    Ok(()) => return Ok(()),
134                    Err(Errno::NOSYS) => NOSYS.store(true, Relaxed),
135                    Err(Errno::INVAL) => {}
136                    Err(e) => return Err(e.into()),
137                }
138            }
139        }
140
141        // Otherwise use `hard_link` to create the new filesystem name, which
142        // will fail if the name already exists, and then `unlink` to remove
143        // the old name.
144        hard_link(old_path, new_path)?;
145
146        // Ignore unlink errors. Can we do better?
147        let _ = unlink(old_path);
148    }
149    Ok(())
150}
151
152#[cfg(target_os = "redox")]
153pub fn persist(_old_path: &Path, _new_path: &Path, _overwrite: bool) -> io::Result<()> {
154    // XXX implement when possible
155    use rustix::io::Errno;
156    Err(Errno::NOSYS.into())
157}
158
159pub fn keep(_: &Path) -> io::Result<()> {
160    Ok(())
161}