cap_tempfile/
tempfile.rs

1//! Temporary files.
2
3use cap_std::fs::{Dir, File};
4use std::ffi::OsStr;
5use std::fmt::Debug;
6use std::io::{self, Read, Seek, Write};
7
8/// A file in a directory that is by default deleted when it goes out
9/// of scope, but may also be written persistently.
10///
11/// This corresponds most closely to [`tempfile::NamedTempFile`]; however,
12/// there are some important differences, so read the below carefully
13/// to understand how to port existing code.
14///
15/// # Name-able, but not necessarily named
16///
17/// By default, the file does not necessarily have an name until the file is
18/// written persistently.
19///
20/// On some operating systems like Linux, it is possible to create anonymous
21/// temporary files that can still be written to disk persistently via
22/// `O_TMPFILE`. The advantage of this is that if the process (or operating
23/// system) crashes while the file is being written, the temporary space will
24/// be automatically cleaned up. For this reason, there is no API to retrieve
25/// the name, for either case.
26///
27/// To more closely match the semantics of [`tempfile::tempfile`], use
28/// [`crate::TempFile::new_anonymous`].
29///
30/// # File permissions
31///
32/// Unlike the tempfile crate, the default [`TempFile::new`] will use the same
33/// permissions as [`File::create_new`] in the Rust standard library.
34/// Concretely on Unix systems for example this can (depending on `umask`)
35/// result in files that are readable by all users. The rationale for this is
36/// to make it more ergonomic and natural to use this API to atomically create
37/// new files and replace existing ones. Many cases that want "private" files
38/// will prefer [`TempFile::new_anonymous`] to have the file not be accessible
39/// at all outside the current process.
40///
41/// To fully control the permissions of the resulting file, you can use
42/// [`File::set_permissions`].
43///
44/// [`tempfile::tempfile`]: https://docs.rs/tempfile/latest/tempfile/fn.tempfile.html
45/// [`tempfile::NamedTempFile`]: https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html
46/// [`File::create_new`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new
47/// [`File::set_permissions`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.File.html#method.set_permissions
48pub struct TempFile<'d> {
49    dir: &'d Dir,
50    fd: File,
51    name: Option<String>,
52}
53
54impl<'d> Debug for TempFile<'d> {
55    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
56        // Manual Debug implementation to omit the file reference and name so
57        // we don't leak the path, the same as `cap_std::fs::File`.
58        f.debug_struct("TempFile").field("dir", &self.dir).finish()
59    }
60}
61
62#[cfg(any(target_os = "android", target_os = "linux"))]
63fn new_tempfile_linux(d: &Dir, anonymous: bool) -> io::Result<Option<File>> {
64    use rustix::fs::{Mode, OFlags};
65    // openat's API uses WRONLY. There may be use cases for reading too, so let's
66    // support it.
67    let mut oflags = OFlags::CLOEXEC | OFlags::TMPFILE | OFlags::RDWR;
68    if anonymous {
69        oflags |= OFlags::EXCL;
70    }
71    // We default to 0o666, same as main rust when creating new files; this will be
72    // modified by umask: <https://github.com/rust-lang/rust/blob/44628f7273052d0bb8e8218518dacab210e1fe0d/library/std/src/sys/unix/fs.rs#L762>
73    let mode = Mode::from_raw_mode(0o666);
74    // Happy path - Linux with O_TMPFILE
75    match rustix::fs::openat(d, ".", oflags, mode) {
76        Ok(r) => Ok(Some(File::from(r))),
77        // See <https://github.com/Stebalien/tempfile/blob/1a40687e06eb656044e3d2dffa1379f04b3ef3fd/src/file/imp/unix.rs#L81>
78        Err(rustix::io::Errno::OPNOTSUPP | rustix::io::Errno::ISDIR | rustix::io::Errno::NOENT) => {
79            Ok(None)
80        }
81        Err(e) => Err(e.into()),
82    }
83}
84
85/// Assign a random name to a currently anonymous O_TMPFILE descriptor.
86#[cfg(any(target_os = "android", target_os = "linux"))]
87fn generate_name_in(subdir: &Dir, f: &File) -> io::Result<String> {
88    use rustix::fd::AsFd;
89    use rustix::fs::AtFlags;
90    let procself_fd = rustix::procfs::proc_self_fd()?;
91    let fdnum = rustix::path::DecInt::from_fd(f.as_fd());
92    let fdnum = fdnum.as_c_str();
93    super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
94        rustix::fs::linkat(procself_fd, fdnum, subdir, name, AtFlags::SYMLINK_FOLLOW)
95            .map_err(Into::into)
96    })
97    .map(|(_, name)| name)
98}
99
100/// Create a new temporary file in the target directory, which may or may not
101/// have a (randomly generated) name at this point. If anonymous is specified,
102/// the file will be deleted
103fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option<String>)> {
104    // On Linux, try O_TMPFILE
105    #[cfg(any(target_os = "android", target_os = "linux"))]
106    if let Some(f) = new_tempfile_linux(d, anonymous)? {
107        return Ok((f, None));
108    }
109    // Otherwise, fall back to just creating a randomly named file.
110    let mut opts = cap_std::fs::OpenOptions::new();
111    opts.read(true);
112    opts.write(true);
113    opts.create_new(true);
114    let (f, name) = super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
115        d.open_with(name, &opts)
116    })?;
117    if anonymous {
118        d.remove_file(name)?;
119        Ok((f, None))
120    } else {
121        Ok((f, Some(name)))
122    }
123}
124
125impl<'d> TempFile<'d> {
126    /// Crate a new temporary file in the provided directory.
127    pub fn new(dir: &'d Dir) -> io::Result<Self> {
128        let (fd, name) = new_tempfile(dir, false)?;
129        Ok(Self { dir, fd, name })
130    }
131
132    /// Crate a new temporary file in the provided directory that will not have
133    /// a name. This corresponds to [`tempfile::tempfile_in`].
134    ///
135    /// [`tempfile::tempfile_in`]: https://docs.rs/tempfile/latest/tempfile/fn.tempfile_in.html
136    pub fn new_anonymous(dir: &'d Dir) -> io::Result<File> {
137        new_tempfile(dir, true).map(|v| v.0)
138    }
139
140    /// Get a reference to the underlying file.
141    pub fn as_file(&self) -> &File {
142        &self.fd
143    }
144
145    /// Get a mutable reference to the underlying file.
146    pub fn as_file_mut(&mut self) -> &mut File {
147        &mut self.fd
148    }
149
150    fn impl_replace(mut self, destname: &OsStr) -> io::Result<()> {
151        // At this point on Linux if O_TMPFILE is used, we need to give the file a
152        // temporary name in order to link it into place. There are patches to
153        // add an `AT_LINKAT_REPLACE` API. With that we could skip this and
154        // have file-leak-proof atomic file replacement: <https://marc.info/?l=linux-fsdevel&m=158028833007418&w=2>
155        #[cfg(any(target_os = "android", target_os = "linux"))]
156        let tempname = self
157            .name
158            .take()
159            .map(Ok)
160            .unwrap_or_else(|| generate_name_in(self.dir, &self.fd))?;
161        // SAFETY: We only support anonymous files on Linux, so the file must have a
162        // name here.
163        #[cfg(not(any(target_os = "android", target_os = "linux")))]
164        let tempname = self.name.take().unwrap();
165        // And try the rename into place.
166        self.dir.rename(&tempname, self.dir, destname).map_err(|e| {
167            // But, if we catch an error here, then move ownership back into self,
168            // which means the Drop invocation will clean it up.
169            self.name = Some(tempname);
170            e
171        })
172    }
173
174    /// Write the file to the target directory with the provided name.
175    /// Any existing file will be replaced.
176    ///
177    /// The file permissions will default to read-only.
178    pub fn replace(self, destname: impl AsRef<OsStr>) -> io::Result<()> {
179        let destname = destname.as_ref();
180        self.impl_replace(destname)
181    }
182}
183
184impl<'d> Read for TempFile<'d> {
185    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
186        self.as_file_mut().read(buf)
187    }
188}
189
190impl<'d> Write for TempFile<'d> {
191    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
192        self.as_file_mut().write(buf)
193    }
194
195    #[inline]
196    fn flush(&mut self) -> io::Result<()> {
197        self.as_file_mut().flush()
198    }
199}
200
201impl<'d> Seek for TempFile<'d> {
202    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
203        self.as_file_mut().seek(pos)
204    }
205}
206
207impl<'d> Drop for TempFile<'d> {
208    fn drop(&mut self) {
209        if let Some(name) = self.name.take() {
210            let _ = self.dir.remove_file(name);
211        }
212    }
213}
214
215#[cfg(test)]
216mod test {
217    use super::*;
218
219    /// On Unix, calling `umask()` actually *mutates* the process global state.
220    /// This uses Linux `/proc` to read the current value.
221    #[cfg(any(target_os = "android", target_os = "linux"))]
222    fn get_process_umask() -> io::Result<u32> {
223        use io::BufRead;
224        let status = std::fs::File::open("/proc/self/status")?;
225        let bufr = io::BufReader::new(status);
226        for line in bufr.lines() {
227            let line = line?;
228            let l = if let Some(v) = line.split_once(':') {
229                v
230            } else {
231                continue;
232            };
233            let (k, v) = l;
234            if k != "Umask" {
235                continue;
236            }
237            return Ok(u32::from_str_radix(v.trim(), 8).unwrap());
238        }
239        panic!("Could not determine process umask")
240    }
241
242    /// Older Windows versions don't support removing open files
243    fn os_supports_unlinked_tmp(d: &Dir) -> bool {
244        if cfg!(not(windows)) {
245            return true;
246        }
247        let name = "testfile";
248        let _f = d.create(name).unwrap();
249        d.remove_file(name).and_then(|_| d.create(name)).is_ok()
250    }
251
252    #[test]
253    fn test_tempfile() -> io::Result<()> {
254        use crate::ambient_authority;
255
256        let td = crate::tempdir(ambient_authority())?;
257
258        // Base case, verify we clean up on drop
259        let tf = TempFile::new(&td).unwrap();
260        drop(tf);
261        assert_eq!(td.entries()?.into_iter().count(), 0);
262
263        let mut tf = TempFile::new(&td)?;
264        // Test that we created with the right permissions
265        #[cfg(any(target_os = "android", target_os = "linux"))]
266        {
267            use cap_std::fs_utf8::MetadataExt;
268            use rustix::fs::Mode;
269            let umask = get_process_umask()?;
270            let metadata = tf.as_file().metadata().unwrap();
271            let mode = metadata.mode();
272            let mode = Mode::from_bits_truncate(mode);
273            assert_eq!(0o666 & !umask, mode.bits() & 0o777);
274        }
275        // And that we can write
276        tf.write_all(b"hello world")?;
277        drop(tf);
278        assert_eq!(td.entries()?.into_iter().count(), 0);
279
280        let mut tf = TempFile::new(&td)?;
281        tf.write_all(b"hello world")?;
282        tf.replace("testfile").unwrap();
283        assert_eq!(td.entries()?.into_iter().count(), 1);
284
285        assert_eq!(td.read("testfile")?, b"hello world");
286
287        if os_supports_unlinked_tmp(&td) {
288            let mut tf = TempFile::new_anonymous(&td).unwrap();
289            tf.write_all(b"hello world, I'm anonymous").unwrap();
290            tf.seek(std::io::SeekFrom::Start(0)).unwrap();
291            let mut buf = String::new();
292            tf.read_to_string(&mut buf).unwrap();
293            assert_eq!(&buf, "hello world, I'm anonymous");
294        } else if cfg!(windows) {
295            eprintln!("notice: Detected older Windows");
296        }
297
298        td.close()
299    }
300}