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 = if anonymous {
78 Mode::from_raw_mode(0o000)
79 } else {
80 Mode::from_raw_mode(0o666)
81 };
82 match rustix::fs::openat(d, ".", oflags, mode) {
84 Ok(r) => Ok(Some(File::from(r))),
85 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#[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
108fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option<String>)> {
112 #[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 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 #[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 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 pub fn new_anonymous(dir: &'d Dir) -> io::Result<File> {
163 new_tempfile(dir, true).map(|v| v.0)
164 }
165
166 pub fn as_file(&self) -> &File {
168 &self.fd
169 }
170
171 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 #[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 #[cfg(not(any(target_os = "android", target_os = "linux")))]
190 let tempname = self.name.take().unwrap();
191 self.dir.rename(&tempname, self.dir, destname).map_err(|e| {
193 self.name = Some(tempname);
196 e
197 })
198 }
199
200 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 #[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 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 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 #[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 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 #[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}