tempfile/file/imp/
unix.rs

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