use std::fmt;
use std::fs;
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use tempfile::NamedTempFile;
pub struct TempArtifact {
_file: NamedTempFile,
path: PathBuf,
}
impl fmt::Debug for TempArtifact {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TempArtifact")
.field("path", &self.path)
.finish_non_exhaustive()
}
}
impl TempArtifact {
pub fn new_bytes(prefix: &str, suffix: &str, bytes: &[u8]) -> std::io::Result<Self> {
let mut builder = tempfile::Builder::new();
builder.prefix(prefix).suffix(suffix);
let mut file = builder.tempfile()?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perm = fs::Permissions::from_mode(0o600);
file.as_file().set_permissions(perm)?;
}
file.as_file_mut().write_all(bytes)?;
file.as_file_mut().flush()?;
let path = file.path().to_path_buf();
Ok(Self { _file: file, path })
}
pub fn new_string(prefix: &str, suffix: &str, s: &str) -> std::io::Result<Self> {
Self::new_bytes(prefix, suffix, s.as_bytes())
}
pub fn path(&self) -> &Path {
&self.path
}
pub fn read_to_bytes(&self) -> std::io::Result<Vec<u8>> {
let mut f = fs::File::open(&self.path)?;
let mut buf = Vec::new();
f.read_to_end(&mut buf)?;
Ok(buf)
}
pub fn read_to_string(&self) -> std::io::Result<String> {
let bytes = self.read_to_bytes()?;
Ok(String::from_utf8_lossy(&bytes).to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::thread;
use std::time::Duration;
#[test]
fn new_bytes_round_trip() {
let data = vec![1u8, 2, 3, 4, 5];
let temp = TempArtifact::new_bytes("uk-test-", ".bin", &data).unwrap();
let read_back = temp.read_to_bytes().unwrap();
assert_eq!(read_back, data);
}
#[test]
fn new_string_round_trip() {
let text = "hello temp";
let temp = TempArtifact::new_string("uk-test-", ".txt", text).unwrap();
let read_back = temp.read_to_string().unwrap();
assert_eq!(read_back, text);
}
#[test]
fn read_to_string_replaces_invalid_utf8() {
let bytes = [0xff, 0xfe, 0xfd];
let temp = TempArtifact::new_bytes("uk-test-", ".bin", &bytes).unwrap();
let read_back = temp.read_to_string().unwrap();
assert!(read_back.contains('\u{FFFD}'));
}
#[test]
fn tempfile_deleted_on_drop() {
let path = {
let temp = TempArtifact::new_string("uk-test-", ".txt", "cleanup").unwrap();
let path = temp.path().to_path_buf();
assert!(path.exists());
path
};
let mut attempts = 0;
loop {
thread::sleep(Duration::from_millis(10));
attempts += 1;
if !path.exists() || attempts >= 5 {
break;
}
}
assert!(!path.exists(), "tempfile should be deleted on drop");
}
#[test]
fn debug_includes_type_name() {
let temp = TempArtifact::new_string("uk-test-", ".txt", "dbg").unwrap();
let dbg = format!("{:?}", temp);
assert!(dbg.contains("TempArtifact"));
}
}