diff --git a/cap-tempfile/src/tempfile.rs b/cap-tempfile/src/tempfile.rs index 73c2b932..55284704 100644 --- a/cap-tempfile/src/tempfile.rs +++ b/cap-tempfile/src/tempfile.rs @@ -60,7 +60,7 @@ impl<'d> Debug for TempFile<'d> { } #[cfg(any(target_os = "android", target_os = "linux"))] -fn new_tempfile_linux(d: &Dir, anonymous: bool) -> io::Result> { +fn new_tempfile_linux(d: &Dir, anonymous: bool, mode: Option) -> io::Result> { use rustix::fs::{Mode, OFlags}; // openat's API uses WRONLY. There may be use cases for reading too, so let's // support it. @@ -70,7 +70,8 @@ fn new_tempfile_linux(d: &Dir, anonymous: bool) -> io::Result> { } // We default to 0o666, same as main rust when creating new files; this will be // modified by umask: - let mode = Mode::from_raw_mode(0o666); + let mode = Mode::from(mode.unwrap_or(0o666)); + // Happy path - Linux with O_TMPFILE match rustix::fs::openat(d, ".", oflags, mode) { Ok(r) => Ok(Some(File::from(r))), @@ -100,10 +101,15 @@ fn generate_name_in(subdir: &Dir, f: &File) -> io::Result { /// Create a new temporary file in the target directory, which may or may not /// have a (randomly generated) name at this point. If anonymous is specified, /// the file will be deleted -fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option)> { +fn new_tempfile( + d: &Dir, + anonymous: bool, + #[cfg(any(target_os = "android", target_os = "linux"))] unnamed_mode: Option, + #[cfg(unix)] named_mode: Option, +) -> io::Result<(File, Option)> { // On Linux, try O_TMPFILE #[cfg(any(target_os = "android", target_os = "linux"))] - if let Some(f) = new_tempfile_linux(d, anonymous)? { + if let Some(f) = new_tempfile_linux(d, anonymous, unnamed_mode)? { return Ok((f, None)); } // Otherwise, fall back to just creating a randomly named file. @@ -111,6 +117,13 @@ fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option)> opts.read(true); opts.write(true); opts.create_new(true); + #[cfg(unix)] + { + use cap_std::fs::OpenOptionsExt; + if let Some(mode) = named_mode { + opts.mode(mode); + } + } let (f, name) = super::retry_with_name_ignoring(io::ErrorKind::AlreadyExists, |name| { d.open_with(name, &opts) })?; @@ -125,7 +138,34 @@ fn new_tempfile(d: &Dir, anonymous: bool) -> io::Result<(File, Option)> impl<'d> TempFile<'d> { /// Create a new temporary file in the provided directory. pub fn new(dir: &'d Dir) -> io::Result { - let (fd, name) = new_tempfile(dir, false)?; + let (fd, name) = new_tempfile( + dir, + false, + #[cfg(any(target_os = "android", target_os = "linux"))] + None, + #[cfg(unix)] + None, + )?; + Ok(Self { dir, fd, name }) + } + + /// Create a new temporary file in the provided directory, with the provided modes. + /// `unnamed_mode` is used when the file is unnamed (created with `O_TMPFILE`). + /// `named_mode` is used when the file is named (fallback case). + /// Process umask is taken into account for the actual file mode. + #[cfg(unix)] + pub fn new_with_modes( + dir: &'d Dir, + #[cfg(any(target_os = "android", target_os = "linux"))] unnamed_mode: Option, + named_mode: Option, + ) -> io::Result { + let (fd, name) = new_tempfile( + dir, + false, + #[cfg(any(target_os = "android", target_os = "linux"))] + unnamed_mode, + named_mode, + )?; Ok(Self { dir, fd, name }) } @@ -134,7 +174,15 @@ impl<'d> TempFile<'d> { /// /// [`tempfile::tempfile_in`]: https://docs.rs/tempfile/latest/tempfile/fn.tempfile_in.html pub fn new_anonymous(dir: &'d Dir) -> io::Result { - new_tempfile(dir, true).map(|v| v.0) + new_tempfile( + dir, + true, + #[cfg(any(target_os = "android", target_os = "linux"))] + None, + #[cfg(unix)] + Some(0o000), + ) + .map(|v| v.0) } /// Get a reference to the underlying file. @@ -147,6 +195,11 @@ impl<'d> TempFile<'d> { &mut self.fd } + /// Returns whether the tempfile is named (visible in the folder) + pub fn is_named(&self) -> bool { + self.name.is_some() + } + 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 @@ -264,13 +317,10 @@ mod test { // Test that we created with the right permissions #[cfg(any(target_os = "android", target_os = "linux"))] { - use cap_std::fs_utf8::MetadataExt; - use rustix::fs::Mode; + use cap_std::fs::MetadataExt; 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); + let mode = tf.as_file().metadata()?.mode(); + assert_eq!(0o666 & !umask, mode & 0o777); } // And that we can write tf.write_all(b"hello world")?; @@ -295,6 +345,29 @@ mod test { eprintln!("notice: Detected older Windows"); } + // Test that we can create with 0o000 mode + #[cfg(any(target_os = "android", target_os = "linux"))] + { + use cap_std::fs::MetadataExt; + let mut tf = TempFile::new_with_modes(&td, Some(0o000), Some(0o000))?; + assert_eq!(tf.as_file().metadata()?.mode() & 0o777, 0o000); + tf.write_all(b"mode 0")?; + tf.replace("testfile")?; + let metadata = td.metadata("testfile")?; + assert_eq!(metadata.len(), 6); + assert_eq!(metadata.mode() & 0o777, 0o000); + } + + // Test that mode is limited by umask + #[cfg(any(target_os = "android", target_os = "linux"))] + { + use cap_std::fs::MetadataExt; + let tf = TempFile::new_with_modes(&td, Some(0o777), Some(0o777))?; + let umask = get_process_umask()?; + assert_ne!(umask & 0o777, 0o000); + assert_eq!(tf.as_file().metadata()?.mode() & 0o777, 0o777 & !umask); + } + td.close() } }