docs(phase-3): research round-trip verification and testing strategy
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
620
.planning/phases/03-round-trip-verification/03-RESEARCH.md
Normal file
620
.planning/phases/03-round-trip-verification/03-RESEARCH.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# Phase 3: Round-Trip Verification - Research
|
||||
|
||||
**Researched:** 2026-02-25
|
||||
**Domain:** Rust testing infrastructure, round-trip byte fidelity, golden test vectors, pipeline unit testing
|
||||
**Confidence:** HIGH
|
||||
|
||||
## Summary
|
||||
|
||||
Phase 3 adds a comprehensive test suite to the existing `encrypted_archive` Rust CLI tool built in Phase 2. The tool already correctly implements pack/unpack/inspect commands -- manually verified during this research by running round-trip tests on: empty files, normal text, Cyrillic filenames, and 11MB binary data. All passed byte-identical verification via SHA-256 comparison.
|
||||
|
||||
The key challenge is structuring the test suite so that golden test vectors work deterministically. Since archive output varies per run (random IVs), golden vectors must test individual pipeline stages (compression, encryption, HMAC, SHA-256) with fixed inputs, not the full CLI. The project is currently a pure binary crate (`src/main.rs` only, no `src/lib.rs`), which means integration tests in `tests/` cannot import modules. The recommended approach is to add a `src/lib.rs` that re-exports all modules, then write both unit tests (inside `#[cfg(test)]` in each module) and integration tests (in `tests/` directory).
|
||||
|
||||
**Primary recommendation:** Add `src/lib.rs` to re-export modules. Use `tempfile 3.26` for temporary directories, `assert_cmd 2.1` for CLI integration tests, and `hex-literal 1.1` for readable golden vectors. Structure tests as: (1) unit tests per module in `#[cfg(test)]`, (2) integration round-trip tests in `tests/`, (3) golden vector tests in `tests/golden.rs` that call library functions directly with fixed IVs.
|
||||
|
||||
<phase_requirements>
|
||||
## Phase Requirements
|
||||
|
||||
| ID | Description | Research Support |
|
||||
|----|-------------|-----------------|
|
||||
| INT-02 | Unpacked files byte-identical to originals (round-trip fidelity) | Already verified manually during research: pack -> unpack -> diff succeeds for empty file, "Hello" text, Cyrillic filename, and 11MB binary. Tests must automate this verification with SHA-256 comparison. Need `tempfile` for temp dirs, `assert_cmd` for CLI subprocess testing. |
|
||||
| TST-01 | Round-trip tests: archive Rust -> unarchive Rust | Integration tests in `tests/round_trip.rs` using `assert_cmd::Command::cargo_bin("encrypted_archive")` to run pack then unpack, then compare file contents byte-by-byte. Test cases: empty file, small text, multiple files, large binary (>10MB), Cyrillic filename. |
|
||||
| TST-02 | Golden test vectors: known plaintext/key/IV -> expected ciphertext | Unit-level tests in `tests/golden.rs` (or `src/crypto.rs` `#[cfg(test)]` module) that call `crypto::encrypt_data()` with the project KEY and a fixed IV, then assert exact ciphertext bytes. Cross-verified with `openssl enc` during research. Also test HMAC and SHA-256 with known inputs. Requires `hex-literal` for readable hex constants. |
|
||||
| TST-03 | Unit tests for each pipeline module (compression, encryption, HMAC, format serialization/deserialization) | Unit tests inside each module: `compression.rs` (compress/decompress round-trip, `should_compress` logic), `crypto.rs` (encrypt/decrypt round-trip, HMAC compute/verify, SHA-256), `format.rs` (header write/read round-trip, TOC entry write/read round-trip, offset calculations). All functions are already `pub`. |
|
||||
</phase_requirements>
|
||||
|
||||
## Standard Stack
|
||||
|
||||
### Core
|
||||
| Library | Version | Purpose | Why Standard |
|
||||
|---------|---------|---------|--------------|
|
||||
| `tempfile` | 3.26.0 | Temporary directories/files for tests | De facto standard for Rust test fixtures. Auto-cleanup on drop. 200M+ downloads. |
|
||||
| `assert_cmd` | 2.1.2 | CLI binary integration testing | Standard for testing Rust CLI tools. Runs `cargo_bin()` as subprocess, asserts exit code/stdout/stderr. |
|
||||
| `hex-literal` | 1.1.0 | Compile-time hex byte array literals | Clean syntax for golden test vectors: `hex!("aabbccdd")` instead of `[0xaa, 0xbb, 0xcc, 0xdd]`. |
|
||||
|
||||
### Supporting
|
||||
| Library | Version | Purpose | When to Use |
|
||||
|---------|---------|---------|-------------|
|
||||
| `predicates` | 3.1.4 | Rich assertions for assert_cmd | When asserting stdout contains specific strings. Comes as transitive dep of assert_cmd. |
|
||||
|
||||
### Alternatives Considered
|
||||
| Instead of | Could Use | Tradeoff |
|
||||
|------------|-----------|----------|
|
||||
| `assert_cmd` (CLI subprocess) | Inline function calls in `tests/` | `assert_cmd` tests the actual binary as users experience it. Function calls test library code. Both are needed -- not alternatives but complementary. |
|
||||
| `hex-literal` macro | Manual `[0xaa, 0xbb, ...]` arrays | `hex-literal` is far more readable for 32-byte hashes. Minimal dependency cost. |
|
||||
| `hex` crate (runtime) | `hex-literal` (compile-time) | `hex-literal` is sufficient for test constants. `hex` adds runtime decode which we don't need. |
|
||||
| `proptest` (property-based) | Manual edge case tests | Overkill for Phase 3. Manual edge cases (empty, large, Cyrillic) are sufficient and more readable. Property-based testing can be added in v2. |
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
cargo add --dev tempfile@3.26 assert_cmd@2.1 hex-literal@1.1 predicates@3.1
|
||||
```
|
||||
|
||||
## Architecture Patterns
|
||||
|
||||
### Recommended Project Structure
|
||||
```
|
||||
encrypted_archive/
|
||||
├── Cargo.toml # Add [dev-dependencies]
|
||||
├── src/
|
||||
│ ├── lib.rs # NEW: re-exports all modules for integration tests
|
||||
│ ├── main.rs # Entry point (uses lib crate)
|
||||
│ ├── cli.rs # (add #[cfg(test)] unit tests)
|
||||
│ ├── format.rs # (add #[cfg(test)] unit tests)
|
||||
│ ├── crypto.rs # (add #[cfg(test)] unit tests + golden vectors)
|
||||
│ ├── compression.rs # (add #[cfg(test)] unit tests)
|
||||
│ ├── archive.rs # (orchestration - tested via integration tests)
|
||||
│ └── key.rs # KEY constant
|
||||
├── tests/
|
||||
│ ├── round_trip.rs # INT-02, TST-01: Full CLI round-trip tests
|
||||
│ ├── golden.rs # TST-02: Golden test vectors with fixed IV/key
|
||||
│ └── fixtures/ # Optional: pre-built test archives for regression
|
||||
│ └── README # Explain fixture purpose
|
||||
└── docs/
|
||||
└── FORMAT.md
|
||||
```
|
||||
|
||||
### Pattern 1: Library + Binary Hybrid
|
||||
**What:** Add `src/lib.rs` that re-exports modules, keep `src/main.rs` as thin entry point.
|
||||
**When to use:** Always, when integration tests need to import project modules.
|
||||
**Why:** Without `lib.rs`, integration tests in `tests/` cannot `use encrypted_archive::crypto`. The binary crate's modules are inaccessible from outside.
|
||||
**Example:**
|
||||
```rust
|
||||
// src/lib.rs
|
||||
pub mod archive;
|
||||
pub mod cli;
|
||||
pub mod compression;
|
||||
pub mod crypto;
|
||||
pub mod format;
|
||||
pub mod key;
|
||||
```
|
||||
|
||||
```rust
|
||||
// src/main.rs (updated to use library crate)
|
||||
use clap::Parser;
|
||||
use encrypted_archive::cli::{Cli, Commands};
|
||||
use encrypted_archive::archive;
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let cli = Cli::parse();
|
||||
match cli.command {
|
||||
Commands::Pack { files, output, no_compress } => archive::pack(&files, &output, &no_compress)?,
|
||||
Commands::Unpack { archive: path, output_dir } => archive::unpack(&path, &output_dir)?,
|
||||
Commands::Inspect { archive: path } => archive::inspect(&path)?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 2: Unit Tests in Module
|
||||
**What:** `#[cfg(test)] mod tests { }` blocks inside each source module.
|
||||
**When to use:** For testing individual functions in isolation (TST-03).
|
||||
**Example:**
|
||||
```rust
|
||||
// Inside src/crypto.rs
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
fn test_encrypt_decrypt_roundtrip() {
|
||||
let key: [u8; 32] = crate::key::KEY;
|
||||
let iv: [u8; 16] = [0u8; 16];
|
||||
let plaintext = b"Hello, World!";
|
||||
|
||||
let ciphertext = encrypt_data(plaintext, &key, &iv);
|
||||
let decrypted = decrypt_data(&ciphertext, &key, &iv).unwrap();
|
||||
|
||||
assert_eq!(decrypted, plaintext);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sha256_known_value() {
|
||||
// SHA-256("Hello") from FORMAT.md Section 12.3
|
||||
let expected = hex!("185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969");
|
||||
let actual = sha256_hash(b"Hello");
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 3: CLI Integration Test with assert_cmd
|
||||
**What:** Run the actual binary, verify output files match originals.
|
||||
**When to use:** For round-trip tests (TST-01, INT-02).
|
||||
**Example:**
|
||||
```rust
|
||||
// tests/round_trip.rs
|
||||
use assert_cmd::Command;
|
||||
use tempfile::tempdir;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_single_text_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let input_dir = dir.path().join("input");
|
||||
let output_dir = dir.path().join("output");
|
||||
let archive_path = dir.path().join("test.bin");
|
||||
|
||||
fs::create_dir_all(&input_dir).unwrap();
|
||||
fs::write(input_dir.join("hello.txt"), b"Hello").unwrap();
|
||||
|
||||
// Pack
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("pack")
|
||||
.arg(input_dir.join("hello.txt"))
|
||||
.arg("-o").arg(&archive_path)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Unpack
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("unpack")
|
||||
.arg(&archive_path)
|
||||
.arg("-o").arg(&output_dir)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
// Verify byte-identical
|
||||
let original = fs::read(input_dir.join("hello.txt")).unwrap();
|
||||
let extracted = fs::read(output_dir.join("hello.txt")).unwrap();
|
||||
assert_eq!(original, extracted);
|
||||
}
|
||||
```
|
||||
|
||||
### Pattern 4: Golden Test Vector with Fixed IV
|
||||
**What:** Call crypto functions directly with known key + known IV, assert exact ciphertext bytes.
|
||||
**When to use:** For golden vectors (TST-02). The binary CLI cannot inject a fixed IV, so golden tests MUST call library functions directly.
|
||||
**Why critical:** Golden vectors prove the encryption implementation matches the specification. They can be cross-verified with `openssl enc` on the command line.
|
||||
**Example:**
|
||||
```rust
|
||||
// tests/golden.rs
|
||||
use encrypted_archive::{crypto, key::KEY};
|
||||
use hex_literal::hex;
|
||||
|
||||
#[test]
|
||||
fn test_golden_aes256cbc_hello() {
|
||||
// "Hello" (5 bytes) encrypted with project KEY and fixed IV
|
||||
// Cross-verified: echo -n "Hello" | openssl enc -aes-256-cbc -nosalt \
|
||||
// -K "7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550" \
|
||||
// -iv "00000000000000000000000000000001" | xxd -p
|
||||
let iv = hex!("00000000000000000000000000000001");
|
||||
let plaintext = b"Hello";
|
||||
let expected_ct = hex!("6e66ae8bc740efeefe83b5713fcb716f");
|
||||
|
||||
let ciphertext = crypto::encrypt_data(plaintext, &KEY, &iv);
|
||||
assert_eq!(ciphertext, expected_ct);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_hmac_sha256() {
|
||||
// HMAC-SHA256 of IV || ciphertext from the AES test above
|
||||
// Cross-verified with openssl dgst
|
||||
let iv = hex!("00000000000000000000000000000001");
|
||||
let ciphertext = hex!("6e66ae8bc740efeefe83b5713fcb716f");
|
||||
let expected_hmac = hex!("0c85780b6628ba3b52654d2f6e0d9bbec67443cf2a6104eb3120ec93fc2d38d4");
|
||||
|
||||
let hmac = crypto::compute_hmac(&KEY, &iv, &ciphertext);
|
||||
assert_eq!(hmac, expected_hmac);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_golden_sha256_hello() {
|
||||
// SHA-256("Hello") from FORMAT.md Section 12.3
|
||||
let expected = hex!("185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969");
|
||||
let actual = crypto::sha256_hash(b"Hello");
|
||||
assert_eq!(actual, expected);
|
||||
}
|
||||
```
|
||||
|
||||
### Anti-Patterns to Avoid
|
||||
- **Testing golden vectors via CLI binary:** The CLI generates random IVs, making output non-deterministic. Golden vectors MUST call library functions with fixed IVs.
|
||||
- **Not creating `lib.rs`:** Without it, integration tests cannot import project modules. The test suite would be limited to CLI subprocess tests only.
|
||||
- **Hardcoding temp paths (`/tmp/test_xxx`):** Use `tempfile::tempdir()` for auto-cleanup and parallel test safety. Hardcoded paths cause test interference when `cargo test` runs tests in parallel.
|
||||
- **Testing only happy path:** Phase 3 success criteria explicitly require edge cases: empty file, >10MB file, Cyrillic filename. Test all three.
|
||||
- **Ignoring gzip non-determinism in golden vectors:** Do NOT create golden vectors for "compress then encrypt" -- gzip output can vary across platforms. Instead, test compression and encryption as separate stages with deterministic inputs.
|
||||
|
||||
## Don't Hand-Roll
|
||||
|
||||
| Problem | Don't Build | Use Instead | Why |
|
||||
|---------|-------------|-------------|-----|
|
||||
| Temporary test directories | Manual mkdir + cleanup | `tempfile::tempdir()` | Auto-cleanup on drop, no leftover files, parallel-safe |
|
||||
| CLI binary invocation in tests | Manual `std::process::Command` | `assert_cmd::Command::cargo_bin()` | Handles cargo binary lookup, provides fluent assertions |
|
||||
| Hex constants in tests | Manual byte arrays `[0x18, 0x5f, ...]` | `hex_literal::hex!("185f...")` | Far more readable for 32-byte hashes, compile-time checked |
|
||||
| File comparison | Manual byte-by-byte loop | `assert_eq!(fs::read(a), fs::read(b))` | Rust's assert_eq gives a diff on failure, sufficient for byte comparison |
|
||||
|
||||
**Key insight:** The test infrastructure is straightforward -- all the complexity is in the code being tested (which already exists). Phase 3 is about adding assertions, not building new infrastructure.
|
||||
|
||||
## Common Pitfalls
|
||||
|
||||
### Pitfall 1: Binary-Only Crate Blocking Integration Tests
|
||||
**What goes wrong:** Integration tests in `tests/` fail to compile with "unresolved import `encrypted_archive::crypto`".
|
||||
**Why it happens:** The project has `src/main.rs` but no `src/lib.rs`. Rust treats binary-only crates as opaque -- their modules cannot be imported externally.
|
||||
**How to avoid:** Add `src/lib.rs` with `pub mod archive; pub mod cli; pub mod compression; pub mod crypto; pub mod format; pub mod key;`. Update `src/main.rs` to use `encrypted_archive::` prefixed imports. Verify `cargo test` still compiles.
|
||||
**Warning signs:** "unresolved import" errors when running `cargo test`.
|
||||
|
||||
### Pitfall 2: Gzip Non-Determinism Breaking Golden Vectors
|
||||
**What goes wrong:** Golden test for "compress then encrypt" passes locally but fails in CI or on different platforms.
|
||||
**Why it happens:** Gzip output varies between platforms due to the OS byte in gzip headers. Even with `mtime(0)`, the OS byte defaults to the build platform.
|
||||
**How to avoid:** Never create golden vectors for the combined pipeline (compress+encrypt). Test compression and encryption separately: (1) test compress/decompress round-trip, (2) test encrypt/decrypt with known plaintext directly (no compression). Golden vectors test encryption of raw plaintext, not compressed data.
|
||||
**Warning signs:** Tests pass on Linux but fail on macOS (or vice versa).
|
||||
|
||||
### Pitfall 3: Parallel Test Interference
|
||||
**What goes wrong:** Tests intermittently fail when run with `cargo test` (which runs tests in parallel).
|
||||
**Why it happens:** Multiple tests write to the same temporary paths (e.g., `/tmp/test_archive.bin`).
|
||||
**How to avoid:** Use `tempfile::tempdir()` which creates unique temporary directories per test. Never share state between tests.
|
||||
**Warning signs:** Tests pass individually (`cargo test test_name`) but fail when run together.
|
||||
|
||||
### Pitfall 4: Forgetting to Test Empty File Edge Case
|
||||
**What goes wrong:** Encryption of 0-byte input fails with panic or produces incorrect output.
|
||||
**Why it happens:** PKCS7 padding on 0-byte input adds a full 16-byte padding block. `encrypted_size = ((0 / 16) + 1) * 16 = 16`. If the code assumes input length > 0, it may break.
|
||||
**How to avoid:** Explicitly test: (1) pack empty file, (2) unpack and verify 0 bytes, (3) golden vector for encrypting 0 bytes with known IV.
|
||||
**Warning signs:** `PadError` or incorrect file size after extraction.
|
||||
|
||||
### Pitfall 5: Not Verifying SHA-256 in Round-Trip Tests
|
||||
**What goes wrong:** Round-trip tests compare file content but don't verify the SHA-256 stored in the archive matches.
|
||||
**Why it happens:** The round-trip test only checks "extracted file == original file", which is necessary but not sufficient. It doesn't verify the integrity pipeline is correct.
|
||||
**How to avoid:** In addition to file content comparison, add golden tests that verify `crypto::sha256_hash()` produces expected values for known inputs (TST-02). The `inspect` command can also be used to verify stored SHA-256 values.
|
||||
|
||||
### Pitfall 6: Not Testing Format Serialization Round-Trip
|
||||
**What goes wrong:** Format serialization has a subtle endianness or offset bug that only manifests with specific field values.
|
||||
**Why it happens:** Phase 2 tested manually; no automated tests for write_header/read_header cycle.
|
||||
**How to avoid:** Add unit tests that: (1) create a Header, write it to a Vec<u8>, read it back, assert equality; (2) same for TocEntry with various name lengths (including empty name and long Cyrillic name). These tests catch endianness bugs, off-by-one in name_length, etc.
|
||||
|
||||
## Code Examples
|
||||
|
||||
Verified patterns from official sources and research validation:
|
||||
|
||||
### Golden Vector Cross-Verification (verified during research)
|
||||
```bash
|
||||
# AES-256-CBC encrypt "Hello" with project KEY and IV=0...01
|
||||
# Verified on 2026-02-25
|
||||
echo -n "Hello" | openssl enc -aes-256-cbc -nosalt \
|
||||
-K "7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550" \
|
||||
-iv "00000000000000000000000000000001" | xxd -p
|
||||
# Output: 6e66ae8bc740efeefe83b5713fcb716f
|
||||
|
||||
# HMAC-SHA256 of IV || ciphertext
|
||||
KEY_HEX="7a35c1d94fe82b6a910df358bc74a61e428fd063e5179b2cfa8406cd3e79b550"
|
||||
IV_HEX="00000000000000000000000000000001"
|
||||
CT_HEX="6e66ae8bc740efeefe83b5713fcb716f"
|
||||
{ printf '%b' "$(echo "$IV_HEX" | sed 's/../\\x&/g')"; \
|
||||
printf '%b' "$(echo "$CT_HEX" | sed 's/../\\x&/g')"; } \
|
||||
| openssl dgst -sha256 -mac HMAC -macopt "hexkey:${KEY_HEX}" -hex
|
||||
# Output: 0c85780b6628ba3b52654d2f6e0d9bbec67443cf2a6104eb3120ec93fc2d38d4
|
||||
|
||||
# SHA-256 of "Hello" (from FORMAT.md)
|
||||
echo -n "Hello" | sha256sum
|
||||
# Output: 185f8db32271fe25f561a6fc938b2e264306ec304eda518007d1764826381969
|
||||
```
|
||||
|
||||
### Compression Unit Test
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_compress_decompress_roundtrip() {
|
||||
let original = b"Hello, World! This is test data for compression.";
|
||||
let compressed = compress(original).unwrap();
|
||||
let decompressed = decompress(&compressed).unwrap();
|
||||
assert_eq!(decompressed, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compress_decompress_empty() {
|
||||
let original = b"";
|
||||
let compressed = compress(original).unwrap();
|
||||
let decompressed = decompress(&compressed).unwrap();
|
||||
assert_eq!(decompressed, original);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_compress_text() {
|
||||
assert!(should_compress("readme.txt", &[]));
|
||||
assert!(should_compress("data.json", &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_not_compress_apk() {
|
||||
assert!(!should_compress("app.apk", &[]));
|
||||
assert!(!should_compress("photo.jpg", &[]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_should_not_compress_excluded() {
|
||||
assert!(!should_compress("special.dat", &["special.dat".into()]));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Format Serialization Round-Trip Test
|
||||
```rust
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::io::Cursor;
|
||||
|
||||
#[test]
|
||||
fn test_header_roundtrip() {
|
||||
let header = Header {
|
||||
version: VERSION,
|
||||
flags: 0x01,
|
||||
file_count: 3,
|
||||
toc_offset: HEADER_SIZE,
|
||||
toc_size: 330,
|
||||
toc_iv: [0u8; 16],
|
||||
reserved: [0u8; 8],
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
write_header(&mut buf, &header).unwrap();
|
||||
assert_eq!(buf.len(), HEADER_SIZE as usize);
|
||||
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let parsed = read_header(&mut cursor).unwrap();
|
||||
|
||||
assert_eq!(parsed.version, header.version);
|
||||
assert_eq!(parsed.flags, header.flags);
|
||||
assert_eq!(parsed.file_count, header.file_count);
|
||||
assert_eq!(parsed.toc_offset, header.toc_offset);
|
||||
assert_eq!(parsed.toc_size, header.toc_size);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toc_entry_roundtrip_ascii() {
|
||||
let entry = TocEntry {
|
||||
name: "hello.txt".to_string(),
|
||||
original_size: 1024,
|
||||
compressed_size: 800,
|
||||
encrypted_size: 816,
|
||||
data_offset: 500,
|
||||
iv: [0xAA; 16],
|
||||
hmac: [0xBB; 32],
|
||||
sha256: [0xCC; 32],
|
||||
compression_flag: 1,
|
||||
padding_after: 0,
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
write_toc_entry(&mut buf, &entry).unwrap();
|
||||
assert_eq!(buf.len(), 101 + "hello.txt".len());
|
||||
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let parsed = read_toc_entry(&mut cursor).unwrap();
|
||||
|
||||
assert_eq!(parsed.name, entry.name);
|
||||
assert_eq!(parsed.original_size, entry.original_size);
|
||||
assert_eq!(parsed.encrypted_size, entry.encrypted_size);
|
||||
assert_eq!(parsed.iv, entry.iv);
|
||||
assert_eq!(parsed.hmac, entry.hmac);
|
||||
assert_eq!(parsed.sha256, entry.sha256);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_toc_entry_roundtrip_cyrillic() {
|
||||
let entry = TocEntry {
|
||||
name: "тестовый_файл.txt".to_string(),
|
||||
original_size: 512,
|
||||
compressed_size: 400,
|
||||
encrypted_size: 416,
|
||||
data_offset: 300,
|
||||
iv: [0x11; 16],
|
||||
hmac: [0x22; 32],
|
||||
sha256: [0x33; 32],
|
||||
compression_flag: 1,
|
||||
padding_after: 0,
|
||||
};
|
||||
|
||||
let mut buf = Vec::new();
|
||||
write_toc_entry(&mut buf, &entry).unwrap();
|
||||
// Cyrillic UTF-8: each Cyrillic character is 2 bytes
|
||||
assert_eq!(buf.len(), 101 + entry.name.len());
|
||||
|
||||
let mut cursor = Cursor::new(&buf);
|
||||
let parsed = read_toc_entry(&mut cursor).unwrap();
|
||||
assert_eq!(parsed.name, entry.name);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-File Round-Trip Integration Test
|
||||
```rust
|
||||
// tests/round_trip.rs
|
||||
use assert_cmd::Command;
|
||||
use tempfile::tempdir;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_multiple_files() {
|
||||
let dir = tempdir().unwrap();
|
||||
let input_dir = dir.path().join("input");
|
||||
let output_dir = dir.path().join("output");
|
||||
let archive = dir.path().join("multi.bin");
|
||||
|
||||
fs::create_dir_all(&input_dir).unwrap();
|
||||
fs::write(input_dir.join("text.txt"), b"Some text content").unwrap();
|
||||
fs::write(input_dir.join("binary.dat"), &[0x42u8; 256]).unwrap();
|
||||
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("pack")
|
||||
.arg(input_dir.join("text.txt"))
|
||||
.arg(input_dir.join("binary.dat"))
|
||||
.arg("-o").arg(&archive)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("unpack")
|
||||
.arg(&archive)
|
||||
.arg("-o").arg(&output_dir)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert_eq!(
|
||||
fs::read(input_dir.join("text.txt")).unwrap(),
|
||||
fs::read(output_dir.join("text.txt")).unwrap()
|
||||
);
|
||||
assert_eq!(
|
||||
fs::read(input_dir.join("binary.dat")).unwrap(),
|
||||
fs::read(output_dir.join("binary.dat")).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_empty_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let archive = dir.path().join("empty.bin");
|
||||
|
||||
fs::write(dir.path().join("empty.txt"), b"").unwrap();
|
||||
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("pack")
|
||||
.arg(dir.path().join("empty.txt"))
|
||||
.arg("-o").arg(&archive)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let output_dir = dir.path().join("out");
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("unpack")
|
||||
.arg(&archive)
|
||||
.arg("-o").arg(&output_dir)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let extracted = fs::read(output_dir.join("empty.txt")).unwrap();
|
||||
assert!(extracted.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_cyrillic_filename() {
|
||||
let dir = tempdir().unwrap();
|
||||
let archive = dir.path().join("cyrillic.bin");
|
||||
|
||||
fs::write(dir.path().join("файл.txt"), "Содержимое").unwrap();
|
||||
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("pack")
|
||||
.arg(dir.path().join("файл.txt"))
|
||||
.arg("-o").arg(&archive)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let output_dir = dir.path().join("out");
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("unpack")
|
||||
.arg(&archive)
|
||||
.arg("-o").arg(&output_dir)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert_eq!(
|
||||
fs::read(dir.path().join("файл.txt")).unwrap(),
|
||||
fs::read(output_dir.join("файл.txt")).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_roundtrip_large_file() {
|
||||
let dir = tempdir().unwrap();
|
||||
let archive = dir.path().join("large.bin");
|
||||
|
||||
// 11MB of pseudo-random data (deterministic seed for reproducibility)
|
||||
let data: Vec<u8> = (0..11_000_000u32).map(|i| (i.wrapping_mul(2654435761)) as u8).collect();
|
||||
fs::write(dir.path().join("large.bin"), &data).unwrap();
|
||||
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("pack")
|
||||
.arg(dir.path().join("large.bin"))
|
||||
.arg("--no-compress").arg("bin")
|
||||
.arg("-o").arg(&archive)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
let output_dir = dir.path().join("out");
|
||||
Command::cargo_bin("encrypted_archive").unwrap()
|
||||
.arg("unpack")
|
||||
.arg(&archive)
|
||||
.arg("-o").arg(&output_dir)
|
||||
.assert()
|
||||
.success();
|
||||
|
||||
assert_eq!(
|
||||
fs::read(dir.path().join("large.bin")).unwrap(),
|
||||
fs::read(output_dir.join("large.bin")).unwrap()
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## State of the Art
|
||||
|
||||
| Old Approach | Current Approach | When Changed | Impact |
|
||||
|--------------|------------------|--------------|--------|
|
||||
| Manual `std::process::Command` for CLI tests | `assert_cmd 2.x` with fluent API | 2023+ | Cleaner test code, better failure messages |
|
||||
| `tempdir` crate (deprecated) | `tempfile 3.x` (includes tempdir) | 2020 | `tempdir` crate is deprecated, use `tempfile::tempdir()` |
|
||||
| `hex` crate for runtime decode | `hex-literal` macro for compile-time | Stable since 2023 | Zero runtime cost, compile-time checked hex strings |
|
||||
|
||||
**Deprecated/outdated:**
|
||||
- `tempdir` crate: Merged into `tempfile`. Use `tempfile::tempdir()` instead.
|
||||
- `assert_cli` crate: Replaced by `assert_cmd`. Do not use `assert_cli`.
|
||||
|
||||
## Open Questions
|
||||
|
||||
1. **Large file test timing in CI**
|
||||
- What we know: 11MB round-trip test takes ~1 second locally (debug build). In CI it may take longer.
|
||||
- What's unclear: Should the large file test be marked `#[ignore]` to keep default test runs fast?
|
||||
- Recommendation: Keep it in the default test suite. 1-2 seconds is acceptable. Only add `#[ignore]` if CI feedback is too slow (measure first, optimize later).
|
||||
|
||||
2. **Cross-platform gzip golden vectors**
|
||||
- What we know: `GzBuilder::new().mtime(0)` zeroes the timestamp but the OS byte in gzip header is platform-dependent.
|
||||
- What's unclear: Will compression golden vectors fail on macOS if developed on Linux?
|
||||
- Recommendation: Do NOT create golden vectors for compressed output. Test compression as a round-trip (compress -> decompress == original). Test encryption golden vectors on raw (uncompressed) plaintext only. This eliminates all cross-platform issues.
|
||||
|
||||
## Sources
|
||||
|
||||
### Primary (HIGH confidence)
|
||||
- `docs/FORMAT.md` v1.0 -- Binary format specification, worked example with SHA-256 values, decode pipeline
|
||||
- `/stebalien/tempfile` via Context7 -- TempDir API, auto-cleanup patterns, test fixture patterns
|
||||
- `docs.rs/assert_cmd/2.1.2` via WebFetch -- `Command::cargo_bin()` API, assertion chains
|
||||
- Manual verification: round-trip tested empty file, "Hello" text, Cyrillic filename, 11MB binary -- all passed (2026-02-25)
|
||||
- `openssl enc` cross-verification: AES-256-CBC ciphertext and HMAC-SHA256 computed for golden vectors with project KEY
|
||||
|
||||
### Secondary (MEDIUM confidence)
|
||||
- `crates.io` version listings: `tempfile 3.26.0`, `assert_cmd 2.1.2`, `hex-literal 1.1.0`, `predicates 3.1.4` -- verified via `cargo search`
|
||||
- Rust by Example (Context7 `/rust-lang/rust-by-example`) -- integration test structure, `tests/` directory organization
|
||||
|
||||
### Tertiary (LOW confidence)
|
||||
- None. All findings verified against official docs and hands-on testing.
|
||||
|
||||
## Metadata
|
||||
|
||||
**Confidence breakdown:**
|
||||
- Standard stack: HIGH -- All crates verified via `cargo search`, tempfile API verified via Context7, assert_cmd API verified via docs.rs
|
||||
- Architecture: HIGH -- Binary crate limitation identified and solution verified (lib.rs pattern). Test structure follows standard Rust conventions.
|
||||
- Pitfalls: HIGH -- All pitfalls discovered through hands-on testing (round-trip verified for all edge cases) and code analysis (binary-only crate issue found by checking for lib.rs)
|
||||
- Golden vectors: HIGH -- Exact ciphertext/HMAC values cross-verified between `openssl enc` and the project's encryption functions
|
||||
|
||||
**Research date:** 2026-02-25
|
||||
**Valid until:** 2026-04-25 (stable crates, slow-moving test ecosystem)
|
||||
Reference in New Issue
Block a user