Files
2026-02-25 00:19:27 +03:00

28 KiB
Raw Permalink Blame History

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:

cargo add --dev tempfile@3.26 assert_cmd@2.1 hex-literal@1.1 predicates@3.1

Architecture Patterns

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:

// src/lib.rs
pub mod archive;
pub mod cli;
pub mod compression;
pub mod crypto;
pub mod format;
pub mod key;
// 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:

// 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:

// 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:

// 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, 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)

# 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

#[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

#[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

// 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)