# Shua-Zip

A slim, `#![no_std]` compatible ZIP library written in pure Rust.

## Feature Flags

| Feature | Default | Description |
|---------|---------|-------------|
| `deflate` | on | Deflate decompression via `miniz_oxide` |
| `crc32` | on | CRC-32 checksums via `crc32fast` |

With `default-features = false` you get a minimal `no_std` build — no compression support, no CRC-32 dependency. Bring your own CRC via the `Crc32` trait.

## Library Usage

### Creating archives

```rust
use shua_zip::ZipArchive;

let mut archive = ZipArchive::new();
archive.add_file("hello.txt", b"Hello, world!");
archive.add_file("data.bin", vec![0, 1, 2, 3]);

let zip_bytes: Vec<u8> = archive.build().unwrap();
std::fs::write("output.zip", &zip_bytes).unwrap();
```

### Appending to an existing archive

Use `build_with_readat` to avoid loading existing file data into memory:

```rust
use shua_zip::{ZipArchive, ReadAt};

let reader = /* impl ReadAt */;
let mut archive = ZipArchive::new()
    .with_binary(&reader)?;    // parse existing entries
archive.add_file("new.txt", b"fresh content");
let zip_bytes = archive.build_with_readat(&reader)?;
```

### Reading archives

`ReadAt` is the central trait — implement it for your storage backend:

```rust
use shua_zip::{ZipArchive, ReadAt, ZipReadStrategy};

let reader = /* impl ReadAt */;
let archive = ZipArchive::new().with_binary(&reader)?;

// List entries from the central directory
for entry in archive.entries() {
    println!("{} ({} bytes)", entry.name, entry.uncompressed_size);
}

// Find by name
if let Some(idx) = archive.find_by_name("hello.txt") {
    let data = archive.read_file(idx, &reader)?;
}

// Or use a specific read strategy
let data = archive.read_file_with_strategy(
    idx,
    &reader,
    ZipReadStrategy::Merged,
)?;
```

### Read Strategies

| Strategy | Reads | Description |
|----------|-------|-------------|
| `Merged` | 1–2 | Reads LFH + compressed data in one chunk. Falls back to a second read if data extends beyond the chunk. **Recommended for high-latency backends.** |
| `Strict` | 2 | Reads and validates the LFH first, then the data at the resolved offset. Safest for malformed files. |
| `Optimistic` | 1 | Skips header verification, jumps straight to the cached data offset. Fastest, may read garbage if offsets are wrong. |

### `ReadAt` trait

```rust
pub trait ReadAt {
    type Error: core::error::Error;
    fn size(&self) -> u64;
    fn read_at(&self, offset: u64, buf: &mut [u8]) -> Result<(), Self::Error>;
}
```

No `Seek` or `Read` required — just random-access reads. Works with files, byte slices, HTTP range requests, embedded flash, etc.

### Custom decompressors

```rust
archive.register_decompressor(114514, |data| {
    // your custom decompression logic
    Ok(data.to_vec())
});
```

### `no_std` with custom CRC

When `crc32` feature is disabled, use `add_file_with_crc` with your own implementation:

```rust
struct MyCrc;
impl Crc32 for MyCrc {
    fn hash(data: &[u8]) -> u32 { /* ... */ }
}

archive.add_file_with_crc::<MyCrc>("data.bin", payload);
```

## Supported Compression

| Method | ID | Read | Write |
|--------|----|------|-------|
| Stored | 0 | Yes | Yes |
| Deflate | 8 | Yes (opt-in) | No |
| Custom | * | Yes (via `register_decompressor`) | No |

## CLI Example

```bash
# List contents
cargo run --example cli -- list archive.zip

# Extract to a directory
cargo run --example cli -- extract archive.zip ./output/

# Extract to default directory (archive name without extension)
cargo run --example cli -- extract archive.zip

# Create a new archive
cargo run --example cli -- create new.zip file1.txt file2.png

# Add files to an existing archive
cargo run --example cli -- add existing.zip another.txt
```

## Compatibility

Cross-tested against the `zip` crate: archives written by either library are correctly read by the other, including deflate-compressed entries, empty files, Unicode filenames, and large payloads. See `tests/compat_zip.rs` for the full suite.

Benchmarks in `benches/compare_zip.rs` cover build/parse/read/roundtrip against `zip-rs`.

## License

[MIT](./LICENSE.txt)
