Repository: Stebalien/tempfile (rust)

Issue:
Set temp directory (override TMPDIR environment)
Now that `std::env::set_var` is unsafe and essentially unusable anymore, it would be great if tempfile provided a method to override the global temporary directory on UNIX.


Pull request description:
feat: add the ability to override the default temporary directory
This adds a new `env` module with `override_temp_dir` and `temp_dir` functions.

- `temp_dir` will defer to `std::env::temp_dir` by default unless the temporary directory has been overridden.
- `override_temp_dir` allows the user to override the default temporary directory ONCE. Once this value has been set, it cannot be changed Care should be taken to ensure that the chosen directory is actually writable.

This API is designed for use by the application author when the application may run in an environment without a reliable global temporary directory (e.g., Android).

fixes #285

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1955a7e..82f6500 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # Changelog
 
+## 3.11.0
+
+- Add the ability to override the default temporary directory. This API shouldn't be used in general, but there are some cases where it's unavoidable.
+
 ## 3.10.1
 
 - Handle potential integer overflows in 32-bit systems when seeking/truncating "spooled" temporary files past 4GiB (2³²).
diff --git a/Cargo.toml b/Cargo.toml
index 0bfa28e..503f152 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -19,6 +19,8 @@ description = "A library for managing temporary files and directories."
 [dependencies]
 cfg-if = "1"
 fastrand = "2.0.1"
+# Not available in stdlib until 1.70, but we support 1.63 to support Debian stable.
+once_cell = { version = "1.19.0", default-features = false, features = ["std"] }
 
 [target.'cfg(any(unix, target_os = "wasi"))'.dependencies]
 rustix = { version = "0.38.31", features = ["fs"] }
diff --git a/src/dir/mod.rs b/src/dir/mod.rs
index ba1b0db..ea9017d 100644
--- a/src/dir/mod.rs
+++ b/src/dir/mod.rs
@@ -17,6 +17,9 @@ use std::{fmt, io};
 use crate::error::IoResultExt;
 use crate::Builder;
 
+#[cfg(doc)]
+use crate::env;
+
 /// Create a new temporary directory.
 ///
 /// The `tempdir` function creates a directory in the file system
@@ -39,7 +42,7 @@ use crate::Builder;
 /// use std::fs::File;
 /// use std::io::Write;
 ///
-/// // Create a directory inside of `std::env::temp_dir()`
+/// // Create a directory inside of `env::temp_dir()`
 /// let tmp_dir = tempdir()?;
 ///
 /// let file_path = tmp_dir.path().join("my-temporary-note.txt");
@@ -109,7 +112,7 @@ pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> io::Result<TempDir> {
 /// `TempDir` creates a new directory with a randomly generated name.
 ///
 /// The default constructor, [`TempDir::new()`], creates directories in
-/// the location returned by [`std::env::temp_dir()`], but `TempDir`
+/// the location returned by [`env::temp_dir()`], but `TempDir`
 /// can be configured to manage a temporary directory in any location
 /// by constructing with a [`Builder`].
 ///
@@ -144,7 +147,7 @@ pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> io::Result<TempDir> {
 /// use std::io::Write;
 /// use tempfile::TempDir;
 ///
-/// // Create a directory inside of `std::env::temp_dir()`
+/// // Create a directory inside of `env::temp_dir()`
 /// let tmp_dir = TempDir::new()?;
 /// # Ok::<(), std::io::Error>(())
 /// ```
@@ -156,7 +159,7 @@ pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> io::Result<TempDir> {
 /// use std::io::Write;
 /// use tempfile::Builder;
 ///
-/// // Create a directory inside of `std::env::temp_dir()`,
+/// // Create a directory inside of `env::temp_dir()`,
 /// // whose name will begin with 'example'.
 /// let tmp_dir = Builder::new().prefix("example").tempdir()?;
 /// # Ok::<(), std::io::Error>(())
@@ -170,7 +173,6 @@ pub fn tempdir_in<P: AsRef<Path>>(dir: P) -> io::Result<TempDir> {
 /// [`TempDir::new()`]: struct.TempDir.html#method.new
 /// [`TempDir::path()`]: struct.TempDir.html#method.path
 /// [`TempDir`]: struct.TempDir.html
-/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html
 /// [`std::fs`]: http://doc.rust-lang.org/std/fs/index.html
 /// [`std::process::exit()`]: http://doc.rust-lang.org/std/process/fn.exit.html
 pub struct TempDir {
@@ -196,7 +198,7 @@ impl TempDir {
     /// use std::io::Write;
     /// use tempfile::TempDir;
     ///
-    /// // Create a directory inside of `std::env::temp_dir()`
+    /// // Create a directory inside of `env::temp_dir()`
     /// let tmp_dir = TempDir::new()?;
     ///
     /// let file_path = tmp_dir.path().join("my-temporary-note.txt");
@@ -378,7 +380,7 @@ impl TempDir {
     /// use std::io::Write;
     /// use tempfile::TempDir;
     ///
-    /// // Create a directory inside of `std::env::temp_dir()`.
+    /// // Create a directory inside of `env::temp_dir()`.
     /// let tmp_dir = TempDir::new()?;
     /// let file_path = tmp_dir.path().join("my-temporary-note.txt");
     /// let mut tmp_file = File::create(file_path)?;
diff --git a/src/env.rs b/src/env.rs
new file mode 100644
index 0000000..86d5199
--- /dev/null
+++ b/src/env.rs
@@ -0,0 +1,43 @@
+use std::env;
+use std::path::{Path, PathBuf};
+
+// Once rust 1.70 is wide-spread (Debian stable), we can use OnceLock from stdlib.
+use once_cell::sync::OnceCell as OnceLock;
+
+static DEFAULT_TEMPDIR: OnceLock<PathBuf> = OnceLock::new();
+
+/// Override the default temporary directory (defaults to [`std::env::temp_dir`]). This function
+/// changes the _global_ default temporary directory for the entire program and should not be called
+/// except in exceptional cases where it's not configured correctly by the platform.
+///
+/// Only the first call to this function will succeed. All further calls will fail with `Err(path)`
+/// where `path` is previously set default temporary directory override.
+///
+/// **NOTE:** This function does not check if the specified directory exists and/or is writable.
+pub fn override_temp_dir(path: &Path) -> Result<(), PathBuf> {
+    let mut we_set = false;
+    let val = DEFAULT_TEMPDIR.get_or_init(|| {
+        we_set = true;
+        path.to_path_buf()
+    });
+    if we_set {
+        Ok(())
+    } else {
+        Err(val.to_owned())
+    }
+}
+
+/// Returns the default temporary directory, used for both temporary directories and files if no
+/// directory is explicitly specified.
+///
+/// This function simply delegates to [`std::env::temp_dir`] unless the default temporary directory
+/// has been override by a call to [`override_temp_dir`].
+///
+/// **NOTE:** This function does check if the returned directory exists and/or is writable.
+pub fn temp_dir() -> PathBuf {
+    DEFAULT_TEMPDIR
+        .get()
+        .map(|p| p.to_owned())
+        // Don't cache this in case the user uses std::env::set to change the temporary directory.
+        .unwrap_or_else(env::temp_dir)
+}
diff --git a/src/file/imp/unix.rs b/src/file/imp/unix.rs
index 5f2cb5d..2a3cd9a 100644
--- a/src/file/imp/unix.rs
+++ b/src/file/imp/unix.rs
@@ -1,4 +1,3 @@
-use std::env;
 use std::ffi::OsStr;
 use std::fs::{self, File, OpenOptions};
 use std::io;
@@ -40,7 +39,7 @@ fn create_unlinked(path: &Path) -> io::Result<File> {
     // shadow this to decrease the lifetime. It can't live longer than `tmp`.
     let mut path = path;
     if !path.is_absolute() {
-        let cur_dir = env::current_dir()?;
+        let cur_dir = std::env::current_dir()?;
         tmp = cur_dir.join(path);
         path = &tmp;
     }
diff --git a/src/file/mod.rs b/src/file/mod.rs
index 2aad4cd..2780ae1 100644
--- a/src/file/mod.rs
+++ b/src/file/mod.rs
@@ -1,4 +1,3 @@
-use std::env;
 use std::error;
 use std::ffi::OsStr;
 use std::fmt;
@@ -14,6 +13,7 @@ use std::os::wasi::io::{AsFd, AsRawFd, BorrowedFd, RawFd};
 use std::os::windows::io::{AsHandle, AsRawHandle, BorrowedHandle, RawHandle};
 use std::path::{Path, PathBuf};
 
+use crate::env;
 use crate::error::IoResultExt;
 use crate::Builder;
 
@@ -21,7 +21,7 @@ mod imp;
 
 /// Create a new temporary file.
 ///
-/// The file will be created in the location returned by [`std::env::temp_dir()`].
+/// The file will be created in the location returned by [`env::temp_dir()`].
 ///
 /// # Security
 ///
@@ -42,14 +42,12 @@ mod imp;
 /// use tempfile::tempfile;
 /// use std::io::Write;
 ///
-/// // Create a file inside of `std::env::temp_dir()`.
+/// // Create a file inside of `env::temp_dir()`.
 /// let mut file = tempfile()?;
 ///
 /// writeln!(file, "Brian was here. Briefly.")?;
 /// # Ok::<(), std::io::Error>(())
 /// ```
-///
-/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html
 pub fn tempfile() -> io::Result<File> {
     tempfile_in(env::temp_dir())
 }
@@ -59,7 +57,7 @@ pub fn tempfile() -> io::Result<File> {
 /// # Security
 ///
 /// This variant is secure/reliable in the presence of a pathological temporary file cleaner.
-/// If the temporary file isn't created in [`std::env::temp_dir()`] then temporary file cleaners aren't an issue.
+/// If the temporary file isn't created in [`env::temp_dir()`] then temporary file cleaners aren't an issue.
 ///
 /// # Resource Leaking
 ///
@@ -82,8 +80,6 @@ pub fn tempfile() -> io::Result<File> {
 /// writeln!(file, "Brian was here. Briefly.")?;
 /// # Ok::<(), std::io::Error>(())
 /// ```
-///
-/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html
 pub fn tempfile_in<P: AsRef<Path>>(dir: P) -> io::Result<File> {
     imp::create(dir.as_ref())
 }
@@ -363,7 +359,7 @@ impl AsRef<OsStr> for TempPath {
 /// A named temporary file.
 ///
 /// The default constructor, [`NamedTempFile::new()`], creates files in
-/// the location returned by [`std::env::temp_dir()`], but `NamedTempFile`
+/// the location returned by [`env::temp_dir()`], but `NamedTempFile`
 /// can be configured to manage a temporary file in any location
 /// by constructing with [`NamedTempFile::new_in()`].
 ///
@@ -440,7 +436,6 @@ impl AsRef<OsStr> for TempPath {
 /// [`tempfile()`]: fn.tempfile.html
 /// [`NamedTempFile::new()`]: #method.new
 /// [`NamedTempFile::new_in()`]: #method.new_in
-/// [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html
 /// [`std::process::exit()`]: http://doc.rust-lang.org/std/process/fn.exit.html
 /// [`lazy_static`]: https://github.com/rust-lang-nursery/lazy-static.rs/issues/62
 pub struct NamedTempFile<F = File> {
@@ -1017,7 +1012,7 @@ pub(crate) fn create_named(
     // Make the path absolute. Otherwise, changing directories could cause us to
     // delete the wrong file.
     if !path.is_absolute() {
-        path = env::current_dir()?.join(path)
+        path = std::env::current_dir()?.join(path)
     }
     imp::create_named(&path, open_options, permissions)
         .with_err_path(|| path.clone())
diff --git a/src/lib.rs b/src/lib.rs
index 4e78037..057b82b 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -40,7 +40,7 @@
 //! use tempfile::tempdir;
 //! use std::process::Command;
 //!
-//! // Create a directory inside of `std::env::temp_dir()`.
+//! // Create a directory inside of `env::temp_dir()`.
 //! let temp_dir = tempdir()?;
 //!
 //! // Spawn the `touch` command inside the temporary directory and collect the exit status
@@ -67,7 +67,7 @@
 //! use tempfile::tempfile;
 //! use std::io::Write;
 //!
-//! // Create a file inside of `std::env::temp_dir()`.
+//! // Create a file inside of `env::temp_dir()`.
 //! let mut file = tempfile()?;
 //!
 //! writeln!(file, "Brian was here. Briefly.")?;
@@ -82,7 +82,7 @@
 //!
 //! let text = "Brian was here. Briefly.";
 //!
-//! // Create a file inside of `std::env::temp_dir()`.
+//! // Create a file inside of `env::temp_dir()`.
 //! let mut file1 = NamedTempFile::new()?;
 //!
 //! // Re-open it.
@@ -105,7 +105,7 @@
 //! use std::fs::File;
 //! use std::io::Write;
 //!
-//! // Create a directory inside of `std::env::temp_dir()`.
+//! // Create a directory inside of `env::temp_dir()`.
 //! let dir = tempdir()?;
 //!
 //! let file_path = dir.path().join("my-temporary-note.txt");
@@ -126,7 +126,6 @@
 //! [`tempdir()`]: fn.tempdir.html
 //! [`TempDir`]: struct.TempDir.html
 //! [`NamedTempFile`]: struct.NamedTempFile.html
-//! [`std::env::temp_dir()`]: https://doc.rust-lang.org/std/env/fn.temp_dir.html
 //! [`lazy_static`]: https://github.com/rust-lang-nursery/lazy-static.rs/issues/62
 
 #![doc(
@@ -147,8 +146,8 @@ const NUM_RAND_CHARS: usize = 6;
 
 use std::ffi::OsStr;
 use std::fs::OpenOptions;
+use std::io;
 use std::path::Path;
-use std::{env, io};
 
 mod dir;
 mod error;
@@ -156,6 +155,8 @@ mod file;
 mod spooled;
 mod util;
 
+pub mod env;
+
 pub use crate::dir::{tempdir, tempdir_in, TempDir};
 pub use crate::file::{
     tempfile, tempfile_in, NamedTempFile, PathPersistError, PersistError, TempPath,
@@ -469,7 +470,7 @@ impl<'a, 'b> Builder<'a, 'b> {
         )
     }
 
-    /// Attempts to make a temporary directory inside of `env::temp_dir()` whose
+    /// Attempts to make a temporary directory inside of [`env::temp_dir()`] whose
     /// name will have the prefix, `prefix`. The directory and
     /// everything inside it will be automatically deleted once the
     /// returned `TempDir` is destroyed.
@@ -522,7 +523,7 @@ impl<'a, 'b> Builder<'a, 'b> {
         let storage;
         let mut dir = dir.as_ref();
         if !dir.is_absolute() {
-            let cur_dir = env::current_dir()?;
+            let cur_dir = std::env::current_dir()?;
             storage = cur_dir.join(dir);
             dir = &storage;
         }
@@ -540,7 +541,7 @@ impl<'a, 'b> Builder<'a, 'b> {
     /// Attempts to create a temporary file (or file-like object) using the
     /// provided closure. The closure is passed a temporary file path and
     /// returns an [`std::io::Result`]. The path provided to the closure will be
-    /// inside of [`std::env::temp_dir()`]. Use [`Builder::make_in`] to provide
+    /// inside of [`env::temp_dir()`]. Use [`Builder::make_in`] to provide
     /// a custom temporary directory. If the closure returns one of the
     /// following errors, then another randomized file path is tried:
     ///  - [`std::io::ErrorKind::AlreadyExists`]


Test patch:
diff --git a/tests/namedtempfile.rs b/tests/namedtempfile.rs
index 4b940b6..34ddc44 100644
--- a/tests/namedtempfile.rs
+++ b/tests/namedtempfile.rs
@@ -1,11 +1,10 @@
 #![deny(rust_2018_idioms)]
 
-use std::env;
 use std::ffi::{OsStr, OsString};
 use std::fs::File;
 use std::io::{Read, Seek, SeekFrom, Write};
 use std::path::{Path, PathBuf};
-use tempfile::{tempdir, Builder, NamedTempFile, TempPath};
+use tempfile::{env, tempdir, Builder, NamedTempFile, TempPath};
 
 fn exists<P: AsRef<Path>>(path: P) -> bool {
     std::fs::metadata(path.as_ref()).is_ok()
@@ -276,10 +275,10 @@ fn test_write_after_close() {
 
 #[test]
 fn test_change_dir() {
-    env::set_current_dir(env::temp_dir()).unwrap();
+    std::env::set_current_dir(env::temp_dir()).unwrap();
     let tmpfile = NamedTempFile::new_in(".").unwrap();
-    let path = env::current_dir().unwrap().join(tmpfile.path());
-    env::set_current_dir("/").unwrap();
+    let path = std::env::current_dir().unwrap().join(tmpfile.path());
+    std::env::set_current_dir("/").unwrap();
     drop(tmpfile);
     assert!(!exists(path))
 }
diff --git a/tests/tempdir.rs b/tests/tempdir.rs
index 8ca7984..2bc4001 100644
--- a/tests/tempdir.rs
+++ b/tests/tempdir.rs
@@ -10,7 +10,6 @@
 
 #![deny(rust_2018_idioms)]
 
-use std::env;
 use std::fs;
 use std::path::Path;
 use std::sync::mpsc::channel;
@@ -149,7 +148,7 @@ where
     F: FnOnce(),
 {
     let tmpdir = TempDir::new().unwrap();
-    assert!(env::set_current_dir(tmpdir.path()).is_ok());
+    assert!(std::env::set_current_dir(tmpdir.path()).is_ok());
 
     f();
 }