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 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269
//! Temporary files.
use cap_std::fs::{Dir, File};
use std::ffi::OsStr;
use std::fmt::Debug;
use std::io::{self, Read, Seek, Write};
/// A file in a directory that is by default deleted when it goes out
/// of scope, but may also be written persistently.
///
/// This corresponds most closely to [`tempfile::NamedTempFile`]; however,
/// there are some important differences, so read the below carefully
/// to understand how to port existing code.
///
/// # Name-able, but not necessarily named
///
/// By default, the file does not necessarily have an name until the file is written
/// persistently.
///
/// On some operating systems like Linux, it is possible to create anonymous
/// temporary files that can still be written to disk persistently via `O_TMPFILE`.
/// The advantage of this is that if the process (or operating system) crashes
/// while the file is being written, the temporary space will be automatically cleaned up.
/// For this reason, there is no API to retrieve the name, for either case.
///
/// To more closely match the semantics of [`tempfile::tempfile`], use [`crate::TempFile::new_anonymous`].
///
/// # File permissions
///
/// Unlike the tempfile crate, the default [`TempFile::new`] will use the same permissions as [`File::create_new`] in
/// the Rust standard library. Concretely on Unix systems for example this can (depending on `umask`) result in
/// files that are readable by all users. The rationale for this is to make it more ergonomic and natural to use this API to
/// atomically create new files and replace existing ones. Many cases that want "private" files will
/// prefer [`TempFile::new_anonymous`] to have the file not be accessible at all outside the current process.
///
/// To fully control the permissions of the resulting file, you can use [`File::set_permissions`].
///
/// [`tempfile::tempfile`]: https://docs.rs/tempfile/latest/tempfile/fn.tempfile.html
/// [`tempfile::NamedTempFile`]: https://docs.rs/tempfile/latest/tempfile/struct.NamedTempFile.html
/// [`File::create_new`]: https://doc.rust-lang.org/std/fs/struct.OpenOptions.html#method.create_new
/// [`File::set_permissions`]: https://docs.rs/cap-std/latest/cap_std/fs/struct.File.html#method.set_permissions
pub struct TempFile<'d> {
dir: &'d Dir,
fd: File,
name: Option<String>,
}
impl<'d> Debug for TempFile<'d> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Manual Debug implementation to omit the file reference and name so
// we don't leak the path, the same as `cap_std::fs::File`.
f.debug_struct("TempFile").field("dir", &self.dir).finish()
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
fn new_tempfile_linux(d: &Dir) -> io::Result<Option<File>> {
use rustix::fd::FromFd;
use rustix::fs::{Mode, OFlags};
// openat's API uses WRONLY. There may be use cases for reading too, so let's support it.
let oflags = OFlags::CLOEXEC | OFlags::TMPFILE | OFlags::RDWR;
// We default to 0o666, same as main rust when creating new files; this will be modified by
// umask: https://github.com/rust-lang/rust/blob/44628f7273052d0bb8e8218518dacab210e1fe0d/library/std/src/sys/unix/fs.rs#L762
let mode = Mode::from_raw_mode(0o666);
// Happy path - Linux with O_TMPFILE
match rustix::fs::openat(d, ".", oflags, mode) {
Ok(r) => return Ok(Some(File::from_fd(r.into()))),
// See https://github.com/Stebalien/tempfile/blob/1a40687e06eb656044e3d2dffa1379f04b3ef3fd/src/file/imp/unix.rs#L81
// TODO: With newer Rust versions, this could be simplied to only write `Err` once.
Err(rustix::io::Errno::OPNOTSUPP)
| Err(rustix::io::Errno::ISDIR)
| Err(rustix::io::Errno::NOENT) => Ok(None),
Err(e) => {
return Err(e.into());
}
}
}
/// Assign a random name to a currently anonymous O_TMPFILE descriptor.
#[cfg(any(target_os = "android", target_os = "linux"))]
fn generate_name_in(subdir: &Dir, f: &File) -> io::Result<String> {
use rustix::fd::AsFd;
use rustix::fs::AtFlags;
let procself_fd = rustix::io::proc_self_fd()?;
let fdnum = rustix::path::DecInt::from_fd(&f.as_fd());
let fdnum = fdnum.as_c_str();
super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
rustix::fs::linkat(&procself_fd, fdnum, subdir, name, AtFlags::SYMLINK_FOLLOW)
.map_err(Into::into)
})
.map(|(_, name)| name)
}
/// Create a new temporary file in the target directory, which may or may not have a (randomly generated) name at this point.
fn new_tempfile(d: &Dir) -> io::Result<(File, Option<String>)> {
// On Linux, try O_TMPFILE
#[cfg(any(target_os = "android", target_os = "linux"))]
if let Some(f) = new_tempfile_linux(d)? {
return Ok((f, None));
}
// Otherwise, fall back to just creating a randomly named file.
let mut opts = cap_std::fs::OpenOptions::new();
opts.read(true);
opts.write(true);
opts.create_new(true);
super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
d.open_with(name, &opts)
})
.map(|(f, name)| (f, Some(name)))
}
impl<'d> TempFile<'d> {
/// Crate a new temporary file in the provided directory.
pub fn new(dir: &'d Dir) -> io::Result<Self> {
let (fd, name) = new_tempfile(dir)?;
Ok(Self { dir, fd, name })
}
/// Crate a new temporary file in the provided directory that will not have a
/// name. This corresponds to [`tempfile::tempfile_in`].
///
/// [`tempfile::tempfile_in`]: https://docs.rs/tempfile/latest/tempfile/fn.tempfile_in.html
pub fn new_anonymous(dir: &'d Dir) -> io::Result<File> {
let (fd, name) = new_tempfile(dir)?;
if let Some(name) = name {
dir.remove_file(name)?;
}
Ok(fd)
}
/// Get a reference to the underlying file.
pub fn as_file(&self) -> &File {
&self.fd
}
/// Get a mutable reference to the underlying file.
pub fn as_file_mut(&mut self) -> &mut File {
&mut self.fd
}
fn impl_replace(mut self, destname: &OsStr) -> io::Result<()> {
// At this point on Linux if O_TMPFILE is used, we need to give the file a temporary name in
// order to link it into place. There are patches to add an `AT_LINKAT_REPLACE`
// API. With that we could skip this and have file-leak-proof atomic file replacement:
// https://marc.info/?l=linux-fsdevel&m=158028833007418&w=2
#[cfg(any(target_os = "android", target_os = "linux"))]
let tempname = self
.name
.take()
.map(Ok)
.unwrap_or_else(|| generate_name_in(self.dir, &self.fd))?;
// SAFETY: We only support anonymous files on Linux, so the file must have a name here.
#[cfg(not(any(target_os = "android", target_os = "linux")))]
let tempname = self.name.take().unwrap();
// And try the rename into place.
self.dir.rename(&tempname, self.dir, destname).map_err(|e| {
// But, if we catch an error here, then move ownership back into self,
// which means the Drop invocation will clean it up.
self.name = Some(tempname);
e
})
}
/// Write the file to the target directory with the provided name.
/// Any existing file will be replaced.
///
/// The file permissions will default to read-only.
pub fn replace(self, destname: impl AsRef<OsStr>) -> io::Result<()> {
let destname = destname.as_ref();
self.impl_replace(destname)
}
}
impl<'d> Read for TempFile<'d> {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.as_file_mut().read(buf)
}
}
impl<'d> Write for TempFile<'d> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.as_file_mut().write(buf)
}
#[inline]
fn flush(&mut self) -> io::Result<()> {
self.as_file_mut().flush()
}
}
impl<'d> Seek for TempFile<'d> {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
self.as_file_mut().seek(pos)
}
}
impl<'d> Drop for TempFile<'d> {
fn drop(&mut self) {
if let Some(name) = self.name.take() {
let _ = self.dir.remove_file(name);
}
}
}
#[cfg(test)]
mod test {
use super::*;
use std::io;
/// On Unix, calling `umask()` actually *mutates* the process global state.
/// This uses Linux `/proc` to read the current value.
#[cfg(any(target_os = "android", target_os = "linux"))]
fn get_process_umask() -> io::Result<u32> {
use io::BufRead;
let status = std::fs::File::open("/proc/self/status")?;
let bufr = io::BufReader::new(status);
for line in bufr.lines() {
let line = line?;
let l = if let Some(v) = line.split_once(':') {
v
} else {
continue;
};
let (k, v) = l;
if k != "Umask" {
continue;
}
return Ok(u32::from_str_radix(v.trim(), 8).unwrap());
}
panic!("Could not determine process umask")
}
#[test]
fn test_tempfile() -> io::Result<()> {
use crate::ambient_authority;
let td = crate::tempdir(ambient_authority())?;
// Base case, verify we clean up on drop
let tf = TempFile::new(&td).unwrap();
drop(tf);
assert_eq!(td.entries()?.into_iter().count(), 0);
let mut tf = TempFile::new(&td)?;
// Test that we created with the right permissions
#[cfg(any(target_os = "android", target_os = "linux"))]
{
use rustix::fs::{MetadataExt, Mode};
let umask = get_process_umask()?;
let metadata = tf.as_file().metadata().unwrap();
let mode = metadata.mode();
let mode = Mode::from_bits_truncate(mode);
assert_eq!(0o666 & !umask, mode.bits());
}
// And that we can write
tf.write_all(b"hello world")?;
drop(tf);
assert_eq!(td.entries()?.into_iter().count(), 0);
let mut tf = TempFile::new(&td)?;
tf.write_all(b"hello world")?;
tf.replace("testfile").unwrap();
assert_eq!(td.entries()?.into_iter().count(), 1);
assert_eq!(td.read("testfile")?, b"hello world");
td.close()
}
}