|
| 1 | +# AFFLIBv3 parity checklist |
| 2 | + |
| 3 | +Human-maintained checklist tracking feature parity between this crate and the `AFFLIBv3` reference |
| 4 | +implementation. |
| 5 | + |
| 6 | +Reference commit pinned in this repository: |
| 7 | + |
| 8 | +- `external/refs/repos/sshock__AFFLIBv3.commit` |
| 9 | + |
| 10 | +## Scope / notes |
| 11 | + |
| 12 | +- This crate is **read-only** and exposes a Rust-native API (`forensic_image::ReadAt` + |
| 13 | + `read_segment()`), not the full `AFFILE*` stream/DB API from AFFLIB. |
| 14 | +- `[x]` means supported in this crate today; `[ ]` means not supported (or only partially |
| 15 | + supported; noted inline). |
| 16 | +- Implementation note: the current AFF1 backend reads the entire `.aff` file into memory; AFFLIB |
| 17 | + is streaming + page-cache based. |
| 18 | + |
| 19 | +## Container identification & open semantics (AFFLIB vnodes) |
| 20 | + |
| 21 | +- AFFLIB’s vnode probe order matters (from `lib/afflib.cpp`): `s3://` (if enabled) → AFD → AFM → AFF → |
| 22 | + (VMDK/DMG/SPARSEIMAGE if enabled) → split-raw → raw. |
| 23 | + |
| 24 | +- [x] AFF1 (`.aff`) |
| 25 | + - AFFLIB identify: file header `AFF10\r\n\0`, or (for non-existent/empty files) extension `.aff` |
| 26 | + - This crate: sniff header when possible; otherwise uses extension (`.aff`) |
| 27 | +- [x] AFM (`.afm`) |
| 28 | + - AFFLIB identify: extension `.afm` |
| 29 | + - This crate: extension `.afm` (or header sniff + extension disambiguation) |
| 30 | +- [x] AFD directory container |
| 31 | + - AFFLIB identify: a directory whose name ends in `.afd` |
| 32 | + - This crate: any directory containing `file_###.aff` entries (more permissive than AFFLIB) |
| 33 | +- [ ] RAW passthrough (`.raw`, `.iso`, block devices) |
| 34 | +- [ ] RAW passthrough details: AFFLIB fakes `pagesize`/`imagesize`/`sectorsize`/`devicesectors` segments |
| 35 | + even though raw has no segment store |
| 36 | +- [ ] Split-raw standalone (`.000` / `.001` / `.A00`… without `.afm`) |
| 37 | +- [ ] Split-raw identify in AFFLIB: `.000` / `.001` / `.aaa` / `.AAA` (first file of a set) |
| 38 | +- [ ] `s3://` vnode (S3 object store) |
| 39 | +- [ ] QEMU vnodes: VMDK (`.vmdk`), DMG (`.dmg`), SPARSEIMAGE (`.sparseimage`) |
| 40 | +- [ ] URL handling (`file://...`) |
| 41 | + |
| 42 | +## Segment database API (AFFLIB name/value store) |
| 43 | + |
| 44 | +- [x] List segment names (`segment_names()`) |
| 45 | + - [ ] Virtualize encrypted names (hide `*/aes256` and expose the base name; AFFLIB `af_get_next_seg()` strips the suffix when auto-decrypt is enabled) — not implemented |
| 46 | + - [ ] AFM: include virtual `page<N>` entries in the listing (AFFLIB enumerates them via split-raw) — not implemented (we intentionally list metadata only) |
| 47 | + - [ ] Hide 0-length segment names (AFFLIB uses `AF_IGNORE == ""` to mark holes) — not implemented |
| 48 | +- [x] Read a segment by name (`read_segment(name)`) |
| 49 | + - [x] Auto-decrypt `name/aes256` into `name` when a key is available (AFFLIB checks encrypted first) |
| 50 | +- [ ] Cursor-based iteration (`af_get_next_seg` / `af_rewind_seg`) |
| 51 | +- [ ] Write/update/delete segments (`af_update_seg`, `af_update_segf`, `af_del_seg`) |
| 52 | +- [ ] Quadword helpers (`af_get_segq`, `af_update_segq`) — callers can parse 8-byte `aff_quad` manually |
| 53 | + |
| 54 | +## Byte access API |
| 55 | + |
| 56 | +- [x] Random-access reads via `forensic_image::ReadAt` |
| 57 | +- [ ] Stream-style reads/seeks (`af_read`, `af_seek`, `af_tell`, `af_eof`) |
| 58 | +- [ ] Stream-style writes (`af_write`) (crate is read-only) |
| 59 | + |
| 60 | +## Error model / diagnostics |
| 61 | + |
| 62 | +- [x] Missing segment → `Ok(None)` (vs AFFLIB `AF_ERROR_EOF`/`ENOENT`-style conventions) |
| 63 | +- [ ] Distinguish “segment exists but buffer too small” (`AF_ERROR_DATASMALL`) — not applicable (Rust allocates `Vec`) |
| 64 | +- [ ] Repair tooling (`affix`, `affrecover`) — not implemented |
| 65 | + |
| 66 | +## Options & environment-variable behavior |
| 67 | + |
| 68 | +- [x] Auto-decrypt toggle (AFFLIB `AF_OPTION_AUTO_DECRYPT`) via `AffOpenOptions.auto_decrypt` |
| 69 | +- [ ] Auto-encrypt toggle (AFFLIB `AF_OPTION_AUTO_ENCRYPT`) — not implemented (read-only) |
| 70 | +- [x] Page cache sizing (AFFLIB `af_set_cachesize` / `AFFLIB_CACHE_PAGES`) via `AffOpenOptions.page_cache_pages` |
| 71 | +- [ ] AFFLIB open flags (`AF_OPEN_PRIMITIVE`, `AF_HALF_OPEN`, `AF_NO_CRYPTO`, `AF_BADBLOCK_FILL`) |
| 72 | +- [ ] AFFLIB tracing/debug env vars (`AFFLIB_TRACEFILE`, `AFFLIB_CACHE_DEBUG`, `AFFLIB_CACHE_STATS`) |
| 73 | +- [ ] AFFLIB passphrase env vars (`AFFLIB_PASSPHRASE`, `AFFLIB_PASSPHRASE_FILE`, `AFFLIB_PASSPHRASE_FD`) |
| 74 | +- [ ] AFFLIB signing/sealing env vars (e.g. `AFFLIB_PEM_SIGNING_PASSPHRASE`, `AFFLIB_DECRYPTING_PRIVATE_KEYFILE`) |
| 75 | + |
| 76 | +## Container metadata / stats |
| 77 | + |
| 78 | +- [x] Basic info: container kind + `len()` + `page_size()` (subset of AFFLIB `af_vstat`) |
| 79 | +- [ ] Full vnode stats (`af_vstat` fields like `segment_count_total`, `page_count_total`, |
| 80 | + `segment_count_signed`, `segment_count_encrypted`, etc.) |
| 81 | +- [ ] `af_stats()` performance counters (cache hit/miss, bytes copied, pages compressed, …) |
| 82 | + |
| 83 | +## Well-known segment names (AFFLIB constants) |
| 84 | + |
| 85 | +The crate exposes most metadata as **raw segment bytes** via `read_segment()` (when present in the |
| 86 | +container). Only a small subset currently affects behavior (`len()`, page size, crypto, page |
| 87 | +decompression). |
| 88 | + |
| 89 | +### Structural / housekeeping |
| 90 | + |
| 91 | +- [ ] Ignore segments (`AF_IGNORE == ""`, 0-length name) — AFFLIB uses these as “holes”; this crate currently surfaces them as a real segment name `""` |
| 92 | +- [ ] Segment name length limit (`AF_MAX_NAME_LEN == 64`) — not enforced in this crate |
| 93 | + |
| 94 | +### Container type / provenance |
| 95 | + |
| 96 | +- [x] `aff_file_type` (`"AFF"|"AFM"|"AFD"`) — exposed only (this crate does not use it for detection) |
| 97 | +- [x] `afflib_version` — exposed only |
| 98 | +- [x] `creator` — exposed only |
| 99 | +- [x] `dir` (directory segment) — exposed only |
| 100 | +- [x] `batch_name`, `batch_item_name` — exposed only |
| 101 | + |
| 102 | +### Image geometry / device |
| 103 | + |
| 104 | +- [x] `pagesize` (value stored in `arg`, `data_len == 0`) — parsed |
| 105 | +- [x] `segsize` (deprecated alias for `pagesize`) — parsed |
| 106 | +- [x] `imagesize` (8 bytes `aff_quad`) — parsed |
| 107 | +- [ ] `AF_SEG_QUADWORD` flag (`0x0002`) for 8-byte segments — not used (we parse by segment name + length) |
| 108 | +- [x] `sectorsize` (value stored in `arg`) — exposed only (not interpreted) |
| 109 | +- [x] `devicesectors` (8 bytes `aff_quad`) — exposed only (not interpreted) |
| 110 | +- [x] `badsectors` — exposed only |
| 111 | +- [x] `badflag` — exposed only |
| 112 | +- [x] `blanksectors` (8 bytes; count of all-NUL sectors) — exposed only |
| 113 | + |
| 114 | +### Data pages |
| 115 | + |
| 116 | +- [x] `page<N>` segments (logical disk bytes by page index) |
| 117 | +- [x] Deprecated `seg<N>` page naming |
| 118 | +- [x] Page compression flags in `arg` (`AF_PAGE_COMPRESSED`, `AF_PAGE_COMP_ALG_*`) |
| 119 | + - [x] ZLIB (`AF_PAGE_COMP_ALG_ZLIB`) |
| 120 | + - [x] LZMA (`AF_PAGE_COMP_ALG_LZMA`, feature `lzma`) |
| 121 | + - [x] ZERO (`AF_PAGE_COMP_ALG_ZERO`) |
| 122 | + |
| 123 | +### Hashes & parity (as segments) |
| 124 | + |
| 125 | +- [x] Image hashes: `md5`, `sha1`, `sha256` — exposed only (not verified) |
| 126 | +- [x] Piecewise hashes: `page<N>_md5`, `page<N>_sha1`, `page<N>_sha256` — exposed only (not verified) |
| 127 | +- [ ] Auto-generate piecewise hashes (`AF_OPTION_PIECEWISE_*`) — not implemented (read-only) |
| 128 | +- [ ] Verify piecewise hashes (AFFLIB tools: `affinfo -v`) — not implemented |
| 129 | +- [x] `parity0` (parity page) — exposed only |
| 130 | +- [x] `parity0/sha256` — verified when present (treated like any other `*/sha256` signature segment) |
| 131 | +- [ ] Recover broken pages using parity (`affrecover`) — not implemented |
| 132 | + |
| 133 | +### AFM / split-raw metadata |
| 134 | + |
| 135 | +- [x] `raw_image_file_extension` (3 bytes, e.g. `"000"`) — required for AFM open |
| 136 | +- [x] `pages_per_raw_image_file` (8-byte quad) — exposed only (not used to drive splitting) |
| 137 | + |
| 138 | +### Encryption & signatures |
| 139 | + |
| 140 | +- [x] Encrypted segments suffix: `*/aes256` — auto-decrypted on read when key is available |
| 141 | +- [x] Passphrase key segment: `affkey_aes256` — used for key derivation |
| 142 | +- [x] Public-key sealed key segments: `affkey_evp%d` — used for unsealing (rev-1 only) |
| 143 | +- [x] Signature suffix: `*/sha256` — verified (MODE0 + MODE1) |
| 144 | +- [x] Signing certificate: `cert-sha256` (PEM X.509) — used for signature verification |
| 145 | +- [ ] Chain-of-custody BOM segments: `affbom%d` — not parsed/verified |
| 146 | +- [ ] BOM XML schema elements (`affbom`, `date`, `signingcert`, `segmenthash`) — not parsed/verified |
| 147 | + |
| 148 | +### Acquisition metadata (common AFFLIB segment names) |
| 149 | + |
| 150 | +- [x] Exposed (no typed parsing): `case_num`, `image_gid`, `acquisition_iso_country`, |
| 151 | + `acquisition_commandline`, `acquisition_date`, `acquisition_notes`, `acquisition_device`, |
| 152 | + `acquisition_seconds` (stored in `arg`), `acquisition_tecnician`, `acquisition_macaddr`, |
| 153 | + `acquisition_dmesg` |
| 154 | + |
| 155 | +### Device metadata (common AFFLIB segment names) |
| 156 | + |
| 157 | +- [x] Exposed (no typed parsing): `device_manufacturer`, `device_model`, `device_sn`, `device_firmware`, |
| 158 | + `device_source`, `cylinders`, `heads`, `sectors_per_track`, `lbasize`, `hpa_present`, `dco_present`, |
| 159 | + `location_in_computer`, `device_capabilities` |
| 160 | + |
| 161 | +## Readers |
| 162 | + |
| 163 | +### AFF1 — `.aff` |
| 164 | + |
| 165 | +- [x] AFF1 header detection (`AFF10\r\n\0`) |
| 166 | +- [x] Segment framing (`AFF\0` + headers + name + data + `ATT\0` + segment length) |
| 167 | +- [x] Segment TOC scan (last-write-wins semantics for duplicate segment names) |
| 168 | +- [x] `pagesize` / deprecated `segsize` (value in `arg`, `data_len == 0`) |
| 169 | +- [x] `imagesize` parsing (AFFLIB `aff_quad` encoding) |
| 170 | +- [x] `imagesize` inference when missing (mirrors AFFLIB `af_read_sizes`) |
| 171 | + - [ ] Prefer encrypted `imagesize/aes256` when a key is available — not implemented (open happens before the crypto wrapper is applied) |
| 172 | +- [x] Page naming: `page<N>` and deprecated `seg<N>` |
| 173 | +- [x] Sparse/missing pages read as zero-filled bytes |
| 174 | +- [ ] Badblock fill for missing/sparse pages (`AF_BADBLOCK_FILL` / `badflag`) — not implemented |
| 175 | +- [x] Page compression: none (plain page segment) |
| 176 | +- [x] Page compression: zlib (`AF_PAGE_COMP_ALG_ZLIB`) |
| 177 | +- [x] Page compression: ZERO (`AF_PAGE_COMP_ALG_ZERO`) |
| 178 | +- [x] Page compression: LZMA (`AF_PAGE_COMP_ALG_LZMA`) — behind feature `lzma` |
| 179 | +- [ ] Page compression: BZIP (`AF_PAGE_COMP_ALG_BZIP`) — not implemented in AFFLIB either |
| 180 | + |
| 181 | +### AFD — directory container |
| 182 | + |
| 183 | +- [x] Discover `file_###.aff` entries |
| 184 | +- [x] Page map across directory (first file containing a page wins) |
| 185 | +- [x] Global `imagesize` = max across subfiles (AFFLIB `afd_vstat` semantics) |
| 186 | +- [x] Read pages by index even if a subfile `imagesize` is smaller (AFD quirk) |
| 187 | +- [x] Missing pages read as zero-filled bytes |
| 188 | +- [x] Segment lookup: first subfile containing the segment wins |
| 189 | + |
| 190 | +### AFM — `.afm` metadata + split-raw payload |
| 191 | + |
| 192 | +- [x] Metadata stored as AFF1 segments in `.afm` |
| 193 | +- [x] Split-raw payload discovery by incrementing 3-char extension (`.000`..`.999`, `.A00`..) |
| 194 | +- [x] Read disk bytes from raw payload via `ReadAt` |
| 195 | +- [x] Synthetic `page<N>` segments backed by raw payload |
| 196 | +- [ ] Use/validate `pages_per_raw_image_file` to drive split sizing and sanity checks — partial (we infer |
| 197 | + split boundaries from file sizes) |
| 198 | + - AFFLIB behavior: if `pages_per_raw_image_file` is missing/0-length, assume “not split”; otherwise |
| 199 | + split size is `pagesize * pages_per_raw_image_file` and additional consistency checks apply. |
| 200 | + |
| 201 | +### Other AFFLIB container kinds |
| 202 | + |
| 203 | +- [ ] RAW images (non-AFF) |
| 204 | +- [ ] Standalone split-raw (without an `.afm` metadata file) |
| 205 | +- [ ] AFFLIB optional backends (S3/VMDK/DMG/SPARSEIMAGE, etc.) |
| 206 | + |
| 207 | +## Crypto / signatures |
| 208 | + |
| 209 | +### AES-256 encryption (`*/aes256`) |
| 210 | + |
| 211 | +- [x] Auto-decrypt `*/aes256` segments on read (feature `crypto`, default) |
| 212 | +- [x] Derive AES-256 key from `affkey_aes256` using passphrase (`SHA256(passphrase)`) |
| 213 | + - [x] Accept legacy AFFLIB packing bug sizes (52/56 bytes) |
| 214 | +- [x] Unseal `affkey_evp%d` using an RSA PEM private key (rev-1 only) |
| 215 | +- [x] AES-256-CBC decrypt semantics incl. AFFLIB “extra bytes” + padding trimming |
| 216 | +- [ ] Auto-encrypt on write (`AF_OPTION_AUTO_ENCRYPT` / `af_update_segf`) — not implemented (read-only) |
| 217 | +- [ ] Passphrase establish/change APIs (`af_establish_aes_passphrase`, `af_change_aes_passphrase`, …) |
| 218 | + |
| 219 | +### SHA-256 signatures (`*/sha256`, `cert-sha256`) |
| 220 | + |
| 221 | +- [x] Verify `*/sha256` signature segments (feature `crypto`, default) |
| 222 | + - [x] MODE0: `(segname + NUL + arg_be + segment_data)` |
| 223 | + - [x] MODE1: `(segname + NUL + 0 + uncompressed page bytes)` (no implicit zero-padding) |
| 224 | +- [ ] Write signatures (`af_set_sign_files`, `af_sign_seg*`, `af_sign_all_unsigned_segments`) |
| 225 | +- [ ] Chain-of-custody / BOM segments (`affbom%d`) generation/verification |
| 226 | + |
| 227 | +## Integrity metadata & hashes |
| 228 | + |
| 229 | +- [ ] Piecewise page hash segments (`page<N>_md5`, `_sha1`, `_sha256`) generation |
| 230 | +- [ ] Piecewise page hash verification / key validation via page hashes |
| 231 | +- [ ] Image-level hash segments (`md5`, `sha1`, `sha256`) generation/verification |
| 232 | +- [ ] `parity0` generation/verification |
| 233 | +- [ ] Bad-sector related metadata (`badflag`, `badsectors`, `blanksectors`) |
| 234 | + |
| 235 | +## Writers |
| 236 | + |
| 237 | +- [ ] AFF1 writer (`.aff`) |
| 238 | +- [ ] AFD writer |
| 239 | +- [ ] AFM writer (metadata + split-raw) |
| 240 | +- [ ] Compression on write (zlib/lzma/zero) |
| 241 | + - [ ] ZERO compression for all-zero pages (AFFLIB tries ZERO first) |
| 242 | + - [ ] ZLIB compression level control |
| 243 | + - [ ] LZMA compression (AFFLIB uses a fixed level in code; tool UX exposes `-L`) |
| 244 | +- [ ] Image-level hashes (`md5`, `sha1`, `sha256`) |
| 245 | +- [ ] Default page size behavior (`AFF_DEFAULT_PAGESIZE` ≈ 16MiB if not set before first write) |
| 246 | +- [ ] Split sizing / maxsize (`af_set_maxsize`) for split-raw and/or multi-file workflows |
| 247 | +- [ ] Segment signing / sealing |
| 248 | + |
| 249 | +## CLI parity (AFFLIB tools) |
| 250 | + |
| 251 | +- [x] `affcat` (partial): `aff-cat` |
| 252 | + - [x] Stream logical image bytes to stdout |
| 253 | + - [x] Offset/length selection |
| 254 | + - [x] Decrypt with passphrase / unseal keyfile (via `AffOpenOptions`) |
| 255 | + - [ ] Segment output mode (`-s <name>`) |
| 256 | + - [ ] Page/sector addressing modes (`-p`, `-S`) |
| 257 | + - [ ] Missing-page reporting / skipping semantics (`-n` / `-q`) |
| 258 | + - [ ] Output badflag for bad blocks (`-b`) |
| 259 | + - [ ] Range syntax (`-r offset:count`), long listing (`-L`) |
| 260 | +- [x] `affinfo` (very partial): `aff-info` |
| 261 | + - [x] Print kind/len/pagesize and segment count |
| 262 | + - [x] List segment names |
| 263 | + - [ ] Segment previews / formatting heuristics (hex vs ascii) |
| 264 | + - [ ] Filter segments (`-s`), suppress data pages by default, wide output |
| 265 | + - [ ] Validate image hashes (`-m`/`-S`) and page hashes (`-v`) |
| 266 | + - [ ] Identify-only mode (`-i`) |
| 267 | +- [x] `affverify` (subset): `aff-verify` |
| 268 | + - [x] Verify `*/sha256` signature segments (MODE0 + MODE1) |
| 269 | + - [ ] Verbose / “print all segments” modes matching AFFLIB |
| 270 | +- [ ] `affsign` (sign existing image + write BOM segments) |
| 271 | +- [ ] `affcrypto` (encrypt/decrypt, change passphrase, sealing/unsealing, password cracking helpers) |
| 272 | +- [ ] `affsegment` (create/update/delete segments; quad/hex output) |
| 273 | +- [ ] `affconvert` (raw↔aff conversions, recompress, AFD splitting, gzip/bzip2 probing) |
| 274 | +- [ ] `affcopy` (copy/reorder/preen + (re)compress + optional signing + S3 copy) |
| 275 | +- [ ] `affcompare` (compare images/dirs, show differing sectors, S3 existence checks) |
| 276 | +- [ ] `affxml` (metadata/stats as XML) |
| 277 | +- [ ] `affdiskprint` (diskprint structure generation/verification) |
| 278 | +- [ ] `affstats` (stats derived from metadata or by scanning) |
| 279 | +- [ ] `affix` (repair corruption / ensure GID) |
| 280 | +- [ ] `affrecover` (recover pages using parity) |
| 281 | +- [ ] `affuse` (FUSE mount) |
| 282 | + |
0 commit comments