use cap_std::fs::{Dir, File};
use std::ffi::OsStr;
use std::fmt::Debug;
use std::io::{self, Read, Seek, Write};
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 {
f.debug_struct("TempFile").field("dir", &self.dir).finish()
}
}
#[cfg(any(target_os = "android", target_os = "linux"))]
fn new_tempfile_linux(d: &Dir, anonymous: bool) -> io::Result<Option<File>> {
use rustix::fs::{Mode, OFlags};
let mut oflags = OFlags::CLOEXEC | OFlags::TMPFILE | OFlags::RDWR;
if anonymous {
oflags |= OFlags::EXCL;
}
let mode = Mode::from_raw_mode(0o666);
match rustix::fs::openat(d, ".", oflags, mode) {
Ok(r) => Ok(Some(File::from(r))),
Err(rustix::io::Errno::OPNOTSUPP | rustix::io::Errno::ISDIR | rustix::io::Errno::NOENT) => {
Ok(None)
}
Err(e) => Err(e.into()),
}
}
#[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::procfs::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)
}
fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option<String>)> {
#[cfg(any(target_os = "android", target_os = "linux"))]
if let Some(f) = new_tempfile_linux(d, anonymous)? {
return Ok((f, None));
}
let mut opts = cap_std::fs::OpenOptions::new();
opts.read(true);
opts.write(true);
opts.create_new(true);
let (f, name) = super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| {
d.open_with(name, &opts)
})?;
if anonymous {
d.remove_file(name)?;
Ok((f, None))
} else {
Ok((f, Some(name)))
}
}
impl<'d> TempFile<'d> {
pub fn new(dir: &'d Dir) -> io::Result<Self> {
let (fd, name) = new_tempfile(dir, false)?;
Ok(Self { dir, fd, name })
}
pub fn new_anonymous(dir: &'d Dir) -> io::Result<File> {
new_tempfile(dir, true).map(|v| v.0)
}
pub fn as_file(&self) -> &File {
&self.fd
}
pub fn as_file_mut(&mut self) -> &mut File {
&mut self.fd
}
fn impl_replace(mut self, destname: &OsStr) -> io::Result<()> {
#[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))?;
#[cfg(not(any(target_os = "android", target_os = "linux")))]
let tempname = self.name.take().unwrap();
self.dir.rename(&tempname, self.dir, destname).map_err(|e| {
self.name = Some(tempname);
e
})
}
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::*;
#[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")
}
fn os_supports_unlinked_tmp(d: &Dir) -> bool {
if cfg!(not(windows)) {
return true;
}
let name = "testfile";
let _f = d.create(name).unwrap();
d.remove_file(name).and_then(|_| d.create(name)).is_ok()
}
#[test]
fn test_tempfile() -> io::Result<()> {
use crate::ambient_authority;
let td = crate::tempdir(ambient_authority())?;
let tf = TempFile::new(&td).unwrap();
drop(tf);
assert_eq!(td.entries()?.into_iter().count(), 0);
let mut tf = TempFile::new(&td)?;
#[cfg(any(target_os = "android", target_os = "linux"))]
{
use cap_std::fs_utf8::MetadataExt;
use rustix::fs::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() & 0o777);
}
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");
if os_supports_unlinked_tmp(&td) {
let mut tf = TempFile::new_anonymous(&td).unwrap();
tf.write_all(b"hello world, I'm anonymous").unwrap();
tf.seek(std::io::SeekFrom::Start(0)).unwrap();
let mut buf = String::new();
tf.read_to_string(&mut buf).unwrap();
assert_eq!(&buf, "hello world, I'm anonymous");
} else if cfg!(windows) {
eprintln!("notice: Detected older Windows");
}
td.close()
}
}