atx_reader 0.1.0

Parser and decoder for Apple .atx texture archives (AAPL container with ASTC payload), as produced by tools like Cellebrite UFED iOS exports.
Documentation
# atx_reader

Parser and decoder for Apple `.atx` texture archives — an AAPL-container
format that wraps ASTC-compressed image data, used by Apple internally and
surfaced in third-party forensic exports such as Cellebrite UFED.

## Quick start

```toml
[dependencies]
atx_reader = "0.1"
```

```rust
let bytes = std::fs::read("snapshot.atx")?;

// High-level: get an RGBA8 buffer at the original (width, height).
let img = atx_reader::decode(&bytes)?;
assert_eq!(img.pixels.len(), (img.width * img.height * 4) as usize);

// With the `image` feature (default), save directly to disk.
let rgba = atx_reader::decode_to_image(&bytes)?;
rgba.save("snapshot.png")?;
```

CLI examples:

```text
cargo run --example inspect       -- path/to/file.atx
cargo run --example decode_to_png -- path/to/file.atx out.png
```

## Cargo features

| Feature  | Default | Pulls in       | Effect                                                |
|----------|---------|----------------|-------------------------------------------------------|
| `decode` | yes     | `astc-decode`  | Enables `decode` / `decode_with` / `AtxDecoder`       |
| `image`  | yes     | `image` (PNG)  | Enables `decode_to_image` returning `RgbaImage`       |
| `lzfse`  | yes     | `lzfse_rust`   | Adds support for the `LZFS` chunk (compressed variant) |

Disable defaults if you only need parsing:

```toml
atx_reader = { version = "0.1", default-features = false }
```

## API surface

- `AtxContainer::parse(&bytes)` — validate the `AAPL\r\n\x1a\n` signature
  and obtain a zero-copy view.
- `container.chunks()` — iterate over well-formed chunks.
- `container.header()` — parse the `HEAD` chunk into `AtxHeader`.
- `container.astc_payload()` — slice the raw ASTC blocks, robust to
  alignment padding between the last walked chunk and the `astc` marker.
- `decode(&bytes)` / `decode_with(&bytes, &opts)` — full pipeline.
- `AtxDecoder::new(header, payload)` — when you already have the parts.

`DecodeOptions` lets you pick the [`AstcFootprint`], the
[`PayloadLayout`], and override the padded image size if your sample
uses a different scheme than the default.

## Format notes

The crate was reverse-engineered against real `.atx` samples. The verified
layout is documented below; see [`src/parser.rs`](src/parser.rs) and
[`src/decode.rs`](src/decode.rs) for the authoritative reference.

### Container

- 8-byte signature: `41 41 50 4C 0D 0A 1A 0A` = `"AAPL\r\n\x1a\n"`
  (PNG-style trailer for transport-corruption detection).
- Chunks follow immediately, in `[size:u32 LE][tag:4 ASCII][payload:size bytes]`
  layout. `size` covers the payload only.

### `HEAD` chunk

Payload offsets observed in the verified sample (relative to the start of
the chunk's payload):

| Offset | Type | Field                       |
|-------:|------|-----------------------------|
| `0x00` | u32  | flags / version             |
| `0x18` | u32  | width                       |
| `0x1C` | u32  | height                      |
| `0x20` | u32  | depth                       |
| `0x28` | u32  | array layers                |
| `0x2C` | u32  | mipmap count                |
| `0x3C` | 16 B | per-texture UUID            |
| `0x4C` | u32  | pixel-format discriminator A |
| `0x50` | u32  | pixel-format discriminator B |

The exact `(A, B)` → ASTC-footprint mapping is proprietary. Only `(3, 5) → Astc4x4`
is confirmed today; contributions of new samples welcome.

### `FILL` chunk

All-zero padding for GPU alignment. Skipped by the decoder.

### `astc` chunk

`[size][astc][inner_size:u32 LE][raw ASTC blocks]`. The 4-byte `inner_size`
field sits between the tag and the actual block data — `astc_payload()`
already accounts for this. Used by snapshot variants (iOS lock-screen and
snapshot-switcher captures, ~3 MB files).

### `LZFS` chunk

`[size][LZFS][inner_size:u32 LE][LZFSE stream `bvx2`/`bvxn`...]`. The
decompressed output is a linear row-major sequence of raw ASTC 4×4 blocks
at the unpadded block grid (`ceil(W/4) × ceil(H/4)` blocks). Used by camera
thumbnails and camera-grid icons (kilobyte-scale files).

### Decode pipeline

The high-level `decode` / `decode_to_image` entry points auto-detect the
chunk type and apply the right layout:

| Chunk  | Decompress? | Default layout                      | Default padding                |
|--------|-------------|-------------------------------------|--------------------------------|
| `astc` | no          | macro-tiled Morton, `macro_blocks=32` | next multiple of `32 × 4 = 128` px |
| `LZFS` | LZFSE       | linear row-major                    | next multiple of `4` px        |

For the canonical `astc`-chunk layout:

1. Read `width × height` from `HEAD`.
2. Pad each dimension up to a multiple of `128` px (e.g. `1170 × 2532 → 1280 × 2560`).
3. Iterate the payload as `32 × 32`-block macro-tiles in row-major order;
   inside each macro-tile, blocks are in Morton (Z-order) order.
4. Re-linearize the block grid, ASTC-decode at the padded size, crop to
   `(width, height)`.

For the `LZFS`-chunk layout:

1. Decompress the `bvx2` LZFSE stream — the result is exactly
   `ceil(W/4) × ceil(H/4) × 16` bytes of raw ASTC 4×4 blocks.
2. ASTC-decode at the block-aligned size and crop to `(width, height)`.

To target a sample that does *not* match this layout (e.g. plain linear
ASTC), build a decoder explicitly:

```rust
use atx_reader::{AtxContainer, AtxDecoder, AstcFootprint, PayloadLayout};

let c = AtxContainer::parse(&bytes)?;
let img = AtxDecoder::new(c.header()?, c.astc_payload()?)
    .footprint(AstcFootprint::Astc8x8)
    .layout(PayloadLayout::Linear)
    .decode()?;
```

## License

Licensed under either of [MIT](LICENSE-MIT) or
[Apache-2.0](LICENSE-APACHE) at your option.