use std::fs;
use std::path::{Path, PathBuf};
use ferrocrypt::secrecy::SecretString;
use ferrocrypt::{CryptoError, Decryptor, Encryptor, PrivateKey, PublicKey};
use ferrocrypt_test_support::{fast_keypair_generator, fast_passphrase_encryptor};
const FIXTURE_PASSPHRASE: &str = "fixture-passphrase-not-secret-do-not-reuse";
const TEST_WORKSPACE: &str = "tests/workspace_fixture_stability";
const SMALL_FILE_NAME: &str = "small_file.txt";
const SMALL_DIR_NAME: &str = "small_dir";
const PASSPHRASE_FILE_FCR: &str = "small_file.passphrase.fcr";
const PASSPHRASE_DIR_FCR: &str = "small_dir.passphrase.fcr";
const RECIPIENT_FILE_FCR: &str = "small_file.recipient.fcr";
const RECIPIENT_DIR_FCR: &str = "small_dir.recipient.fcr";
const PUBLIC_KEY_FILE: &str = "public.key";
const PRIVATE_KEY_FILE: &str = "private.key";
fn fixtures_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn source_dir() -> PathBuf {
fixtures_dir().join("source")
}
fn encrypted_dir() -> PathBuf {
fixtures_dir().join("encrypted")
}
fn keys_dir() -> PathBuf {
fixtures_dir().join("keys")
}
fn fresh_temp(name: &str) -> PathBuf {
let dir = Path::new(TEST_WORKSPACE).join(name);
if dir.exists() {
fs::remove_dir_all(&dir).expect("clean fixture-stability temp");
}
fs::create_dir_all(&dir).expect("create fixture-stability temp");
dir
}
fn fixture_passphrase() -> SecretString {
SecretString::from(FIXTURE_PASSPHRASE.to_string())
}
#[ctor::dtor]
fn cleanup() {
if Path::new(TEST_WORKSPACE).exists() {
let _ = fs::remove_dir_all(TEST_WORKSPACE);
}
}
fn read_files_recursive(root: &Path) -> Vec<(PathBuf, Vec<u8>)> {
let mut out = Vec::new();
let mut stack = vec![root.to_path_buf()];
while let Some(dir) = stack.pop() {
for entry in fs::read_dir(&dir).expect("read_dir fixture tree") {
let entry = entry.expect("dir entry");
let path = entry.path();
let ft = entry.file_type().expect("file_type");
if ft.is_dir() {
stack.push(path);
} else if ft.is_file() {
let rel = path.strip_prefix(root).expect("strip_prefix").to_path_buf();
let bytes = fs::read(&path).expect("read fixture file");
out.push((rel, bytes));
}
}
}
out.sort_by(|a, b| a.0.cmp(&b.0));
out
}
fn assert_dirs_equal(expected_root: &Path, actual_root: &Path) {
let expected = read_files_recursive(expected_root);
let actual = read_files_recursive(actual_root);
let expected_paths: Vec<_> = expected.iter().map(|(p, _)| p.clone()).collect();
let actual_paths: Vec<_> = actual.iter().map(|(p, _)| p.clone()).collect();
assert_eq!(
expected_paths, actual_paths,
"fixture file set differs between expected and actual"
);
for ((path, expected_bytes), (_, actual_bytes)) in expected.iter().zip(actual.iter()) {
assert_eq!(
expected_bytes,
actual_bytes,
"fixture content drifted at {}",
path.display()
);
}
}
fn passphrase_decrypt(fcr: PathBuf, out: &Path) -> Result<(), CryptoError> {
match Decryptor::open(&fcr)? {
Decryptor::Passphrase(d) => {
d.decrypt(fixture_passphrase(), out, |_| {})?;
Ok(())
}
other => panic!("expected passphrase decryptor, got {other:?}"),
}
}
fn recipient_decrypt(fcr: PathBuf, out: &Path) -> Result<(), CryptoError> {
match Decryptor::open(&fcr)? {
Decryptor::PrivateKey(d) => {
d.decrypt(
PrivateKey::from_key_file(keys_dir().join(PRIVATE_KEY_FILE)),
fixture_passphrase(),
out,
|_| {},
)?;
Ok(())
}
other => panic!("expected private-key decryptor, got {other:?}"),
}
}
#[test]
fn decrypt_passphrase_file_fixture_matches_source() {
let out = fresh_temp("decrypt_passphrase_file");
passphrase_decrypt(encrypted_dir().join(PASSPHRASE_FILE_FCR), &out)
.expect("decrypt passphrase-file fixture");
let decrypted = fs::read(out.join(SMALL_FILE_NAME)).expect("read decrypted plaintext");
let expected = fs::read(source_dir().join(SMALL_FILE_NAME)).expect("read source plaintext");
assert_eq!(
decrypted, expected,
"passphrase-file fixture plaintext drifted"
);
}
#[test]
fn decrypt_passphrase_dir_fixture_matches_source() {
let out = fresh_temp("decrypt_passphrase_dir");
passphrase_decrypt(encrypted_dir().join(PASSPHRASE_DIR_FCR), &out)
.expect("decrypt passphrase-dir fixture");
assert_dirs_equal(
&source_dir().join(SMALL_DIR_NAME),
&out.join(SMALL_DIR_NAME),
);
}
#[test]
fn decrypt_recipient_file_fixture_matches_source() {
let out = fresh_temp("decrypt_recipient_file");
recipient_decrypt(encrypted_dir().join(RECIPIENT_FILE_FCR), &out)
.expect("decrypt recipient-file fixture");
let decrypted = fs::read(out.join(SMALL_FILE_NAME)).expect("read decrypted plaintext");
let expected = fs::read(source_dir().join(SMALL_FILE_NAME)).expect("read source plaintext");
assert_eq!(
decrypted, expected,
"recipient-file fixture plaintext drifted"
);
}
#[test]
fn decrypt_recipient_dir_fixture_matches_source() {
let out = fresh_temp("decrypt_recipient_dir");
recipient_decrypt(encrypted_dir().join(RECIPIENT_DIR_FCR), &out)
.expect("decrypt recipient-dir fixture");
assert_dirs_equal(
&source_dir().join(SMALL_DIR_NAME),
&out.join(SMALL_DIR_NAME),
);
}
#[test]
#[ignore]
fn regenerate_fixtures() {
if encrypted_dir().exists() {
fs::remove_dir_all(encrypted_dir()).expect("clean encrypted/");
}
if keys_dir().exists() {
fs::remove_dir_all(keys_dir()).expect("clean keys/");
}
fs::create_dir_all(encrypted_dir()).expect("create encrypted/");
fs::create_dir_all(keys_dir()).expect("create keys/");
let kg_outcome = fast_keypair_generator(fixture_passphrase())
.write(keys_dir(), |_| {})
.expect("generate fixture key pair");
eprintln!(
"fixture key pair regenerated; public fingerprint = {}",
kg_outcome.fingerprint
);
fast_passphrase_encryptor(fixture_passphrase())
.save_as(encrypted_dir().join(PASSPHRASE_FILE_FCR))
.write(source_dir().join(SMALL_FILE_NAME), encrypted_dir(), |_| {})
.expect("encrypt passphrase-file fixture");
fast_passphrase_encryptor(fixture_passphrase())
.save_as(encrypted_dir().join(PASSPHRASE_DIR_FCR))
.write(source_dir().join(SMALL_DIR_NAME), encrypted_dir(), |_| {})
.expect("encrypt passphrase-dir fixture");
Encryptor::with_public_key(PublicKey::from_key_file(keys_dir().join(PUBLIC_KEY_FILE)))
.save_as(encrypted_dir().join(RECIPIENT_FILE_FCR))
.write(source_dir().join(SMALL_FILE_NAME), encrypted_dir(), |_| {})
.expect("encrypt recipient-file fixture");
Encryptor::with_public_key(PublicKey::from_key_file(keys_dir().join(PUBLIC_KEY_FILE)))
.save_as(encrypted_dir().join(RECIPIENT_DIR_FCR))
.write(source_dir().join(SMALL_DIR_NAME), encrypted_dir(), |_| {})
.expect("encrypt recipient-dir fixture");
}