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    // For anonymous files, open with no permissions to discourage other
72    // processes from opening them.
73    //
74    // For named files, default to 0o666, same as main rust when creating new
75    // files; this will be modified by umask:
76    // <https://github.com/rust-lang/rust/blob/44628f7273052d0bb8e8218518dacab210e1fe0d/library/std/src/sys/unix/fs.rs#L762>
77    let mode = if anonymous {
78        Mode::from_raw_mode(0o000)
79    } else {
80        Mode::from_raw_mode(0o666)
81    };
82    // Happy path - Linux with O_TMPFILE
83    match rustix::fs::openat(d, ".", oflags, mode) {
84        Ok(r) => Ok(Some(File::from(r))),
85        // See <https://github.com/Stebalien/tempfile/blob/1a40687e06eb656044e3d2dffa1379f04b3ef3fd/src/file/imp/unix.rs#L81>
86        Err(rustix::io::Errno::OPNOTSUPP | rustix::io::Errno::ISDIR | rustix::io::Errno::NOENT) => {
87            Ok(None)
88        }
89        Err(e) => Err(e.into()),
90    }
91}
92
93/// Assign a random name to a currently anonymous O_TMPFILE descriptor.
94#[cfg(any(target_os = "android", target_os = "linux"))]
95fn generate_name_in(subdir: &Dir, f: &File) -> io::Result<String> {
96    use rustix::fd::AsFd;
97    use rustix::fs::AtFlags;
98    let procself_fd = rustix_linux_procfs::proc_self_fd()?;
99    let fdnum = rustix::path::DecInt::from_fd(f.as_fd());
100    let fdnum = fdnum.as_c_str();
101    super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
102        rustix::fs::linkat(procself_fd, fdnum, subdir, name, AtFlags::SYMLINK_FOLLOW)
103            .map_err(Into::into)
104    })
105    .map(|(_, name)| name)
106}
107
108/// Create a new temporary file in the target directory, which may or may not
109/// have a (randomly generated) name at this point. If anonymous is specified,
110/// the file will be deleted
111fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option<String>)> {
112    // On Linux, try O_TMPFILE
113    #[cfg(any(target_os = "android", target_os = "linux"))]
114    if let Some(f) = new_tempfile_linux(d, anonymous)? {
115        return Ok((f, None));
116    }
117    // Otherwise, fall back to just creating a randomly named file.
118    let mut opts = cap_std::fs::OpenOptions::new();
119    opts.read(true);
120    opts.write(true);
121    opts.create_new(true);
122    #[cfg(unix)]
123    if anonymous {
124        use cap_std::fs::OpenOptionsExt;
125        opts.mode(0);
126    }
127    #[cfg(windows)]
128    if anonymous {
129        use cap_std::fs::OpenOptionsExt;
130        use windows_sys::Win32::Storage::FileSystem::{
131            FILE_ATTRIBUTE_TEMPORARY, FILE_FLAG_DELETE_ON_CLOSE,
132        };
133        opts.share_mode(0);
134        opts.custom_flags(FILE_ATTRIBUTE_TEMPORARY | FILE_FLAG_DELETE_ON_CLOSE);
135    }
136    let (f, name) = super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
137        d.open_with(name, &opts)
138    })?;
139    if anonymous {
140        // On Windows we use `FILE_FLAG_DELETE_ON_CLOSE` instead.
141        #[cfg(not(windows))]
142        {
143            d.remove_file(name)?;
144        }
145        Ok((f, None))
146    } else {
147        Ok((f, Some(name)))
148    }
149}
150
151impl<'d> TempFile<'d> {
152    /// Create a new temporary file in the provided directory.
153    pub fn new(dir: &'d Dir) -> io::Result<Self> {
154        let (fd, name) = new_tempfile(dir, false)?;
155        Ok(Self { dir, fd, name })
156    }
157
158    /// Create a new temporary file in the provided directory that will not have
159    /// a name. This corresponds to [`tempfile::tempfile_in`].
160    ///
161    /// [`tempfile::tempfile_in`]: https://docs.rs/tempfile/latest/tempfile/fn.tempfile_in.html
162    pub fn new_anonymous(dir: &'d Dir) -> io::Result<File> {
163        new_tempfile(dir, true).map(|v| v.0)
164    }
165
166    /// Get a reference to the underlying file.
167    pub fn as_file(&self) -> &File {
168        &self.fd
169    }
170
171    /// Get a mutable reference to the underlying file.
172    pub fn as_file_mut(&mut self) -> &mut File {
173        &mut self.fd
174    }
175
176    fn impl_replace(mut self, destname: &OsStr) -> io::Result<()> {
177        // At this point on Linux if O_TMPFILE is used, we need to give the file a
178        // temporary name in order to link it into place. There are patches to
179        // add an `AT_LINKAT_REPLACE` API. With that we could skip this and
180        // have file-leak-proof atomic file replacement: <https://marc.info/?l=linux-fsdevel&m=158028833007418&w=2>
181        #[cfg(any(target_os = "android", target_os = "linux"))]
182        let tempname = self
183            .name
184            .take()
185            .map(Ok)
186            .unwrap_or_else(|| generate_name_in(self.dir, &self.fd))?;
187        // SAFETY: We only support anonymous files on Linux, so the file must have a
188        // name here.
189        #[cfg(not(any(target_os = "android", target_os = "linux")))]
190        let tempname = self.name.take().unwrap();
191        // And try the rename into place.
192        self.dir.rename(&tempname, self.dir, destname).map_err(|e| {
193            // But, if we catch an error here, then move ownership back into self,
194            // which means the Drop invocation will clean it up.
195            self.name = Some(tempname);
196            e
197        })
198    }
199
200    /// Write the file to the target directory with the provided name.
201    /// Any existing file will be replaced.
202    ///
203    /// The file permissions will default to read-only.
204    pub fn replace(self, destname: impl AsRef<OsStr>) -> io::Result<()> {
205        let destname = destname.as_ref();
206        self.impl_replace(destname)
207    }
208}
209
210impl<'d> Read for TempFile<'d> {
211    fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
212        self.as_file_mut().read(buf)
213    }
214}
215
216impl<'d> Write for TempFile<'d> {
217    fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
218        self.as_file_mut().write(buf)
219    }
220
221    #[inline]
222    fn flush(&mut self) -> io::Result<()> {
223        self.as_file_mut().flush()
224    }
225}
226
227impl<'d> Seek for TempFile<'d> {
228    fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
229        self.as_file_mut().seek(pos)
230    }
231}
232
233impl<'d> Drop for TempFile<'d> {
234    fn drop(&mut self) {
235        if let Some(name) = self.name.take() {
236            let _ = self.dir.remove_file(name);
237        }
238    }
239}
240
241#[cfg(test)]
242mod test {
243    use super::*;
244
245    /// On Unix, calling `umask()` actually *mutates* the process global state.
246    /// This uses a temporary file instead.
247    #[cfg(unix)]
248    fn get_process_umask() -> io::Result<u32> {
249        use std::os::unix::fs::{MetadataExt, OpenOptionsExt};
250
251        let d = ::tempfile::tempdir().unwrap();
252        let p = d.path().join("file");
253
254        let mut opts = std::fs::OpenOptions::new();
255        opts.read(true);
256        opts.write(true);
257        opts.create_new(true);
258        opts.mode(0o777);
259        let f = opts.open(p).unwrap();
260        let m = f.metadata().unwrap();
261        Ok(!m.mode() & 0o777)
262    }
263
264    /// Older Windows versions don't support removing open files
265    fn os_supports_unlinked_tmp(d: &Dir) -> bool {
266        if cfg!(not(windows)) {
267            return true;
268        }
269        let name = "testfile";
270        let _f = d.create(name).unwrap();
271        d.remove_file(name).and_then(|_| d.create(name)).is_ok()
272    }
273
274    #[test]
275    fn test_tempfile() -> io::Result<()> {
276        use crate::ambient_authority;
277
278        let td = crate::tempdir(ambient_authority())?;
279
280        // Base case, verify we clean up on drop
281        let tf = TempFile::new(&td).unwrap();
282        drop(tf);
283        assert_eq!(td.entries()?.into_iter().count(), 0);
284
285        let mut tf = TempFile::new(&td)?;
286        // Test that we created with the right permissions
287        #[cfg(unix)]
288        {
289            use cap_std::fs_utf8::MetadataExt;
290            use rustix::fs::Mode;
291            let umask = get_process_umask()?;
292            let metadata = tf.as_file().metadata().unwrap();
293            let mode = metadata.mode();
294            let mode = Mode::from_bits_truncate(mode as _);
295            assert_eq!(0o666 & !umask, (mode.bits() & 0o777) as _);
296        }
297        // And that we can write
298        tf.write_all(b"hello world")?;
299        drop(tf);
300        assert_eq!(td.entries()?.into_iter().count(), 0);
301
302        let mut tf = TempFile::new(&td)?;
303        tf.write_all(b"hello world")?;
304        tf.replace("testfile").unwrap();
305        assert_eq!(td.entries()?.into_iter().count(), 1);
306
307        assert_eq!(td.read("testfile")?, b"hello world");
308
309        if os_supports_unlinked_tmp(&td) {
310            let mut tf = TempFile::new_anonymous(&td).unwrap();
311            tf.write_all(b"hello world, I'm anonymous").unwrap();
312            tf.seek(std::io::SeekFrom::Start(0)).unwrap();
313            let mut buf = String::new();
314            tf.read_to_string(&mut buf).unwrap();
315            assert_eq!(&buf, "hello world, I'm anonymous");
316
317            // Test that we created with the right permissions
318            #[cfg(unix)]
319            {
320                use cap_std::fs_utf8::MetadataExt;
321                use rustix::fs::Mode;
322                let metadata = tf.metadata().unwrap();
323                let mode = metadata.mode();
324                let mode = Mode::from_bits_truncate(mode as _);
325                assert_eq!(0o000, mode.bits() & 0o777);
326            }
327        } else if cfg!(windows) {
328            eprintln!("notice: Detected older Windows");
329        }
330
331        td.close()
332    }
333}