feat(core): decode WebKit/Chrome .localstorage UTF-16-LE values#2
Merged
Conversation
…hema
Failing tests for a WebKit/Chrome Local Storage value-decode convenience:
decode_localstorage_value(&[u8]) -> LocalStorageValue { text, lossy } and an
is_local_storage_item_table schema recognizer. Neither exists yet, so the tests
fail to compile (RED).
Coverage: an independent oracle of hand-specified UTF-16-LE bytes (derived from
the Unicode code points + surrogate formula, not Rust's encoder); adversarial
empty / odd-length / lone-surrogate inputs (must set lossy, never panic); and a
sqlite3-CLI real .localstorage round-trip incl. CJK + emoji, gated/skip-if-absent.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A .localstorage file is a standard SQLite database sqlite-forensic already
opens, dumps, and carves; the one artifact-specific gap is that ItemTable.value
is a BLOB holding the string as raw UTF-16-LE (no BOM, no type-prefix byte), so
it surfaces as opaque hex. This adds a thin decode convenience — not a new
reader:
- decode_localstorage_value(&[u8]) -> LocalStorageValue { text, lossy }. The
lossy flag is a struct field (secure by design) so a caller cannot render a
lossy decode as faithful. Odd-length BLOB or unpaired surrogate => U+FFFD +
lossy; empty => empty string; never panics.
- is_local_storage_item_table(&str) recognizes the distinctive ItemTable name so
callers know when the decode applies (name-keyed: the real schema's ON CONFLICT
clauses defeat a lightweight column-name parse).
- DRY: the UTF-16 pairing is factored into decode_utf16_units, reused by the
existing TextEncoding::decode_utf16 (behavior-preserving) — no second decoder.
Validated against an independent sqlite3-CLI oracle: a real .localstorage with
UTF-16-LE BLOB values (incl. CJK + emoji surrogate pairs) round-trips exactly
through the normal reader; plus hand-specified UTF-16-LE bytes (from the Unicode
code points, not Rust's encoder) and adversarial empty/odd/lone-surrogate/NULL
inputs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
RUSTSEC-2026-0194 / RUSTSEC-2026-0195 are DoS-only (not RCE) and reach the tree only via calamine v0.35.0, a dev-dependency of the CLI that pins quick-xml ^0.39, so the tree cannot bump to >=0.41 until calamine widens its requirement. Fleet- wide-fresh: main was green before these advisories published. Interim ignore; drop once calamine allows quick-xml >=0.41. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…16-decode # Conflicts: # deny.toml
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
A small UTF-16-LE value-decode convenience for WebKit/Chrome
.localstoragefiles. This is not a new reader — a.localstoragefile is a standard SQLite database thatsqlite-forensicalready opens read-only, dumps (ItemTable), and carves. The one artifact-specific gap:ItemTable.valueis a BLOB holding the string as raw UTF-16-LE (little-endian, no BOM, no type-prefix byte), so today it surfaces as opaque hex instead of readable text.API (in
core)decode_localstorage_value(blob: &[u8]) -> LocalStorageValue { text: String, lossy: bool }— thelossyflag is a struct field, not a side-channel warning, so a caller cannot render a lossy decode as if it were faithful (secure by design). An odd-length BLOB (a trailing half code unit) or an unpaired surrogate yields U+FFFD and setslossy; an empty BLOB decodes to the empty string; it never panics.is_local_storage_item_table(table_name: &str) -> bool— recognizes the distinctiveItemTablename so callers know when the decode applies. Name-keyed on purpose: the real WebKit/Chromium schema declares the columns withON CONFLICTclauses (key TEXT UNIQUE ON CONFLICT REPLACE, value BLOB NOT NULL ON CONFLICT FAIL) that a lightweightCREATE TABLEparse does not split cleanly.DRY
The UTF-16 code-unit pairing is factored into one private
decode_utf16_units(which now reports lossiness), reused by the existingTextEncoding::decode_utf16(behavior-preserving — the DB-encoding path still discards the flag) and by the new Local Storage decode. No second UTF-16 decoder was added.Validation (oracle-checked, not just self-encoded)
sqlite3-CLI oracle (gated, skips if absent): a real.localstoragefile is built with genuine UTF-16-LE BLOB values — including CJK and emoji (surrogate pairs) — read back through the normal reader, decoded, and asserted to recover the exact original strings.lossy/ replacement / distinctValue::Null), never a panic.Gate
cargo fmt --all -- --checkclean ·cargo clippy --workspace --all-targets -- -D warningsclean ·cargo test --workspace514 passed / 0 failed.Two commits: RED (failing tests) then GREEN (implementation).
🤖 Generated with Claude Code