openpack 0.2.2

Safe archive-reader for ZIP-derived formats (ZIP, CRX, JAR, APK, IPA) with BOM-safe checks.
Documentation
use std::fs::File;
use std::io::Write;
use std::path::PathBuf;

use zip::write::SimpleFileOptions;
use zip::CompressionMethod;
use zip::ZipWriter;

use openpack::{Limits, OpenPack, OpenPackError};

struct Scratch {
    _tmp: tempfile::TempDir,
    path: PathBuf,
}

impl Scratch {
    fn new(suffix: &str) -> Self {
        let tmp = tempfile::tempdir().expect("tempdir");
        let path = tmp.path().join(format!("archive.{suffix}"));
        Self { _tmp: tmp, path }
    }
}

fn write_zip(path: &std::path::Path, entries: &[(&str, &[u8], CompressionMethod)]) {
    let file = File::create(path).unwrap();
    let mut zip = ZipWriter::new(file);
    for (name, data, comp) in entries {
        let options = SimpleFileOptions::default().compression_method(*comp);
        zip.start_file(*name, options).unwrap();
        zip.write_all(data).unwrap();
    }
    zip.finish().unwrap();
}

#[test]
fn zip_bomb_ratio_check_in_read_entry() {
    let archive = Scratch::new("zip");

    // Highly compressible payload
    let payload = vec![0u8; 1024 * 1024 * 10]; // 10MB of zeros

    write_zip(
        &archive.path,
        &[("bomb.txt", &payload, CompressionMethod::Deflated)],
    );

    let mut limits = Limits::default();
    limits.max_compression_ratio = 5.0; // Strictly allow only 5x compression
    
    // We increase total limit so it passes entries() iteration
    limits.max_entry_uncompressed_size = 20 * 1024 * 1024;
    limits.max_total_uncompressed_size = 20 * 1024 * 1024;

    let pack = OpenPack::open(&archive.path, limits).unwrap();

    let err = pack.read_entry("bomb.txt").unwrap_err();
    assert!(
        matches!(err, OpenPackError::LimitExceeded(ref msg) if msg.contains("compression ratio limit")),
        "Expected compression ratio limit error, got {:?}",
        err
    );
}

#[test]
fn path_traversal_in_read_entry() {
    let archive = Scratch::new("zip");
    
    // Create an archive with a malicious entry name
    write_zip(
        &archive.path,
        &[("../../etc/passwd", b"secret", CompressionMethod::Stored)],
    );

    let pack = OpenPack::open_default(&archive.path).unwrap();
    
    let err = pack.read_entry("../../etc/passwd").unwrap_err();
    assert!(
        matches!(err, OpenPackError::ZipSlip(_)),
        "Expected ZipSlip error on read_entry"
    );

    let contains_err = pack.contains("../../etc/passwd").unwrap_err();
    assert!(
        matches!(contains_err, OpenPackError::ZipSlip(_)),
        "Expected ZipSlip error on contains"
    );
}

#[test]
fn path_traversal_in_extract_all_to() {
    let archive = Scratch::new("zip");
    
    write_zip(
        &archive.path,
        &[("../../etc/passwd", b"secret", CompressionMethod::Stored)],
    );

    let pack = OpenPack::open_default(&archive.path).unwrap();
    
    let extract_dir = tempfile::tempdir().unwrap();
    
    let err = pack.extract_all_to(extract_dir.path()).unwrap_err();
    assert!(
        matches!(err, OpenPackError::ZipSlip(_)),
        "Expected ZipSlip error on extract_all_to"
    );
}