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}