From 587fcc7b89cda8abbbd1f08722b2b3e1349869c8 Mon Sep 17 00:00:00 2001 From: sternenseemann <0rpkxez4ksa01gb3typccl0i@systemli.org> Date: Sat, 13 Feb 2021 23:30:40 +0100 Subject: pkgs/sternenseemann/temp: add tiny temporary file/dir crate The crate implements a safe wrapper around mkdtemp(3) and mkstemp(3) which provide decent temporary file and directory creation. The key feature we provide is that we wrap the resulting path in a TempFile / TempDir struct. This allows us to implement the Drop trait such that the temporary directory / file is automatically deleted when the value goes out of scope in Rust which saves the programmer from cleaning up temporary artifacts themselves. The API is indeed very tiny currently, only implementing creation and AsRef which should allow all necessary path / file manipulation. --- pkgs/sternenseemann/default.nix | 11 ++ pkgs/sternenseemann/rust/default.nix | 24 +++++ pkgs/sternenseemann/rust/temp.rs | 191 +++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 pkgs/sternenseemann/rust/default.nix create mode 100644 pkgs/sternenseemann/rust/temp.rs diff --git a/pkgs/sternenseemann/default.nix b/pkgs/sternenseemann/default.nix index 23845195..8ee48ce5 100644 --- a/pkgs/sternenseemann/default.nix +++ b/pkgs/sternenseemann/default.nix @@ -33,6 +33,12 @@ let }; }; + rust = callPackage ./rust { + inherit (profpatsch) + writeRustSimpleLib + ; + }; + in lib.fix (self: { @@ -75,6 +81,11 @@ lib.fix (self: { ''; }); + inherit (rust) + temp + temp-tests + ; + # don't bother hydra with trivial text substitution scripts = dontRecurseIntoAttrs (callPackage ./scripts { inherit (writers) writeBashBin; diff --git a/pkgs/sternenseemann/rust/default.nix b/pkgs/sternenseemann/rust/default.nix new file mode 100644 index 00000000..787cf2e1 --- /dev/null +++ b/pkgs/sternenseemann/rust/default.nix @@ -0,0 +1,24 @@ +{ writeRustSimpleLib +, buildRustCrate +}: + +let + + temp-common = tests: writeRustSimpleLib "temp" { + buildTests = tests; + release = false; + verbose = true; + meta = { + description = "Tiny temp dir/file crate for rust"; + }; + } ./temp.rs; + + temp = temp-common false; + temp-tests = temp-common true; + +in { + inherit + temp + temp-tests + ; + } diff --git a/pkgs/sternenseemann/rust/temp.rs b/pkgs/sternenseemann/rust/temp.rs new file mode 100644 index 00000000..94424b56 --- /dev/null +++ b/pkgs/sternenseemann/rust/temp.rs @@ -0,0 +1,191 @@ +//! Tiny temp dir/file crate. +//! +//! This crate implements a tiny rust wrapper around libc's +//! `mkdtemp(3)` and `mkstemp(3)` which has the following +//! notable features: +//! +//! * Temporary files and directories are distinguished on +//! the type level: [`TempDir`] and [`TempFile`] are different +//! types to ensure the right kind of temporary artifact +//! is passed to a function and dealt with accordingly. +//! * The killer feature: Temporary artifacts are automatically +//! deleted as soon as the associated value goes out of +//! scope using the [`Drop`] trait, meaning a) it is impossible +//! to forget to delete a temporary artifact and b) temporary +//! artifact are cleaned up as soon as possible. +//! +//! The intended use of this crate is to create the desired +//! artifact via [`TempDir::new()`] or [`TempFile::new()`]. +//! Interfacing with the rest of rust's `std` is possible by using +//! the [`AsRef`] trait to get a [`Path`] reference for a given +//! temporary artifact. +//! +//! Note that you need to take care not to let the [`TempDir`] or +//! [`TempFile`] get dropped early while you are still using a copy +//! of the represented path which can easily happen when using +//! `temp_dir.as_ref().join("foo.txt")` to work with a temporary +//! directory. +use std::ffi::OsStr; +use std::io::{Error, ErrorKind}; +use std::os::unix::ffi::{OsStrExt, OsStringExt}; +use std::path::{Path, PathBuf}; + +// libc interaction + +#[link(name = "c")] +extern { + fn mkdtemp(template: *mut u8) -> *mut u8; + fn mkstemp(template: *mut u8) -> *mut u8; +} + +fn template(prefix: &str) -> std::io::Result> { + if prefix.contains('/') { + return Err(Error::new(ErrorKind::Other, "prefix may not contain any slashes")); + } + + // TODO(sterni): systemd support + let mut template = std::env::temp_dir(); + template.push(prefix); + + // mkdtemp and mkstemp require the template to end in 6 or more 'X's + template.set_extension("XXXXXX"); + + Ok(template.into_os_string().into_vec()) +} + +// internal implementation for files and directories + +enum TempKind { + File, + Dir, +} + +struct Temp { + path: Vec, + kind: TempKind, +} + +impl AsRef for Temp { + fn as_ref(&self) -> &Path { + OsStr::from_bytes(&self.path[..]).as_ref() + } +} + +impl Drop for Temp { + fn drop(&mut self) { + match self.kind { + TempKind::File => std::fs::remove_file(self.as_ref()), + TempKind::Dir => std::fs::remove_dir_all(self.as_ref()), + }; + } +} + +fn temp(kind: TempKind, prefix: &str) -> std::io::Result { + let mut tpl = template(prefix)?; + tpl.push(0); + let tpl_ptr: *mut u8 = tpl.as_mut_ptr(); + + + let res: *mut u8 = match kind { + TempKind::Dir => unsafe { mkdtemp(tpl_ptr) }, + TempKind::File => unsafe { mkstemp(tpl_ptr) }, + }; + + if res.is_null() { + Err(Error::last_os_error()) + } else { + // get rid of NUL byte + tpl.pop(); + + Ok(Temp { + path: tpl, + kind: kind, + }) + } +} + +// public, type safe API which wraps the internal one + +pub struct TempDir(Temp); + +impl TempDir { + /// Create a temporary directory in the directory returned + /// by [`std::env::temp_dir()`]. The temporary directory's + /// name will have the following form: `.XXXXXX`. + /// The six `X`s are replaced by random characters. + /// + /// See `mkdtemp(3)` for details of the underlying + /// implementation and possible errors. + pub fn new(prefix: &str) -> std::io::Result { + temp(TempKind::Dir, prefix).map(|t| TempDir(t)) + } +} + +impl AsRef for TempDir { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +pub struct TempFile(Temp); + +impl TempFile { + /// Create a temporary file in the directory returned + /// by [`std::env::temp_dir()`]. The temporary file's + /// name will have the following form: `.XXXXXX`. + /// The six `X`s are replaced by random characters. + /// + /// See `mkstemp(3)` for details of the underlying + /// implementation and possible errors. + pub fn new(prefix: &str) -> std::io::Result { + temp(TempKind::File, prefix).map(|t| TempFile(t)) + } +} + +impl AsRef for TempFile { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // Check that the temporary artifact is + // + // * created as expected + // * deleted after it goes out of scope + // + // for both temp files and temp dirs. + + #[test] + fn tempdir_exists() { + let temp = TempDir::new("temp-dir-test"); + assert!(temp.map(|p| p.as_ref().exists()).unwrap()) + } + + #[test] + fn tempdir_cleaned() { + let temp_copy = { + let temp = TempDir::new("temp-dir-test").unwrap(); + temp.as_ref().to_path_buf() + }; + assert!(!temp_copy.exists()) + } + + #[test] + fn tempfile_exists() { + let temp = TempFile::new("temp-file-test"); + assert!(temp.map(|p| p.as_ref().exists()).unwrap()) + } + + #[test] + fn tempfile_cleaned() { + let temp_copy = { + let temp = TempFile::new("temp-file-test").unwrap(); + temp.as_ref().to_path_buf() + }; + assert!(!temp_copy.exists()) + } +} -- cgit 1.4.1