Skip to main content

uselesskey_core_sink/
lib.rs

1//! Sink types for writing key material to temporary files or in-memory buffers.
2//!
3//! This crate provides [`TempArtifact`], a tempfile-backed container that holds
4//! generated key material on disk and cleans up automatically on drop.  It is
5//! useful when downstream libraries require `Path`-based APIs rather than
6//! in-memory byte slices.
7
8#![forbid(unsafe_code)]
9#![warn(missing_docs)]
10
11use std::fmt;
12use std::fs;
13use std::io::{Read, Write};
14use std::path::{Path, PathBuf};
15
16use tempfile::NamedTempFile;
17
18/// A tempfile-backed artifact that cleans up on drop.
19///
20/// Useful when downstream libraries insist on `Path`-based APIs.
21/// The temporary file is automatically deleted when the `TempArtifact` is dropped.
22///
23/// # Examples
24///
25/// ```
26/// use uselesskey_core_sink::TempArtifact;
27///
28/// // Create a temp file with string content
29/// let temp = TempArtifact::new_string("prefix-", ".pem", "-----BEGIN KEY-----\n").unwrap();
30///
31/// // Get the path to pass to other libraries
32/// let path = temp.path();
33/// assert!(path.exists());
34///
35/// // Read the content back
36/// let content = temp.read_to_string().unwrap();
37/// assert!(content.contains("BEGIN KEY"));
38///
39/// // File is deleted when `temp` goes out of scope
40/// ```
41pub struct TempArtifact {
42    /// The temp file handle; kept to ensure cleanup on drop.
43    _file: NamedTempFile,
44    path: PathBuf,
45}
46
47impl fmt::Debug for TempArtifact {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        f.debug_struct("TempArtifact")
50            .field("path", &self.path)
51            .finish_non_exhaustive()
52    }
53}
54
55impl TempArtifact {
56    /// Create a new temporary artifact with the provided bytes.
57    ///
58    /// The file is created with a name like `{prefix}XXXXXX{suffix}` where `XXXXXX`
59    /// is random characters.
60    ///
61    /// # Examples
62    ///
63    /// ```
64    /// use uselesskey_core_sink::TempArtifact;
65    ///
66    /// let der_bytes = vec![0x30, 0x82, 0x01, 0x22];
67    /// let temp = TempArtifact::new_bytes("key-", ".der", &der_bytes).unwrap();
68    ///
69    /// let read_back = temp.read_to_bytes().unwrap();
70    /// assert_eq!(read_back, der_bytes);
71    /// ```
72    pub fn new_bytes(prefix: &str, suffix: &str, bytes: &[u8]) -> std::io::Result<Self> {
73        let mut builder = tempfile::Builder::new();
74        builder.prefix(prefix).suffix(suffix);
75
76        let mut file = builder.tempfile()?;
77
78        #[cfg(unix)]
79        {
80            use std::os::unix::fs::PermissionsExt;
81            let perm = fs::Permissions::from_mode(0o600);
82            file.as_file().set_permissions(perm)?;
83        }
84
85        file.as_file_mut().write_all(bytes)?;
86        file.as_file_mut().flush()?;
87
88        let path = file.path().to_path_buf();
89        Ok(Self { _file: file, path })
90    }
91
92    /// Create a new temporary artifact with the provided UTF-8 string.
93    ///
94    /// This is a convenience wrapper around [`new_bytes`](Self::new_bytes).
95    ///
96    /// # Examples
97    ///
98    /// ```
99    /// use uselesskey_core_sink::TempArtifact;
100    ///
101    /// let pem = "-----BEGIN PRIVATE KEY-----\nMIIBVQ==\n-----END PRIVATE KEY-----\n";
102    /// let temp = TempArtifact::new_string("key-", ".pem", pem).unwrap();
103    ///
104    /// assert!(temp.path().extension().unwrap() == "pem");
105    /// ```
106    pub fn new_string(prefix: &str, suffix: &str, s: &str) -> std::io::Result<Self> {
107        Self::new_bytes(prefix, suffix, s.as_bytes())
108    }
109
110    /// Returns the path to the temporary file.
111    ///
112    /// This path can be passed to libraries that require file paths.
113    /// The file exists as long as this `TempArtifact` is alive.
114    ///
115    /// # Examples
116    ///
117    /// ```
118    /// use uselesskey_core_sink::TempArtifact;
119    ///
120    /// let temp = TempArtifact::new_string("test-", ".txt", "hello").unwrap();
121    /// let path = temp.path();
122    ///
123    /// assert!(path.exists());
124    /// assert!(path.is_file());
125    /// ```
126    pub fn path(&self) -> &Path {
127        &self.path
128    }
129
130    /// Read the file contents as bytes.
131    ///
132    /// # Examples
133    ///
134    /// ```
135    /// use uselesskey_core_sink::TempArtifact;
136    ///
137    /// let data = vec![1, 2, 3, 4, 5];
138    /// let temp = TempArtifact::new_bytes("test-", ".bin", &data).unwrap();
139    ///
140    /// let read_back = temp.read_to_bytes().unwrap();
141    /// assert_eq!(read_back, data);
142    /// ```
143    pub fn read_to_bytes(&self) -> std::io::Result<Vec<u8>> {
144        let mut f = fs::File::open(&self.path)?;
145        let mut buf = Vec::new();
146        f.read_to_end(&mut buf)?;
147        Ok(buf)
148    }
149
150    /// Read the file contents as a UTF-8 string.
151    ///
152    /// Invalid UTF-8 sequences are replaced with the Unicode replacement character.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use uselesskey_core_sink::TempArtifact;
158    ///
159    /// let temp = TempArtifact::new_string("test-", ".txt", "Hello, World!").unwrap();
160    ///
161    /// let content = temp.read_to_string().unwrap();
162    /// assert_eq!(content, "Hello, World!");
163    /// ```
164    pub fn read_to_string(&self) -> std::io::Result<String> {
165        let bytes = self.read_to_bytes()?;
166        Ok(String::from_utf8_lossy(&bytes).to_string())
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use std::thread;
174    use std::time::Duration;
175
176    #[test]
177    fn new_bytes_round_trip() {
178        let data = vec![1u8, 2, 3, 4, 5];
179        let temp = TempArtifact::new_bytes("uk-test-", ".bin", &data).unwrap();
180
181        let read_back = temp.read_to_bytes().unwrap();
182        assert_eq!(read_back, data);
183    }
184
185    #[test]
186    fn new_string_round_trip() {
187        let text = "hello temp";
188        let temp = TempArtifact::new_string("uk-test-", ".txt", text).unwrap();
189
190        let read_back = temp.read_to_string().unwrap();
191        assert_eq!(read_back, text);
192    }
193
194    #[test]
195    fn read_to_string_replaces_invalid_utf8() {
196        let bytes = [0xff, 0xfe, 0xfd];
197        let temp = TempArtifact::new_bytes("uk-test-", ".bin", &bytes).unwrap();
198
199        let read_back = temp.read_to_string().unwrap();
200        assert!(read_back.contains('\u{FFFD}'));
201    }
202
203    #[test]
204    fn tempfile_deleted_on_drop() {
205        let path = {
206            let temp = TempArtifact::new_string("uk-test-", ".txt", "cleanup").unwrap();
207            let path = temp.path().to_path_buf();
208            assert!(path.exists());
209            path
210        };
211
212        let mut attempts = 0;
213        loop {
214            thread::sleep(Duration::from_millis(10));
215            attempts += 1;
216            if !path.exists() || attempts >= 5 {
217                break;
218            }
219        }
220
221        assert!(!path.exists(), "tempfile should be deleted on drop");
222    }
223
224    #[test]
225    fn debug_includes_type_name() {
226        let temp = TempArtifact::new_string("uk-test-", ".txt", "dbg").unwrap();
227        let dbg = format!("{:?}", temp);
228        assert!(dbg.contains("TempArtifact"));
229    }
230}