1use cap_std::fs::{Dir, File};
4use std::ffi::OsStr;
5use std::fmt::Debug;
6use std::io::{self, Read, Seek, Write};
7
8pub 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 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 let mut oflags = OFlags::CLOEXEC | OFlags::TMPFILE | OFlags::RDWR;
68 if anonymous {
69 oflags |= OFlags::EXCL;
70 }
71 let mode = Mode::from_raw_mode(0o666);
74 match rustix::fs::openat(d, ".", oflags, mode) {
76 Ok(r) => Ok(Some(File::from(r))),
77 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#[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
100fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option<String>)> {
104 #[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 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 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 pub fn new_anonymous(dir: &'d Dir) -> io::Result<File> {
137 new_tempfile(dir, true).map(|v| v.0)
138 }
139
140 pub fn as_file(&self) -> &File {
142 &self.fd
143 }
144
145 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 #[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 #[cfg(not(any(target_os = "android", target_os = "linux")))]
164 let tempname = self.name.take().unwrap();
165 self.dir.rename(&tempname, self.dir, destname).map_err(|e| {
167 self.name = Some(tempname);
170 e
171 })
172 }
173
174 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 #[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 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 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 #[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 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}