#![cfg(all(feature = "aes", feature = "lzma2"))]
use std::io::Cursor;
use zesven::read::Archive;
use zesven::{ArchivePath, Error, Password, PasswordDetectionMethod, WriteOptions, Writer};
fn test_content() -> Vec<u8> {
b"This is secret content for encryption testing.".to_vec()
}
fn create_header_encrypted_archive(password: &str) -> Vec<u8> {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(WriteOptions::new().password(password).encrypt_header(true));
let path = ArchivePath::new("secret.txt").expect("Invalid path");
writer
.add_bytes(path, &test_content())
.expect("Failed to add entry");
let _ = writer.finish().expect("Failed to finish archive");
}
archive_bytes
}
#[test]
fn test_header_encryption_correct_password_succeeds() {
let password = "correct_password";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let archive =
Archive::open_with_password(cursor, password).expect("Should open with correct password");
assert_eq!(archive.len(), 1, "Should have 1 entry");
let entry = &archive.entries()[0];
assert_eq!(entry.path.as_str(), "secret.txt");
assert_eq!(entry.size, test_content().len() as u64);
}
#[test]
fn test_header_encryption_full_roundtrip() {
let password = "roundtrip_test_password";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive =
Archive::open_with_password(cursor, password).expect("Should open with correct password");
let info = archive.info();
assert_eq!(info.entry_count, 1);
assert!(
info.has_encrypted_header,
"Archive should report header is encrypted"
);
let entry = &archive.entries()[0];
assert_eq!(entry.path.as_str(), "secret.txt");
assert!(!entry.is_directory);
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with correct password");
assert_eq!(
extracted,
test_content(),
"Decrypted content should match original"
);
}
#[test]
fn test_header_encryption_wrong_password_rejected() {
let correct_password = "correct_password";
let wrong_password = "wrong_password";
let archive_bytes = create_header_encrypted_archive(correct_password);
let cursor = Cursor::new(&archive_bytes);
match Archive::open_with_password(cursor, wrong_password) {
Ok(_) => {
panic!("Opening header-encrypted archive with wrong password should fail");
}
Err(Error::WrongPassword {
detection_method, ..
}) => {
assert_eq!(
detection_method,
PasswordDetectionMethod::EarlyHeaderValidation,
"Expected early header validation for header encryption"
);
}
Err(Error::InvalidFormat(msg)) => {
assert!(
!msg.is_empty(),
"InvalidFormat error should have descriptive message"
);
}
Err(Error::Io(io_err)) => {
let acceptable_kinds = [
std::io::ErrorKind::UnexpectedEof,
std::io::ErrorKind::InvalidData,
std::io::ErrorKind::InvalidInput,
];
assert!(
acceptable_kinds.contains(&io_err.kind()),
"Unexpected IO error kind: {:?}",
io_err.kind()
);
}
Err(other) => {
panic!(
"Unexpected error type when opening with wrong password: {:?}\n\
Expected: WrongPassword, InvalidFormat, or Io (UnexpectedEof/InvalidData)",
other
);
}
}
}
#[test]
fn test_header_encryption_no_password() {
let password = "secret_password";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
match Archive::open(cursor) {
Ok(_) => {
panic!("Opening header-encrypted archive without password should fail");
}
Err(Error::PasswordRequired) => {
}
Err(e) => {
eprintln!("Got error (acceptable): {e}");
}
}
}
#[test]
fn test_header_encryption_empty_password_rejected() {
let correct_password = "non_empty_password";
let archive_bytes = create_header_encrypted_archive(correct_password);
let cursor = Cursor::new(&archive_bytes);
let result = Archive::open_with_password(cursor, "");
assert!(
result.is_err(),
"Empty password should not decrypt header-encrypted archive"
);
}
#[test]
fn test_header_encryption_long_password_succeeds() {
let password: String = "a".repeat(100);
let archive_bytes = create_header_encrypted_archive(&password);
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open_with_password(cursor, password.as_str())
.expect("Should open with correct long password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with correct password");
assert_eq!(extracted, test_content(), "Decrypted content should match");
}
#[test]
fn test_header_encryption_mixed_whitespace_password_succeeds() {
let password = " \t\n \t\n "; let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open_with_password(cursor, password)
.expect("Should open with mixed whitespace password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with mixed whitespace password");
assert_eq!(extracted, test_content(), "Decrypted content should match");
}
#[test]
fn test_write_with_empty_password_behavior() {
let mut archive_bytes = Vec::new();
{
let cursor = Cursor::new(&mut archive_bytes);
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(WriteOptions::new().password("").encrypt_header(true));
let path = ArchivePath::new("test.txt").expect("valid path");
writer
.add_bytes(path, b"test content")
.expect("add_bytes should succeed with empty password");
let _result = writer
.finish()
.expect("finish should succeed with empty password");
}
let cursor = Cursor::new(&archive_bytes);
let open_result = Archive::open(cursor.clone());
assert!(
open_result.is_err(),
"Archive encrypted with empty password should require a password to open"
);
let mut archive = Archive::open_with_password(cursor, "")
.expect("Archive created with empty password should open with empty password");
assert!(
archive.info().has_encrypted_header,
"Archive should have encrypted header"
);
let extracted = archive
.extract_to_vec("test.txt")
.expect("Should extract file from archive opened with empty password");
assert_eq!(extracted, b"test content");
}
#[test]
fn test_data_encryption_roundtrip() {
let cursor = Cursor::new(Vec::new());
let password = "test_password";
let test_content = b"test content for data encryption";
let mut writer = Writer::create(cursor)
.expect("Failed to create writer")
.options(
WriteOptions::new().password(password).encrypt_data(true), );
let path = ArchivePath::new("test.txt").expect("valid path");
writer
.add_bytes(path, test_content)
.expect("Should write encrypted content");
let (_result, cursor) = writer.finish_into_inner().expect("Should finish archive");
let archive_data = cursor.into_inner();
let mut archive =
Archive::open_with_password(Cursor::new(archive_data), Password::new(password))
.expect("Should open encrypted archive");
let extracted = archive
.extract_to_vec("test.txt")
.expect("Should extract encrypted content");
assert_eq!(
extracted, test_content,
"Decrypted content should match original"
);
}
#[test]
fn test_password_encoding_edge_cases() {
let password = "test\0null\0bytes";
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive =
Archive::open_with_password(cursor, password).expect("Should open with null byte password");
let extracted = archive
.extract_to_vec("secret.txt")
.expect("Should extract with null byte password");
assert_eq!(extracted, test_content());
}
#[test]
fn test_password_unicode_codepoints() {
let test_cases = [
("cafe\u{0301}", "combining character"),
(
"family\u{1F468}\u{200D}\u{1F469}\u{200D}\u{1F467}",
"emoji sequence",
),
("كلمة_السر_123", "RTL characters"),
("test\u{1F4A9}\u{10000}end", "supplementary characters"),
];
for (password, description) in test_cases {
let archive_bytes = create_header_encrypted_archive(password);
let cursor = Cursor::new(&archive_bytes);
let mut archive = Archive::open_with_password(cursor, password)
.unwrap_or_else(|e| panic!("Should open with {} password: {:?}", description, e));
let extracted = archive
.extract_to_vec("secret.txt")
.unwrap_or_else(|e| panic!("Should extract with {} password: {:?}", description, e));
assert_eq!(
extracted,
test_content(),
"Content mismatch for {} password",
description
);
}
}
#[test]
fn test_unicode_normalization_treated_as_different() {
let password_nfc = "caf\u{00E9}";
let password_nfd = "cafe\u{0301}";
assert_ne!(
password_nfc.as_bytes(),
password_nfd.as_bytes(),
"NFC and NFD should be different byte sequences"
);
let archive_bytes = create_header_encrypted_archive(password_nfc);
let cursor = Cursor::new(&archive_bytes);
let result_nfc = Archive::open_with_password(cursor, password_nfc);
assert!(result_nfc.is_ok(), "NFC password should work");
let cursor = Cursor::new(&archive_bytes);
let result_nfd = Archive::open_with_password(cursor, password_nfd);
assert!(
result_nfd.is_err(),
"NFD password should fail (different from NFC)"
);
}