use std::fs::{self, File};
use std::io::{Read, Write};
use std::path::{Path, PathBuf};
use ferrocrypt::secrecy::SecretString;
use ferrocrypt::{Decryptor, Encryptor, PrivateKey, PublicKey};
use ferrocrypt_test_support::{
fast_keypair_generator, per_process_workspace, remove_per_process_workspace,
};
const TEST_WORKSPACE: &str = "tests/workspace_large_file";
const PASSPHRASE: &str = "large-file-test-passphrase";
const DEFAULT_BYTES: u64 = 4 * 1024 * 1024 * 1024 + 3;
const CHUNK: usize = 1024 * 1024;
#[ctor::dtor]
fn cleanup() {
remove_per_process_workspace(TEST_WORKSPACE);
}
fn payload_bytes() -> u64 {
match std::env::var("FERROCRYPT_LARGE_FILE_BYTES") {
Ok(v) => v
.trim()
.parse()
.expect("FERROCRYPT_LARGE_FILE_BYTES must be a byte count"),
Err(_) => DEFAULT_BYTES,
}
}
fn payload_byte(pos: u64) -> u8 {
(pos % 251) as u8
}
fn write_payload(path: &Path, len: u64) {
let mut f = File::create(path).expect("create payload");
let mut buf = vec![0u8; CHUNK];
let mut written: u64 = 0;
while written < len {
let n = std::cmp::min(CHUNK as u64, len - written) as usize;
for (i, b) in buf.iter_mut().enumerate().take(n) {
*b = payload_byte(written + i as u64);
}
f.write_all(&buf[..n]).expect("write payload chunk");
written += n as u64;
}
f.sync_all().expect("sync payload");
}
fn hash_file(path: &Path) -> (u64, u64) {
let mut f = File::open(path).expect("open for hashing");
let mut buf = vec![0u8; CHUNK];
let mut len: u64 = 0;
let mut hash: u64 = 0xcbf2_9ce4_8422_2325; loop {
let n = f.read(&mut buf).expect("read for hashing");
if n == 0 {
break;
}
len += n as u64;
for &b in &buf[..n] {
hash ^= b as u64;
hash = hash.wrapping_mul(0x0000_0100_0000_01b3); }
}
(len, hash)
}
#[test]
#[ignore = "slow-large-file: >4 GiB I/O; run in a release qualification lane"]
fn round_trip_file_larger_than_4gib() {
let len = payload_bytes();
let work = per_process_workspace(TEST_WORKSPACE).join("over_4gib");
if work.exists() {
fs::remove_dir_all(&work).unwrap();
}
fs::create_dir_all(&work).unwrap();
let keys = work.join("keys");
fs::create_dir_all(&keys).unwrap();
let pass = SecretString::from(PASSPHRASE.to_string());
let kg = fast_keypair_generator(pass.clone())
.write(&keys, |_| {})
.expect("keygen");
let input = work.join("big.bin");
write_payload(&input, len);
let (input_len, input_hash) = hash_file(&input);
assert_eq!(
input_len, len,
"payload was not written at the requested length"
);
let enc_dir = work.join("enc");
fs::create_dir_all(&enc_dir).unwrap();
let fcr: PathBuf = Encryptor::with_public_key(PublicKey::from_key_file(&kg.public_key_path))
.write(&input, &enc_dir, |_| {})
.expect("encrypt >4 GiB file")
.output_path;
fs::remove_file(&input).unwrap();
let dec_dir = work.join("dec");
fs::create_dir_all(&dec_dir).unwrap();
let out = match Decryptor::open(&fcr).expect("open") {
Decryptor::PrivateKey(d) => {
d.decrypt(
PrivateKey::from_key_file(&kg.private_key_path),
pass,
&dec_dir,
|_| {},
)
.expect("decrypt >4 GiB file")
.output_path
}
_ => panic!("expected private-key decryptor"),
};
let (out_len, out_hash) = hash_file(&out);
assert_eq!(
out_len, input_len,
"decrypted length differs from the original"
);
assert_eq!(
out_hash, input_hash,
"decrypted content differs from the original"
);
}