use std::io::{Read, Write};
use shadow_crypt_core::memory::{SecureBytes, SecureString};
use crate::{
decryption::file::{DecryptionInputFile, DecryptionOutputFile},
errors::{WorkflowError, WorkflowResult},
};
pub fn store_plaintext_file(
filename: &SecureString,
content: &SecureBytes,
output_dir: &std::path::Path,
) -> WorkflowResult<DecryptionOutputFile> {
let safe_name = std::path::Path::new(filename.as_str())
.file_name()
.ok_or_else(|| {
WorkflowError::File("Decrypted filename contains invalid path components".to_string())
})?;
let safe_name_str = safe_name
.to_str()
.ok_or_else(|| WorkflowError::File("Decrypted filename is not valid UTF-8".to_string()))?
.to_string();
let output_file = DecryptionOutputFile {
path: output_dir.join(safe_name),
filename: safe_name_str,
};
let mut f = std::fs::File::create_new(output_file.path.as_path()).map_err(|e| {
if e.kind() == std::io::ErrorKind::AlreadyExists {
WorkflowError::File(format!(
"Output file '{}' already exists",
output_file.filename
))
} else {
WorkflowError::Io(e)
}
})?;
f.write_all(content.as_slice())?;
Ok(output_file)
}
pub fn load_file_bytes(file: &DecryptionInputFile) -> WorkflowResult<Vec<u8>> {
let size: usize = file.size as usize;
let mut f = std::fs::File::open(&file.path)?;
let mut buffer: Vec<u8> = Vec::with_capacity(size);
f.read_to_end(&mut buffer)?;
Ok(buffer)
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::io::Write;
use tempfile::NamedTempFile;
#[test]
fn test_store_plaintext_file() {
let temp_dir = tempfile::TempDir::new().unwrap();
let filename = SecureString::new("test.txt".to_string());
let content = SecureBytes::new(b"test content".to_vec());
let output = store_plaintext_file(&filename, &content, temp_dir.path()).unwrap();
assert_eq!(output.filename, "test.txt");
let read_content = fs::read(&output.path).unwrap();
assert_eq!(read_content, b"test content");
}
#[test]
fn test_store_plaintext_file_path_traversal_rejected() {
let temp_dir = tempfile::TempDir::new().unwrap();
for malicious_name in &["../../etc/passwd", "../sibling", "/abs/path", ".."] {
let filename = SecureString::new(malicious_name.to_string());
let content = SecureBytes::new(b"evil".to_vec());
let result = store_plaintext_file(&filename, &content, temp_dir.path());
match malicious_name {
&".." => {
assert!(
result.is_err(),
"Expected error for filename '{malicious_name}'"
);
}
_ => {
if let Ok(output) = result {
assert!(
output.path.starts_with(temp_dir.path()),
"Output escaped temp_dir for '{malicious_name}'"
);
}
}
}
}
}
#[test]
fn test_store_plaintext_file_no_overwrite() {
let temp_dir = tempfile::TempDir::new().unwrap();
let filename = SecureString::new("test.txt".to_string());
let content = SecureBytes::new(b"new content".to_vec());
let output_path = temp_dir.path().join("test.txt");
let existing_content = b"existing content";
std::fs::write(&output_path, existing_content).unwrap();
let result = store_plaintext_file(&filename, &content, temp_dir.path());
assert!(result.is_err());
if let Err(WorkflowError::File(msg)) = result {
assert!(msg.contains("already exists"));
} else {
panic!("Expected File error");
}
let read_content = std::fs::read(&output_path).unwrap();
assert_eq!(read_content, existing_content);
}
#[test]
fn test_load_file_bytes() {
let mut temp_file = NamedTempFile::new().unwrap();
let data = b"raw file data";
temp_file.write_all(data).unwrap();
temp_file.flush().unwrap();
let input_file = DecryptionInputFile {
path: temp_file.path().to_path_buf(),
filename: "test.shadow".to_string(),
size: data.len() as u64,
};
let bytes = load_file_bytes(&input_file).unwrap();
assert_eq!(bytes, data);
}
}