# Format Support — Parsers & Viewers This page lists what Crush can parse and how each viewer behaves, plus the current limitations. It is meant to be honest and actionable: if something is missing, you will see it here. Scope: this page covers file **formats** — what gets detected, parsed, and displayed for a given file — not general-purpose tools like the Value Inspector or Blob Inspector, and not how evidence sources (folders, ZIP/TAR/7z archives, device backups) are opened in the first place. ## Contents **Parsers** — [SQLite Database](#sqlite-database) · [Property List (plist)](#property-list-plist) · [XML](#xml) · [JSON](#json) · [Protobuf](#protobuf-explicit-only) · [Android Binary XML (ABX)](#android-binary-xml-abx) · [SEGB (Biome)](#segb-biome) · [LevelDB](#leveldb) · [MMKV](#mmkv-explicit-only) · [Realm Database](#realm-database) · [Images](#images) · [Media (Audio/Video)](#media-audiovideo) · [PDF](#pdf) · [Log Files](#log-files-explicit-only) · [Hex Fallback](#hex-fallback) **Viewers** — [Table](#table-viewer) · [Tree](#tree-viewer) · [Text](#text-viewer) · [Hex](#hex-viewer) · [Image](#image-viewer) · [Media](#media-viewer) · [ABX](#abx-viewer) · [LevelDB](#leveldb-viewer) · [MMKV](#mmkv-viewer) · [Multi-Log Studio](#multi-log-studio) · [Realm](#realm-viewer) · [Protobuf](#protobuf-viewer) [Known Gaps](#known-gaps-planned) ## How Detection Works - File types are identified by magic bytes, not by extension. - A parser is chosen from the registry in priority order. If no parser matches, the Hex Viewer is used. - Some parsers are explicit-only and must be selected via the context menu. ## Parsers (What They Do) ### SQLite Database - Detects SQLite by magic bytes and loads tables and rows into the Table Viewer. - Copies companion `-wal` and `-shm` files if present. - The Table Viewer has an embedded Show Hex pane, byte-precise per cell: selecting a cell highlights its whole row and that specific column's own on-disk bytes; a row whose current version exists only in a not-yet-checkpointed `-wal` frame switches the pane to that file instead of the base file's stale bytes. - The `-wal` companion is parsed directly, frame by frame, rather than only letting SQLite apply it: every frame is classified as **Active** (the current version of that page), **Superseded** (an older version of a page later overwritten in the same WAL — may still hold rows since edited or deleted), **Uncommitted** (part of a transaction that never committed, e.g. a device seized mid-write), or **WAL slack** (salt-mismatch frames left over from a previous WAL reuse cycle, i.e. stale data still physically present in the file). Pages are attributed back to their table by walking the B-tree from each table's root page in `sqlite_master`, across both the WAL frames and the main file. A **WAL Frames** tab lists every frame with its frame number, page, transaction, status, and attributed table; double-click a frame to open its raw page bytes in the Hex Viewer, or select it with the embedded Show Hex pane open to highlight the same header+page bytes in place (bidirectional — clicking a highlighted byte selects the matching frame back). Per table, a **Show WAL history** toggle re-parses the table's non-Active frames as table-leaf pages and injects their rows straight into the row grid alongside the current data, colour-coded by status and labelled with the source frame (e.g. `WAL Superseded (frame 12)`) — these WAL-sourced rows are additional to, and not subject to, the 10,000-row display cap below, and get the same Show Hex precision (row + specific column) as a normal row, keyed by their own frame bytes since they have no rowid. - SQLCipher-encrypted databases are supported when the password or key is known — right-click the file → **Open as** → **SQLite DB (Encrypted)…**; a wrong password/key re-prompts instead of failing silently. This opens the real SQLCipher engine (the `sqlcipher3` package, not a custom decryption), so page and WAL-frame decryption/checkpointing are handled natively rather than reimplemented — including data that only ever made it into a `-wal` companion, never checkpointed into the main file (a device seized mid-session, before the app itself closed its DB connection). By default, opening tries the linked library's current default cipher settings first, then each legacy `cipher_compatibility` preset (SQLCipher 4 down to 1) in turn — each attempt is a real, cryptographically-verified pass/fail via the engine's own per-page HMAC check, not a guess. As with encrypted `.realm` files, a normal double-click open never auto-prompts, since ciphertext (including what would be the plaintext magic header) can't be told apart from corrupt/other binary data. - The credentials dialog has a **Raw key** option for a key that isn't a passphrase — SQLCipher's own recommended approach when the key is "managed externally" (e.g. an Android Keystore-derived key), rather than typed by a user. Raw key applies independently of whether Advanced parameters are also set, since page size and HMAC algorithm still matter even without a passphrase KDF. - An **Advanced** section exposes explicit cipher parameters (page size, KDF iterations, KDF/HMAC digest, plaintext header size) for apps whose settings don't match any standard `cipher_compatibility` preset — notably Signal and its forks (Session, Molly), which set `kdf_iter = 1` since their key already comes from the platform keystore at full entropy, making the passphrase-stretching KDF pointless overhead. When Advanced is used, those exact parameters are applied in a single attempt instead of the auto-try. - The main database file's own freelist and in-page freeblocks — not just the WAL — are carved for leftover/deleted data, across three tabs: **Freelist Recovery** walks the freelist trunk chain and carves any table-leaf cells still intact on freed pages (SQLite doesn't zero a page's content when it's freed, only when a new allocation reuses it), including values that spill onto overflow pages, reconstructed by following the overflow chain through pages still confirmed unmodified on the freelist; a "Candidate Tables" column matches by column count against the current schema — a heuristic hint, not a definitive attribution, since a freed page is no longer referenced by any B-tree. **Freeblocks** catches the far more common case of an ordinary single-row `DELETE` that never frees a whole page — SQLite splices the deleted cell into the page's in-page freeblock list instead, and since the page is still part of a live table's B-tree, attribution here is definite, not a guess. **Unallocated Space** shows the raw, unverified bytes sitting in the gap between a page's cell-pointer array and its content area for manual review — SQLite doesn't guarantee anything meaningful survives there (often stale pointer values or all-zero), unlike Freeblocks. All three also see not-yet-checkpointed WAL content: each scan first checks a page's latest committed `-wal` frame before falling back to the main file's own bytes for that page, so a deletion whose only trace is in the WAL is still carved. Freeblocks and Unallocated Space rows also support the embedded Show Hex pane, at exact cell precision, since each row already knows its own on-disk offset and size; Freelist Recovery does not (double-click a row to open its whole containing page in an isolated Hex Viewer tab instead, same as Freeblocks/Unallocated Space's own double-click action). - A **File Structure** tab shows the database file's physical page/cell/header layout (allocation status, cell pointers, freeblocks, unallocated space) — the raw on-disk structure itself, independent of any table/schema interpretation. Selecting a structure item highlights its exact bytes using the same Show Hex byte-provenance as table cells, across the base file and WAL, and vice versa. Each page's row/column detail loads lazily on first expand rather than all at once, so opening a database with many pages stays responsive. Limitations - Table display is capped at 10,000 rows per table (WAL-sourced rows from the history toggle are not counted against this cap). Use SQL queries to load more of the committed table. - WAL row injection (the **Show WAL history** toggle) still decodes table-leaf pages only — overflow payloads there are not followed and show as ``, unlike Freelist Recovery, which does follow them. Frame classification relies on the WAL header's salt and per-frame commit marker, so a WAL file that has itself been partially overwritten past those markers may misclassify trailing frames. - The File Structure tab has no text search of its own — searching it would mean either decoding every not-yet-expanded page up front (reintroducing the hang the lazy loading exists to avoid) or silently skipping unexpanded pages. Use SQL on the table itself, or the Hex pane's own search, instead. - Parse failures fall back to Hex Viewer. ### Property List (plist) - Parses binary and XML plists into the Tree Viewer. - Attempts to decode NSKeyedArchiver plists when possible. Limitations - NSKeyedArchiver decoding is best-effort and may fall back to raw structures. - Parse failures fall back to Hex Viewer. ### XML - Parses XML into the Tree Viewer. - Flattens Android-style `` structures for easier reading. Limitations - Not a validating parser; malformed XML shows an error record. - Plist XML is handled by the plist parser instead. ### JSON - Parses JSON into the Tree Viewer. Limitations - Assumes UTF-8 input; non-UTF encodings may show replacement characters. - Parse errors show an error node in the Tree Viewer. ### Protobuf (Explicit Only) - Open via context menu: **Open as** → **Protobuf**. - Performs a schema-less wire-format decode and displays it in the Protobuf Viewer. Every varint/fixed32/fixed64 scalar is shown with every plausible interpretation (unsigned/signed/zigzag-signed integer, bool, float/double, Unix/Chrome-WebKit timestamp), not just the raw wire value. A length-delimited field is decoded as a nested message when its bytes happen to parse as one, as a UTF-8 string when the decoded text is mostly printable, or as a hex preview otherwise. - Deprecated group encoding (wire types 3/4) is skipped over rather than aborting the rest of the decode. Limitations - Decoding stops (with a warning) at 50,000 entries per message or 6 levels of nested-message depth — a message beyond either cap is truncated, not silently dropped. - Schema-based decoding requires a `.proto` file or descriptor set. ### Android Binary XML (ABX) - Decodes ABX v1/v2 into a structured tree and reconstructed XML (ABX Viewer). - Files with more than one top-level element and no enclosing root (e.g. `settings_secure.xml`) are wrapped in a synthetic `` so they still parse into a tree, instead of failing on "extra content" errors. - Raw XML-illegal control characters in decoded values (occasionally present in real `settings_secure.xml` data) are re-encoded visibly (`\xHH`) rather than breaking reconstruction or being silently dropped. Limitations - Best-effort decode; newer ABX variants may not parse. - `ENTITY_REF`/`PROCESSING_INSTRUCTION`/`DOCDECL` tokens are not known to be emitted by Android's serializer; if encountered they are shown as a comment with a warning rather than reconstructed to exact original syntax. ### SEGB (Biome) - Parses SEGB v1/v2 records into the Table Viewer. - The Properties panel shows the file's Biome stream name (e.g. `Device.Wireless.Bluetooth`), derived from the file's own path rather than its payload — shown for any stream, including ones whose field-level meaning isn't otherwise decoded. - Protobuf payloads decoded automatically: double fields in the plausible Cocoa-timestamp range get a `[possible Cocoa timestamp: ...]` hint alongside the raw number (same range check as the schema-less Protobuf Viewer — see Protobuf Viewer Limitations), nested messages expanded inline with a `[raw: N B: hex…]` hint alongside them (wire type 2 doesn't declare that the bytes really are a submessage — see Protobuf Viewer Limitations), repeated fields collected into arrays. Length-delimited fields that don't decode as UTF-8 or as a nested message are shown as a `` preview rather than being dropped. Full protobuf field number range (up to 2²⁹−1) is supported. - A backing SQLite database is created on open, enabling SQL queries via the built-in editor with autocomplete. The `Payload` column holds human-readable rendered text; `Payload JSON` holds the same data as JSON for `json_extract` queries — floats are always stored as JSON numbers (never swapped for a date string) so comparisons stay type-consistent: - Single field: `json_extract("Payload JSON", '$.2')` → value of field 2 - Nested field: `json_extract("Payload JSON", '$.6.1')` → sub-field 1 of field 6 - Repeated field: `json_extract("Payload JSON", '$.9[0]')` → first occurrence of field 9 - Double-clicking a Payload cell always opens the raw protobuf bytes in the Blob Inspector. - The Table Viewer's embedded **Show Hex** pane is byte-precise here too: selecting a row highlights its exact on-disk bytes (v2's trailer entry included, even though it physically lives at the end of the file, separate from the row's own data), and selecting a specific column narrows the highlight further where that field maps to a distinct stored byte range (State, Timestamp/Creation, CRC Stored, Payload, and — for v2 — Trailer Offset/Entry End Offset). Limitations - Record parsing is best-effort; some records may show a warning. - Payloads that cannot be decoded as protobuf are stored as raw bytes accessible via the Blob Inspector. - The inline protobuf decode stops at the first field with an unsupported wire type (a deprecated group, or anything outside varint/fixed64/length-delimited/fixed32) and shows only the fields decoded up to that point — unlike a record-level parse failure, this truncation is not flagged with a warning. ### LevelDB - Parses LevelDB directories (`.ldb`/`.log`/`.sst` data files, `MANIFEST-*`, `CURRENT`, `LOG`) into a dedicated LevelDB Viewer. - Every record's key state is classified as **Live**, **Deleted**, or **Unknown** from the underlying key-value log — LevelDB marks a deleted key with a tombstone rather than erasing bytes immediately, so a deleted key's last value stays readable until compaction actually reclaims the space. The Records tab lists all states together with an All/Live/Deleted/Unknown filter and full-text search across key/value; the Files tab breaks out per-file Live/Deleted/Unknown counts. - Parses every `MANIFEST-*` file present (not just the current one) into an Overview tab, plus `CURRENT` and the full `LOG`/`LOG.old` content in their own tabs. - Record rows expose key and value as text and hex; selecting a row shows Key / Value / Internal Key in an embedded Hex Viewer. Limitations - Works on directories only, not single files. - No record-count cap — all records are loaded into the UI's item model, so a very large store (millions of records) can be slow to open and memory-heavy. - Deleted-state tracking only reflects what the on-disk files still contain a tombstone for; a delete marker that has itself already been compacted away leaves no trace either way. ### MMKV (Explicit Only) - Open via context menu: **Open as** → **MMKV** (or **MMKV (Encrypted)…** for an AES-encrypted store). MMKV has no magic bytes, so — unlike every other supported format — it cannot be auto-detected from file content at all; it is reachable only through this explicit action, same as Protobuf. - Built on [abrignoni/mmkv-parser](https://github.com/abrignoni/mmkv-parser) (MIT), vendored unmodified under `crush/third_party/mmkv_parser/` — see the file's own header for the exact commit it was vendored from. Crush's own wrapper (`crush/parsers/mmkv_parser.py`) adapts the vendored reader's path-based API to Crush's VFS via temp files, and adds its own encryption-flag cross-check and value-container handling on top (see below); the on-disk format itself was independently re-verified against Tencent/MMKV's own pinned source (see CHANGELOG) rather than trusted from the reference reader's documentation alone. - Every entry is shown in file order (Records tab), each tagged **Live** (the last write for that key), **Superseded** (an earlier write of a key later overwritten — MMKV is append-only between rewrites, so these remain physically present and readable until the next full rewrite), or **Removed** (the key's last write recorded a zero-length value, which is how MMKV represents a removal rather than actually erasing the entry). - The companion `.crc` meta file (found automatically next to the main file) is read for its version/sequence/CRC fields (Overview tab) and its AES vector; an all-zero vector means the store is not encrypted. A non-zero vector normally means the store is encrypted, but that flag alone isn't trusted blindly: if no password is supplied, Crush first tries reading the store as plaintext, and if that walk completes cleanly it overrides the flag and reports the vector as a false positive instead of demanding a key — observed in the field on a real react-native-mmkv store whose meta `version` field was higher than anything this layout had been verified against. - A string-shaped value's on-disk container carries MMKV's own internal length-prefix varint ahead of the actual bytes (framing that tells a string apart from a bare scalar, since the format itself is otherwise untyped — see below). The hex pane and CSV export always show the complete, untouched container, prefix included — Crush never removes anything from what it calls raw. A separate, additional copy with that prefix stripped is used only for the right-click "Inspect Value…" action, so a value that's itself JSON/XML/etc. can actually be re-parsed as such instead of failing on a stray leading byte. - Encrypted stores (AES-CFB, the mode MMKV uses) are supported via **MMKV (Encrypted)…**, given the key as either literal text or a hex string (the dialog asks explicitly rather than guessing from the text's shape, since a real passphrase could coincidentally look like valid hex) — AES-128 (MMKV's default) or AES-256 is also an explicit checkbox. A wrong key is detected structurally (the reference reader requires the decrypted region to walk cleanly to its last byte) and re-prompts rather than showing decrypted-looking garbage. A key supplied for a store that isn't encrypted (zero vector, or no `.crc` at all) is ignored, and the parse metadata reports that instead of claiming decryption. - Each entry's real on-disk byte span (key+value together, and the value container on its own) is computed independently of the vendored reader (which discards positions while walking) by re-deriving the same walk with its own helper functions reused directly, not duplicated. AES-CFB is a position-preserving stream cipher, so this works the same way for encrypted stores too — the file offset math doesn't change, only finding where entries end needs the decrypted bytes. Limitations - No `.crc` file next to the main file means encryption status and the header's recorded region size can't be cross-checked against the meta file's own copy — the Overview tab states this explicitly rather than silently assuming "not encrypted." - A value's on-disk container is untyped (MMKV records the type in the calling app's code, not in the file) — a container that's exactly a length-prefixed string is shown as text, anything else as a varint scalar; this means a single zero byte (empty string, integer `0`, and boolean `false` are all encoded identically) cannot be told apart and is shown as an empty string. This is an inherent format limitation, not a decode failure. - No record-count cap, same tradeoff as LevelDB above. ### Realm Database - Parses `.realm` files and opens them in the Realm Viewer. - Extracts: file header metadata, schema/class list, top-ref comparison across header slots, and table/column data. - Column decoding is spec-driven — each column's on-disk layout (Cluster/ClusterTree B+-tree, and each of Int/Bool/String/Binary/Timestamp/Float/Double/Decimal128/ObjectId/UUID/Link/LinkList/Set/Dictionary) is dispatched from its actual declared type, not guessed from the data's shape. - Files at file format 9 and earlier (pre-Cluster, last written by realm-core ≤5.23.9 / realm-java up to 6.1.0) use a structurally different, older Table/Spec/per-column-B+tree layout — decoded via a separate dedicated path covering all old column types (Int, Bool, String, Binary, StringEnum, Table/Subtable, Mixed, OldDateTime, Timestamp, Float, Double, Link, LinkList, BackLink). - Realm's "streaming form" (e.g. `Group::write()` output, or a Realm Studio file export — where the real top reference lives in a footer at the end of the file rather than the normal header slots) is also resolved automatically, regardless of file format. - SQL queries run against a temporary SQLite representation of the data; the SQL editor supports autocomplete. Tables with Link/LinkList columns get a matching `v_` view with those columns already resolved to the linked row's data. - The Tables tab's embedded **Show Hex** pane highlights real bytes in the actual `.realm` file, not the temporary SQLite copy above — precomputed per cell while the file is decoded, for both the modern Cluster format and the legacy pre-Cluster layout. - A **File Structure** tab shows the file's own physical array/reference-graph layout (file header, streaming-form footer where applicable, Group top array and its children, free list, and per-table Spec/row-storage), independent of the decoded Tables view, with the same Show Hex byte-provenance — selecting a structure item highlights its exact bytes and vice versa. Pre-Cluster tables' row storage (a separate top-level B+-tree per column, rather than one shared Cluster tree) is walked and shown the same way, under a "Column B+-Trees" branch instead of "ClusterTree". - Double-clicking a Summary row navigates directly to that table. - The Views tab resolves Link/LinkList columns to chosen columns of the linked table interactively, opened as a new tab (no SQL needed). A chosen target column that's itself a Link/LinkList can be expanded into its own checklist to resolve one hop further (capped at 8 hops, and a table already on the current chain is never offered again, guarding against a cyclic schema), so a multi-table chain resolves in one view instead of stopping after the first hop — unlike the `v_
` SQL views above, which stay single-hop by design since chaining those further is already straightforward by hand with `json_each()`. A resolved to-one Link column lands in its own named column per selected field (e.g. `messageAttributes.subject`, or `messageAttributes.spamInfo.category` for a further-nested link), so each field stays independently sortable/filterable; a to-many LinkList column still collapses into one combined text cell, since a variable-length list of targets can't become a fixed set of columns without multiplying rows. The opened tab is backed by a real temp SQLite file, so its own SQL box can further pick/reorder/filter columns for display or CSV export (`SELECT col_a, col_c FROM ...`) without touching the Views tab's configuration. - BLOB column cells expose raw bytes in the Blob Inspector on double-click. - Encrypted `.realm` files (Realm's built-in AES-256-CBC + HMAC-SHA224 per-page encryption) are supported when the 64-byte encryption key is known — right-click the file → **Open as** → **Realm DB (Encrypted)…**, enter the key as a hex string. This is a raw key the app itself generates and stores (e.g. Keychain/Keystore), not a password — there is no key-derivation step. Auto-detection on a normal double-click open is intentionally not attempted (a header that fails to decode is equally consistent with "encrypted" and "corrupt/non-standard", and content alone can't distinguish the two), so encrypted files are only recognized as `.realm` at all via their extension. Limitations - An encrypted file without a `.realm` extension cannot be identified as a Realm database at all — its content is ciphertext, indistinguishable from random bytes without the key, so there is no reliable content-based signal to fall back on the way there is for unencrypted files (the "T-DB" mnemonic). - Dictionary columns are decoded — a per-row 2-slot array whose slots are independent BPlusTree roots for keys and values, paired by index position (dictionary.cpp), with the key's declared type read from the spec's `m_types` array rather than the colkey. Values are always Mixed, same decoder/limitations as Mixed columns below. - Mixed and TypedLink values are decoded on their own or as a List/Set/Dictionary-value element. A Mixed value that itself holds a nested List/Set/Dictionary is expanded too (recursively, up to a depth cap that guards against a corrupt/malicious reference chain), with a nested Dictionary always treated as String-keyed since there is no Spec column to read a key type from in that case. Geospatial values (`type_Geospatial`) have no dedicated on-disk case in Realm Core's Mixed storage at all, so any occurrence falls through to a clearly labelled "unsupported type" marker rather than being silently dropped or shown as blank. - Decimal128, ObjectId, UUID, Mixed, Float/Double, and Dictionary are all dispatched from the declared type same as every other column — none of them are guessed from shape. They are verified against hand-built synthetic test data matching the on-disk format spec (and, for Mixed's Decimal128 word order and UUID's byte order specifically, against the relevant Realm Core source directly) rather than a confirmed real-world sample of that type, since none has appeared in the files this parser has been tested against so far. - On a corrupt or partially-overwritten file, if a Cluster leaf's own row-count slot can't be read, the row count is recovered by cross-checking the (still spec-defined) element counts of that leaf's column arrays instead — a corruption-recovery vote across redundant copies, not a guess on well-formed data. Affected tables are marked "(estimated — file corruption)" in the Schema tab and get a note in the Summary tab; this never happens on an intact file. - When row/table data can't be extracted for a class (on either the pre-Cluster or modern path), the Properties panel's "Row data" field states the specific structural reason (e.g. a missing table-refs slot, an invalid Spec reference), not just that it failed. - Parse failures fall back to Hex Viewer. ### Images - Routes supported image formats to the Image Viewer: JPEG, PNG, GIF, BMP, WebP, TIFF, HEIC/HEIF/AVIF, JPEG XL, Apple ATX texture archives (`.atx`, magic `AAPL\r\n\x1a\n` — iOS PosterBoard/wallpaper assets), and Khronos KTX 1.1 textures (`.ktx`, magic `\xabKTX 11\xbb\r\n\x1a\n` — iOS app snapshots, Safari tab thumbnails and some Photos attachment previews). `.ktx` is used for both this container and Apple's ATX one; snapshots appear in either depending on the release, and Safari tab thumbnails were this container in every tested image. - Extracts a focused set of EXIF metadata (camera, time, GPS, dimensions). - Detects embedded C2PA (Content Credentials) manifests for JPEG, PNG, GIF, WebP, TIFF, and HEIC/HEIF/AVIF, and box-form JPEG XL — generator software, actions and their software agent(s), IPTC Digital Source Type (AI-provenance signal, shown with its official IPTC name), ingredients (prior assets), and the claim signature's leaf certificate identity (Signed By/Cert Issuer/Cert Valid). A second, independent check reads the IPTC Digital Source Type directly from XMP (container-agnostic) for images with no C2PA manifest at all. - ATX is parsed as a chunked container (`HEAD`/`FILL`/`astc`/`LZFS` chunks); a raw ASTC 4x4 payload is decoded to an image, with width/height/depth/array layers/mipmap count/pixel format/texture UUID shown in the Properties panel. ASTC's Morton-order block layout has two plausible X/Y interpretations the format itself doesn't disambiguate — both are decoded and the one with smoother macro-tile boundaries is kept, flagged as a heuristic rather than a spec-verified decode. - KTX 1.1 is parsed to the Khronos specification; an ASTC 4x4 payload (`glInternalFormat` 0x93B0) is decoded to an image, with dimensions, depth, array layers, faces, mipmap count, pixel format, byte order and the key/value entries shown in the Properties panel. iOS also writes an LZFSE-compressed variant, flagged by a `Compression_APPLE` key/value entry and carrying an `LZFS` marker ahead of the compressed block; that is decompressed before decoding. An app snapshot is the image the system captured when the app was last backgrounded, so a decoded snapshot can show what was on screen at that point. Limitations - EXIF coverage is not complete; only a subset of tags is shown. - Decoding depends on Qt image codecs installed on the system. - IFD entries are capped at 512 per directory, and SHORT/LONG/SLONG tag arrays at 8 items; RATIONAL/SRATIONAL arrays (e.g. GPS coordinates) are read in full, uncapped. Data beyond the entry/array cap is not read. HEIF/HEIC/AVIF: the TIFF block's start offset inside the container's `exif` payload is located by pattern/offset heuristics (pillow-heif doesn't expose it directly) — on an HEIF variant whose prefix doesn't match, EXIF silently comes back empty rather than partially wrong. - C2PA detection is structure parsing only: the claim signature's leaf certificate is read but never cryptographically verified against a trust store, and revocation is never checked (the Properties panel's `C2PA Signature` row states this explicitly). BMP, ATX, and KTX have no C2PA embedding defined by the spec at all, and a bare (box-less) JPEG XL codestream cannot carry a manifest either — all four are reported as such, not as "not present". - ATX: only plain (uncompressed) ASTC 4x4 payloads decode to an image; other pixel formats and `LZFS`-compressed payloads are parsed for metadata only, shown as text. The Morton-orientation choice is a heuristic (see above), not a documented Apple flag. - KTX: only ASTC 4x4 decodes to an image. The same extension is used for textures shipped inside system frameworks and apps, which carry other pixel formats (other ASTC block sizes, PVRTC, uncompressed) and are parsed for metadata only, shown as text. KTX 2.0 is not read. Only the first mipmap level, array layer and face is decoded; a file declaring more than one is decoded to its first image with a warning. ### Media (Audio/Video) - Routes supported media formats to the Media Viewer for playback. Audio: MP3, WAV, M4A, AAC, FLAC, OGG, Opus, WMA, AMR. Video: MP4, M4V, MOV, MKV, AVI, WebM, 3GP, 3G2. - Detection is primarily by extension; OGG/Opus and AMR files renamed to another extension (e.g. a voice note saved as `.bin`) are still recognized by magic bytes (`OggS` / `#!AMR`). - For OGG/Opus/AMR files specifically, codec, sample rate, channel count, duration, and embedded Vorbis/Opus comment tags (encoder, title, artist, album, date, comment, creation time) are extracted via PyAV and shown in the Properties panel — the encoder tag can reveal the originating app. Limitations - Detection for every other container (MP4/MOV/MKV/AVI/etc.) is extension-only; a renamed file with a mismatched extension is not recognized. - Metadata extraction (codec/tags) only runs for OGG/Opus/AMR; other containers show no extracted metadata beyond file size. - Metadata extraction requires PyAV; without it, only file size is shown. - Playback depends on system multimedia codecs. ### PDF - Renders pages as images (via `pypdfium2`) in a **Pages** tab, with prev/next navigation and zoom (also Ctrl+scroll wheel), alongside a **Text** tab with the text extracted via `pypdf`. - Password-protected PDFs: right-click → **Open as** → **PDF (Encrypted)…**; a wrong password re-prompts instead of failing silently. A normal double-click never auto-prompts, since an encrypted PDF's content can't be told apart from a corrupt one from the header alone. - Properties panel: PDF version, `/Info` fields, and separately XMP metadata (a mismatch between the two is itself a forensic signal). JavaScript presence (`/Names/JavaScript`, `/OpenAction`), signature form fields (`/AcroForm` `/FT /Sig`), and attachment count are always shown, even when none are found, so it's clear these were actually checked. - Embedded files get an **Attachments** tab — open one as a new tab (routed through the normal parser pipeline) or export to disk. - **Revision history**: PDFs saved multiple times without a full rewrite (incremental updates, chained via each trailer's `/Prev` offset) get a **History** tab exposing every revision, including content, JavaScript, or attachments only present in an earlier revision and since removed from the current one. Sub-tabs: **Browse** (one revision at a time, full Pages/Text/Attachments; revisions with JavaScript/signatures/attachments are flagged with ⚠), **Text Diff** (line-level diff between any two revisions), and **Visual Diff** (pixel-level page comparison — catches a redaction box drawn *over* text, which a text diff can't see since the underlying content stream is untouched). Limitations - Without `pypdf`/`pypdfium2` installed, PDFs open in Hex Viewer with a note. - Some PDFs have no extractable text (scanned or protected files) — the Pages tab still renders normally in that case. - JavaScript detection only checks the two standard document-level locations, not every annotation/form-field's own `/AA` actions. Signature-field detection only checks top-level `/AcroForm` fields, not fields nested inside a `/Kids` hierarchy. - Revision detection was validated against classic cross-reference tables; PDF 1.5+ cross-reference *streams* use the same `/Prev` mechanism through `pypdf`'s public API and aren't expected to need special handling, but weren't separately tested against a real-world sample. - Visual Diff doesn't diff pages whose size differs between the two selected revisions (shows the newer one only, to avoid a misleading resize). - `/Info` and XMP metadata field values are truncated to 200 characters each. - The global/case-wide text search index only covers the first 4,000 characters of a PDF's extracted text; the Text tab itself always shows the complete extraction regardless of length. ### Log Files (Explicit Only) - Open via context menu: **Open in Multi-Log Studio**. - Auto-detects JSON Lines, Android logcat, Syslog (RFC 3164), and generic timestamped/plain-text logs. - **Apple Unified Log** (`.tracev3` / `.logarchive`): parsed via the bundled Mandiant `unifiedlog_iterator` binary, invoked in CSV mode. Extracts timestamp, level, process, PID, subsystem, category, event type, and euid. `lossEvent` entries (buffer overflow gaps) are flagged as WARN. A message the binary couldn't fully resolve (e.g. a redacted/private string in a live-system export) comes back as `[partial] ` rather than a blank field. - **Send to Peach**: right-click a `.logarchive` bundle, an iOS full-FS acquisition's `diagnostics/` folder, or any other file → **Send to Peach** hands the source off to the sibling [peach-forensics](https://github.com/kalink0/peach-forensics) log viewer (tagging, Splunk-style search) via a one-shot CLI spawn — no IPC afterward, peach keeps running independently even after Crush closes. Offered for any file, not just recognized log formats — same as **Open in Multi-Log Studio**'s existing lack of pre-filtering, since peach's own TOML text-log configs live in its per-user data dir and aren't visible to Crush to check against; peach's manual sourcetype-confirm-before-Load step is the real gate. The peach binary is bundled the same way as `unifiedlog_iterator` (`scripts/download_peach_binaries.py` when running from source); **Tools → Peach → Binary Path…** overrides it with a different build, **Tools → Peach → Open Peach** launches an empty instance. Since peach has no IPC, correlating several sources in one session means sending them together — multi-select files in the tree (**Send N files to Peach**) or right-click a plain folder (**Send Logs to Peach…**, recursive discovery with a confirm checklist, same picker **Open Logs in Multi-Log Studio** uses). - Multiple files can be loaded simultaneously into a shared, merged timeline. - Custom formats can be defined via a named-group regex and a `strptime` timestamp format; profiles are saved to `~/.config/crush/log_profiles/`. Limitations - Not auto-detected by default; must be opened explicitly. - Timestamp parsing is heuristic for unrecognised formats; logcat logs do not include the year. - Year is assumed to be the current year for Syslog (RFC 3164). - Apple Unified Log parsing requires the platform `unifiedlog_iterator` binary (included in portable builds; run `scripts/download_unifiedlog_binaries.py` when running from source). - Apple Unified Log support is effectively binary-`.tracev3`/`.logarchive`-only: the parser module also has code paths for `log show`'s own JSON/NDJSON/plain-text export styles, but nothing in the auto-detection or explicit-open flow calls them, so a `log show`-exported file is not recognized as Apple Unified Log at all — an NDJSON export in particular would likely get misdetected as generic JSON Lines instead, with unknown levels and garbled messages. - Apple Unified Log: when a `.tracev3` is parsed without a matching boot record, `unifiedlog_iterator` outputs Unix-epoch-relative timestamps (landing near 1970) instead of real wall-clock time. Any entry timestamp before 2000-01-01 is therefore left blank in the Timestamp column rather than shown as a misleading date — but the excluded value is never discarded, only moved: it's kept in the entry's `extra["excluded_timestamp"]` field, visible in the detail panel, since a genuinely tampered/reset device clock would also produce a pre-2000 timestamp and that's evidence, not noise. ### Hex Fallback - Any file without a matching parser opens in the Hex Viewer. - If the format database recognizes it, the Properties panel shows its name, category, platforms, forensic-relevance notes, a reference link, and whether Crush actually parses it yet ("Supported" / "Not yet supported") — even for a format Crush can identify but doesn't decode. Limitations - Raw bytes only; no structured decoding. ## Viewers (What They Do) ### Table Viewer - Sortable grid, row filtering, SQL queries (`SELECT`, `WITH`, and `PRAGMA` only), CSV export. - BLOB inspection and "Open as new tab" for embedded artifacts. - For SQLite databases, the Summary view lists tables and computes row counts. - For SQLite databases with a `-wal` companion: a **WAL Frames** tab (full frame inventory, double-click for raw page bytes) and, per table, a **Show WAL history** toggle that injects Superseded/Uncommitted/WAL-slack rows into the grid — see SQLite Database above. - For SQLite databases: **Freelist Recovery**, **Freeblocks**, **Unallocated Space**, and **File Structure** tabs expose the physical file layout and carve leftover/deleted data — see SQLite Database above. Limitations - Read-only; write queries are blocked. - Large datasets are capped by parser limits (e.g., SQLite 10,000 rows). ### Tree Viewer - Hierarchical view for plist/XML/JSON structures with search and copy. Limitations - Read-only; no inline editing or advanced type casting. ### Text Viewer - Line numbers, search, and lightweight syntax highlighting. - Auto-detects common encodings (UTF-8 and common UTF-16 variants). Limitations - Non-UTF encodings may show replacement characters. - Highlighting is heuristic, not a full parser. ### Hex Viewer - Paged hex + ASCII view, hex and ASCII search, copy options. Limitations - Read-only; no edit mode. - Copy is page-based, not entire file bytes. ### Image Viewer - Fit-to-window scaling, zoom, magnifier. - Rotate left/right (90° steps); the rotated pixmap is cached per angle. Limitations - No crop/export controls in the viewer. ### Media Viewer - Playback with scrub and time display. Limitations - Dependent on OS/Qt codec support. ### ABX Viewer - Split view with parsed tree and reconstructed XML. - The XML pane is pretty-printed (indented, multi-line) and includes a Ctrl+F search bar (regex/case options, hit navigation) shared with the generic Text/XML viewer. Limitations - XML reconstruction is best-effort. ### LevelDB Viewer - Tabbed view: **Overview** (parsed MANIFEST content), **Files** (per-`.ldb`/`.log`/`.sst` file stats, level, key range, Live/Deleted/Unknown counts), **Records** (all records with an All/Live/Deleted/Unknown filter and search), plus one tab each for `LOG`/`LOG.old` if present. - Selecting a record shows its Key / Value / Internal Key bytes in an embedded Hex Viewer. Limitations - No pagination — very large record sets are all loaded at once (see LevelDB parser limitations above). ### MMKV Viewer - Tabbed view: **Overview** (the `.crc` companion's version/sequence/CRC fields and the encryption verdict derived from its AES vector, with the reason when a non-zero vector was overridden as a false positive) and **Records** (every entry in file order, tagged Live/Superseded/Removed — see MMKV parser above). - Records table columns: Index, Key, State, Type, Size, Value; Superseded/Removed rows are colour-coded. A search box and a Live/Superseded/Removed state filter narrow the list. - Long values are truncated on screen (hex preview capped at 64 bytes, text preview capped at 256 characters) purely for render performance — the full value is always reachable via search, **Copy Value**, CSV export, and **Inspect Value…**, never actually discarded. - **Inspect Value…** opens the value's raw bytes in the Blob Inspector. CSV export is also available. - Selecting a row shows the real store file in the embedded hex pane below the table, with that entry's own on-disk bytes highlighted — real byte provenance in context, not just the entry's own bytes shown in isolation. Falls back to showing just the entry's own bytes when its on-disk span couldn't be computed. Limitations - No pagination — same tradeoff as the LevelDB Viewer above: a very large store is loaded into the table all at once. ### Multi-Log Studio - Level toggles (ERROR / WARN / INFO / DEBUG / TRACE / UNKNOWN), free-text search (message, process, PID, subsystem, category), time-range filter with calendar pickers, and per-source visibility toggle. - Sources are colour-coded; each appears as a chip in the source bar that toggles the source on/off. - Background async loading: the tab opens immediately and rows stream in as they are parsed; files of any size are supported without blocking the UI. - Column sorting runs in a background thread — the UI stays responsive during sort; a progress bar appears while sorting large datasets. - Virtual model: no Qt item objects per cell — handles 200 k+ entries with low memory overhead. - Custom format profiles: define a named-group regex (groups `timestamp`, `level`, `process`, `pid`, `message`; extras go to a side panel), a `strptime` string, an optional line-start regex for multiline events, and a level translation map. Live preview highlights each group in a distinct colour. Profiles are saved as JSON and reloaded on next start. - Detail panel shows the raw original line(s) and any extra fields (e.g. `subsystem`, `category`, `event_type`, `euid`, `thread_id` for Apple Unified Log entries). - Context menu: copy message, copy raw line, copy selection as TSV, filter by column value (pins an exact-match filter chip below the toolbar). - **Column filter bar** — a persistent text-input row above the log table with one field per filterable column (Level, Process, PID, Subsystem, Category, Message); typing performs a live contains-match filter complementing the right-click exact-value filter. Limitations - Time filtering only applies to entries with a parsed timestamp. - Multiline event grouping for custom formats requires an explicit line-start regex. ### Realm Viewer - Tabbed view: **Header** (file metadata), **Schema** (class/table list, with Link/LinkList target tables shown), **Top Refs** (comparison across header slots), **File Structure** (physical array/reference-graph layout, with the embedded Show Hex byte-provenance pane), **Tables** (column data, also with Show Hex), **Views** (interactive Link/LinkList resolution), **Freed Data**, **Strings**, **Hex Preview**. Limitations - On a corrupt or partially overwritten file, a column's name may be unrecoverable and falls back to `col_0`, `col_1`, etc. ### Protobuf Viewer - Schema-less decode in a tree view (field numbers, wire types, values). - Wire type 2 (length-delimited) doesn't declare whether a payload is a nested message, a string, or opaque bytes — a field is rendered as a nested message when its bytes happen to parse as one, but a dimmed "raw bytes" hint is always shown alongside it, the same way numeric fields show every plausible interpretation, since a short blob can coincidentally be grammatically valid protobuf without actually being a submessage. - Optional schema-based decode after loading a `.proto` file or descriptor set. - The **Blob Inspector** (any BLOB field, not just files opened as Protobuf) offers the same schema-based decode: select **Protobuf (schema-less)** first, then a schema-loading toolbar appears above the content view. Limitations - Schema-based decoding depends on the `protobuf` Python library and a valid schema. Loading a raw `.proto` file (rather than a pre-compiled `.pb`/`.desc`/`.fds` descriptor set) additionally requires `grpcio-tools` to compile it first. ## Known Gaps (Planned) Format/viewer content gaps only — planned parsing or display work, not general app features (those get tracked as issues instead). - Extended EXIF/metadata viewer. - General XMP metadata (creation/editing history, software chain, beyond the IPTC Digital Source Type tag — see the Images section above) is not extracted for images or media. - ESE database (`.edb`) has no parser or viewer yet. - Embedded Show Hex byte-provenance is not yet extended to every viewer — done so far: Protobuf, Plist/XML Tree, SQLite Table (cells, WAL Frames, File Structure), SEGB/Biome, Realm (Tables, File Structure), and MMKV. Not yet covered: Plist's binary form specifically (an on-disk offset table exists but not for NSKeyedArchiver-wrapped plists, the common real-world case, whose displayed tree comes from a separate resolution pass with no 1:1 relationship to those offsets) and ABX (its decoder builds a flat XML string with no tree at all; the tree shown is from re-parsing that reconstructed text, which has no relationship to the original binary offsets either). Not planned for JSON, XML, or Multi-Log Studio/Log Viewer — their content is already plain readable text, so a byte-offset hex highlight adds nothing a text position wouldn't already show. Also not planned for the Blob Inspector (it only ever shows already-extracted, isolated blob bytes, never the source file's own bytes in context, which is the entire point of this feature) or LevelDB (real-world `.ldb`/`.sst` files are Snappy-compressed by default, and a compressed value's bytes only exist post-decompression, not as a byte range in the file — most records would get no real provenance either way).