Repository navigation
feat(il2cpp): validate LineNumberMappings.json before packing it - #76
Conversation
Walk directories recursively for .so/.so.dbg/.so.sym (+ --extension), reading libraries in place instead of requiring a pre-built zip. Multiple paths (dirs and/or zips) are collected first so the one-per-build-id preference spans all of them. Each zip gets its own extraction dir; an unreadable directory root fails instead of exiting 0; a missing path is exit 10. Closes #69 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…links Address review on #70: a directory with no matching libraries is a miswired path, not success (exit 10; empty zip still warns). The walk no longer follows directory symlinks (file symlinks are still read). Add changelog entry and tests. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Address adversarial review on #70: - any I/O error while scanning a directory (unreadable subdir/library, dangling link) now fails with exit 11 instead of silently uploading a partial set - the symlink test used same-build-id copies so dedup masked it; the escaped library now has its own id (verified to fail if links are followed) - pin exit 11 for unreadable inputs; hints for a zip-in-directory and a bare .so; NotFound only maps to exit 10 when the input itself is missing - e2e harness gets a directory flow; changelog/help/README updated; remove a leftover block scope 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…reading whole files put_payload read the packed archive into memory and cloned it for every retry attempt, and sha1_hex_of_file read it whole again; with up to 6 uploads in flight that held ~3x the archive size each. Both now stream in 64 KiB chunks (the PUT keeps an explicit Content-Length, which a presigned S3 PUT requires, and re-opens the file per attempt). Peak RSS, 4 x 100 MB libraries: 564 MB -> 143 MB. Shared by every presigned upload (elf, dsym, pdb, rust). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ing them scan_dir read every library whole just to extract its build-id, arch and richness, all of which live in the headers/notes. Map the file read-only instead (memmap2 was already in the tree via symbolic-debuginfo): the largest-file transient disappears. An empty file, which cannot be mapped, is reported as having no build-id rather than failing the scan. Peak RSS, 4 x 100 MB libraries: 143 MB -> 36 MB; --dry-run 112 MB -> 5 MB. Separate commit because it is the only step needing `unsafe`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
… paths Audit of every std::fs::read / read_to_string / tokio::fs::read outside tests: - upload build artefact PUT (IPA/APK/AAB, hundreds of MB) and the build-info PUT now stream from disk via a shared http::file_body (+ exact Content-Length), instead of reading whole and cloning per retry - ProGuard identify: MD5 + SHA-1 in one streamed pass (was whole-file read) - dSYM / PDB / Rust ELF / `xcode` IPA Mach-O / `dsym` identity reads use a shared symbols::mapped::map_file (the single unsafe, empty-file safe) instead of copying 100+ MB binaries to read a UUID from the headers - ELF zip input: stream entries to disk, then map for identity - source maps: streamed SHA-1/size; read_debug_id maps the file (non-UTF-8 still an I/O error, exit 10) Peak RSS: upload build (300 MB artefact + 200 MB mapping) 661 -> 32 MB; 200 MB ProGuard mapping 241 -> 32 MB; ELF zip of 4 x 100 MB 463 -> 36 MB; 89 MB source map 274 -> 183 MB (JSON tree remains). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…n::Value tree A source map is tens of MB (a huge `mappings` string plus whole sources in `sourcesContent`), and parsing it into a Value held ~2x the file just to read or change a few top-level keys. New inject::mapjson walks the document once with struson (syntax still validated end to end, trailing garbage rejected): - read_debug_id / restamp_id carry-check: top-level string keys only - --strip-sources-content: filtered copy, drops sourcesContent at the top level and inside nested sections[].map, everything else copied verbatim - inject's map rewrite: two passes (read ids, then stream-copy with the ids set) into a sibling temp file, copied over the original so permissions / symlinks survive - the inject debug-id (UUIDv5 over len||bundle||map) is hashed incrementally with the map streamed from disk; pinned bit-for-bit to Uuid::new_v5 by a test across buffer boundaries Peak RSS, 89 MB map: upload 183 -> 32 MB, --strip-sources-content 262 -> 32 MB, sourcemaps inject 298 -> 6 MB. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
`sourcemaps inject` read the whole bundle and rewrote it via
`format!("{content}{stub}")`: a full second copy plus the String. The bundle is
now mapped read-only (zero-copy &str for the existing tail / suffix / contains
checks), a plan is computed, the mapping is dropped (Windows refuses to truncate
a mapped file), and the stub is appended - or, to re-key, our own trailing stub
is cut off and the new one appended - with no copy of the bundle. Output bytes
are identical. 150 MB bundle: 317 MB -> 2.4 MB dirty memory.
Tests:
- mapjson differential tests against the serde_json::Value logic it replaced
(600 generated documents per operation, duplicate keys, truncation / trailing
garbage rejected everywhere, a 200 000-case random-corruption sweep proving
the stream accepts exactly what serde accepts and never launders invalid
JSON into an upload, a 3 MB mappings string streamed through intact)
- inject on-disk behaviour: exact bytes for awkward bundles (empty, CRLF, BOM,
multibyte, one huge line), restamp cuts back to the original bytes, foreign
ids only append, dry runs change no byte or mtime, permissions and symlinks
survive on both files, read-only / non-UTF-8 bundle and unusable maps are
errors that leave the files untouched with no temp files
🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ory budget - tests/file_handling.rs: exact bytes on disk after inject (idempotent, dry-run, stdout empty), large artefact PUT intact with exact Content-Length, directory libraries uploaded with their own bytes + build-ids, --strip-sources-content uploads a valid stripped copy and spares the original, ProGuard raw-byte identity (empty / CRLF / non-UTF-8 / large), zero-byte library in a scan - compress: every method round-trips sizes around the copy buffer with CRC verification, missing source is an error, a 12 MB source packs by streaming - symbols::mapped: directory, page-straddling sizes, permission denied - upload::http::file_body: every byte around the chunk size, missing file and a file shorter than its declared length fail the request - scripts/e2e_flows.py: peak-RSS budget (64 MB) for each large-input flow on ~96 MB inputs, measured in a throwaway wrapper process; 5 of 6 flows fail it on the pre-streaming binary (126-222 MB), so it has teeth. Skipped on Windows. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ist, manifests) New corruption tests found four real defects, fixed here: - a Mach-O without LC_UUID was reported / uploaded as the nil UUID (dsym uuid, debug-files upload --type dsym); nil slices are now skipped and a bundle with none is rejected (xcode upload-dsyms fails the build for it) - --type elf accepted any format symbolic knows, so a Mach-O named *.so became an "elf" symbol keyed by its Mach-O UUID; require the \x7fELF magic - build-env read-plist (and the xcode flows) aborted the process with SIGABRT on a plist nested a few tens of thousands of levels deep (recursive plist::Value); parse on a 256 MiB-stack thread, refuse plists over 1 MiB - ios-deps reported a nameless `library::` dependency for a mangled Podfile.lock line such as `- (2.0)` Tests: tests/broken_files.rs drives the binary over broken source maps (19 JSON shapes x upload / strip / inject, incl. 100k-deep nesting), damaged ELF libraries and zips, Mach-O / dSYM / PDB, plists, manifests, build payloads and artefacts; unit sweeps truncate / flip every ELF, Mach-O, PDB, xcactivitylog and .app input and assert the result is an error or the TRUE identity - never a panic and never a different id (the identity is the upload key). Verified against the pre-fix code: the new tests fail there for each defect above. Characterised, not changed: IL2CPP mappings JSON is packed unvalidated. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…n Windows CI) The helper's only user is a #[cfg(unix)] test, so the Windows test jobs failed to compile under -D warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…while scanned Review (Cursor, #72): a library truncated or rewritten under its mapping is undefined behaviour (SIGBUS in practice). That is acceptable for build-finalized outputs, but the constraint is now stated where integrators read it: the SAFETY note, `--help` for `paths`, and the changelog. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… Mach-O size cap Review (Cursor, #73): the cap guarded std::fs::read growing a multi-GB Vec, which no longer happens (the file is mapped, only headers are read). It made `dsym uuid` silently print [] for a legitimate slice over 1 GiB that `debug-files upload` accepted. Removed, with a sparse >1 GiB test (verified to fail with the cap back). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…m a 0600 temp Review (Cursor, #74): fs::copy carries the source file's metadata onto the destination, so every rewritten map was tightened to the temp file's 0o600 (and on Windows given FILE_ATTRIBUTE_TEMPORARY): unreadable by a deploy user afterwards. Write the temp file's content into the original through a truncating open instead, which keeps mode, inode, symlink and hard links. Pinned with a 0o644 map that is also hard-linked (verified to fail with fs::copy back). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…-vacuous The map in permissions_survive_on_both_files was pinned at 0o600, the very mode of the sibling temp file the rewrite goes through, so it passed even while that mode leaked onto the map (the bug Cursor found on #74). Use 0o640 / 0o664. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
… docs Review (Fable): reqwest follows a 307/308 only when it can replay the body, and a streamed body cannot be replayed, so any redirect on a symbol / artefact / build-info PUT became a hard failure (exit 30) where it used to be followed transparently. S3 answers a freshly created bucket's presigned URL with exactly such a temporary 307. http::put_file now owns the streaming PUT for all three call sites: retries like send_with_retry, re-issues the PUT (new stream from the file, same Content-Length and Content-Type) to Location on 307/308, up to 5 hops, and passes an unusable redirect through for the caller to report. Also: send_with_retry's doc no longer claims callers clone the body, and the mapped.rs note no longer calls its unsafe the crate's only one. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…n when strip is skipped Review (Fable): - struson's default reader rejects exponents beyond +-99 and numbers over 100 chars (a guard for code that PARSES numbers). We only skip or copy their text, but transfer_to enforced it: a map with `1e100` made --strip-sources-content give up and upload the UNSTRIPPED map (sources included), exit 0, silently; inject exited 11. Reader now built with restrict_number_values = false (verified: the new test fails with it back on). - The strip fallback for an unusable map now logs a warning saying the upload is UNSTRIPPED instead of passing silently. - The map rewrite's scratch file is tempfile::tempfile() (anonymous, OS temp dir) instead of a sibling NamedTempFile: no write access to the map's directory needed (fs::write never needed it), and a killed run leaves no stray .tmpXXXX. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…ack, read-only dir, docs)
- tests follow the unrestricted-number reader: valid-but-unusual numbers (1e100,
beyond any double, 150 digits) copy verbatim; the 200k corruption sweep
tolerates exactly serde's 'number out of range'; the CLI strip test that relied
on 1e999999 to force the fallback now uses a non-object map and asserts the
UNSTRIPPED warning; new REGRESSION test that strip removes the sources from
maps with such numbers (it used to upload them, silently)
- inject rewrites files in a read-only directory (anonymous scratch file): test
- plist: if the 256 MiB parser thread cannot be spawned, a small (<=16 KiB) plist
is parsed inline and a larger one reads as {} - both with a warning, no longer
silently; a panic in the parser warns too. The true worst-case nesting under the
1 MiB cap (3-byte refs, ~149k levels) is now pinned, not just 110k.
- docs: CLAUDE.md no longer calls map_file's unsafe the crate's only one; changelog
notes that streamed PUTs follow 307/308 themselves
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Review (Cursor, #74): the anonymous scratch lived in $TMPDIR, so rewriting an 89 MB map needed 89 MB free there; in CI containers /tmp is often a small tmpfs and inject failed (exit 10) although dist/ had room. tempfile_in(<map's dir>) keeps it on the volume that already holds the map, falling back to the OS temp dir only when the directory refuses a file (read-only dir). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Review (Cursor, #75, repeated): inject_one stamped the bundle first and only then discovered the map was invalid / not an object / read-only, exiting 10/11 with a stamped bundle beside an un-keyed map; a retry would not repair it because restamp_id leaves our own stub alone. Preserving the old ordering was wrong. The map rewrite is now PLANNED before the first byte of the bundle changes: validated (a JSON object), staged in a scratch file beside the map, and the original opened for writing (a read-only map is refused here). Then the bundle is edited and the staged rewrite applied. Stats and the 'injected' log follow success. restamp_id also treats a non-object map as unusable, so a stamped bundle is not re-keyed for a map the rewrite would refuse. Tests (the first two fail with the old ordering): unusable maps leave BOTH files untouched (also at CLI level, with no 'injected' log line), a read-only map leaves the bundle unstamped, a regenerated unusable map does not restamp, and inject works with TMPDIR pointing nowhere. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The mappings were packed as-is, so a truncated, empty, binary or wrong file
uploaded "successfully" and every IL2CPP crash then failed to symbolicate with
nothing pointing back at the upload (characterised in tests/broken_files.rs).
validate_mappings streams the document (these files run to tens of MB) against the
shape the symbolicator reads, {cpp_path: {cs_path: {cpp_line: cs_line}}}: objects at
each level, C++ line keys that are u32, C# line values that are non-negative
integers fitting u32. Corrupt content is InputInvalid (exit 11) naming the file and
the JSON path of the problem, in dry runs too, before anything is packed or sent;
a real I/O failure stays an I/O error. An empty map is accepted with a warning.
Tests: the committed fixtures validate (exact counts); 23 malformed / misshapen
documents are rejected (incl. BOM, trailing data, two documents, 50k-deep nesting,
u32 overflow, floats, negative lines); every strict prefix of a real map is
rejected; a 300 000-entry map validates; at CLI level corrupt maps exit 11 for dry
and real runs with nothing sent, and valid fixtures still pass. --help, README
and changelog updated.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
docs/unity-il2cpp-linenumber-mappings.md (2.1) says the mappings may carry an optional top-level __debug-id__ sentinel that parsers ignore. The validator treated it as an entry and would have rejected a valid file. Its value is skipped whatever it holds (still syntax-checked); only that exact key is special. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
…for the previous commit) The previous commit's message and changelog described this, but its code edit had not applied. docs/unity-il2cpp-linenumber-mappings.md (2.1): an optional top-level __debug-id__ sentinel is ignored by parsers; the validator now skips its value whatever it holds (still syntax-checked), and only that exact key is special. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Deep review (PR tip
4234404)No remaining P0–P3. The change does what it claims: a corrupt
LineNumberMappings.jsoncan no longer upload as success.What I checked
- Call site:
run_il2cpp_linemap_uploadrunsvalidate_mappingsbeforetempdir/pack_entries/ the presigned POST, including--dry-run. I/O open failures stayError::Io(exit 10); shape/syntax failures areInputInvalid(11). Integrators in the 10–39 band will not fall back and re-upload the same bytes — that is the right contract for this gate.- Shape vs the symbolicator: Sentry
symbolic-il2cppLineMapping::parseis the documented reader. It requires a top-level object, skips exact key__debug-id__, parses C++ line keys withstr::parse::<u32>(), and takes C# line values viaas_u64().try_into(). This validator is the same shape, slightly stricter on non-object siblings (fail the file instead of silently dropping the entry). Unity/Embrace samples are"2576": 24— string keys, JSON-number values — which matches the fixtures and the tests.__debug-id__: The first commit would have treated a non-object sentinel as a misshapen cpp entry. Sentry writes"__debug-id__": {"<uuid>": {}}(that would have passed as an empty nested object), but a string/null sentinel would not. The follow-up skip-whatever-it-holds (still syntax-checked viaskip_value) matches both the epic doc and Sentry’scontinue. Only that exact top-level key is special.- Streaming: Names are held one cpp/cs path at a time; the file is not loaded into a
Value. Default struson nesting cap is 128; this document is three levels. Invalid UTF-8 is classified as content (11), not I/O — pinned by the binary case.- Public surface:
--type il2cpp-linemapclap text, README, and changelog all describe the new exit 11. Empty{}still exits 0 with a warning, by design.Tests I ran
cargo test --bin bugsee-cli symbols::il2cpp_linemap::tests— 12 passed on4234404(fixtures, 23 malformed cases, every prefix of the iOS fixture, 300k-entry stream,__debug-id__skip + syntax-fail, missing file → Io).cargo test --test broken_files il2cpp— 2 passed: corrupt maps exit 11 for dry and real runs withexpect(0)on POST/PUT; android/ios fixtures still dry-run 0.Residual (not findings)
- Success is proof of a well-formed map, not a non-empty one.
{}/{ "a.cpp": { "A.cs": {} } }still warn and exit 0; that is documented.- The 50k-nesting case is rejected at depth 3 (
"a"is not a C++ line key) rather than by walking the whole tree. Harmless: a real map is only three objects deep, andskip_valueon the sentinel still hits struson’s 128 cap.
- Overall risk: Low
- Merge recommendation: Approve once #75 (the stack base) is in. Behaviour change (corrupt maps 0 → 11) is changelogged.
- Most important issues to fix: None remaining.
- Positives: Gate sits at the right point in the upload path; streaming is the right tool for tens-of-MB maps; error messages name the file and the JSON path; catching
__debug-id__in follow-up commits (and testing string/null/object/truncated sentinels) was the right call.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Deep Code Review
PR intent:
debug-files upload --type il2cpp-linemapused to packLineNumberMappings.jsonas-is, so a truncated, empty, binary, or wrong-shape file uploaded with exit 0 and every later IL2CPP crash failed to symbolicate with nothing pointing at the upload. This gates the document as a stream against the 3-level{cpp_path: {cs_path: {cpp_line: cs_line}}}shape (non-negativeu32line numbers), fails InputInvalid (exit 11) before pack/POST (dry-run included), skips the documented top-level__debug-id__sentinel, and still accepts an empty{}with a warning.Reviewed with surrounding code, not the diff alone:
validate_mappings+ tests insrc/symbols/il2cpp_linemap.rs; call site inrun_il2cpp_linemap_upload(validate beforepack_entries/ presigned POST); exit-code mapping (InputInvalid→ 11, no fallback);--help/ README / CHANGELOG; fixtures;inject::mapjsonstruson settings (whererestrict_number_values: falseis actually justified); Sentrysymbolic-il2cppLineMappingshape (__debug-id__as a fake cpp entry{uuid: {}}, cpp-line keysu32, cs-line valuesu32).Adversarial checks that did not become findings: empty vs missing file (11 vs 10); every strict prefix of the iOS fixture; BOM / two documents / trailing junk; string/null/float/exponent/
u32-overflow line values;__debug-id__as string/number/null/object (skipped) vs__other__(rejected); Sentry’s{uuid: {}}sentinel (valid 3-level empty object, also fine without the skip); TOCTOU between validate and pack (same class as every other CLI read of a finished Unity artefact); empty{}accepted with a warning (documented exception to “success means usable”).Tests I ran (green):
cargo test --bin bugsee-cli il2cpp(14);cargo test --test broken_files il2cpp(2);cargo test --test debug_files_flags il2cpp(3);cargo test --test metadata_format il2cpp(1). Also inspecteddebug-files upload --help— the type description matches the new gate. Did not run the full crate, clippy-D warnings, e2e, orcargo +1.88 check.Findings
One non-blocking P3 (inline). No P0–P2.
Verdict
- Overall risk: Low
- Merge recommendation: Approve — the behaviour change is the point, changelogged, and the gate sits in the right place. Stacked on #75; retarget as that merges.
- Most important to fix: Optional: leave struson’s number-length/exponent cap on (this parser needs the opposite of
mapjson). Not a merge blocker.- Positives: Call site fails before pack/HTTP in both dry and real runs (
expect(0)on POST/PUT). Corrupt vs I/O split is correct (empty/truncated/binary → 11; missing file → 10). Sentinel skip landed in a follow-up with tests for “whatever value, still syntax-checked, only that exact key”. Streaming walk is iterative (no recursion on the 3-level shape); 300k-entry test exists.--help, README, and CHANGELOG### Changedstay in sync.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review (
5c0e340)This tip is a merge of
main(the #75 stack) intofix/il2cpp-validate-mappings. No change tovalidate_mappings, the upload call site, tests,--help, README, or changelog vs4234404.Prior P3 (
restrict_number_values: falsecopied frommapjson) is not addressed. No new findings from the merge. IL2CPP unit tests still green (14).
- Overall risk: Low
- Merge recommendation: Approve. The stack base is now in
main; this PR can retargetmain.- Most important to fix: Optional P3 only — leave struson's number cap on. Not a merge blocker.
- Positives: Same as before — gate still sits before pack/POST.
Sent by Cursor Automation: Bugsee code review
… the error Review (Cursor, #76, P3): restrict_number_values=false was copied from inject::mapjson, where copying exotic numbers verbatim is the point. This validator parses C# lines as u32, so the default guard (no exponent beyond +-99, no token over 100 chars) is exactly right, and it stops a huge leaf number (wrong file, partial write) from being buffered and echoed into the error. The number shown is also capped at 32 chars. Pinned with 50 000-digit and huge-exponent leaves. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The previous commit's new test (a 50 000-digit leaf) failed: struson's own error text echoes the offending token. Cap the detail at 240 chars so stderr stays short; the test passes now. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
There was a problem hiding this comment.
Stale comment
Re-review (
a9c734c)Prior P3 fully addressed.
JsonStreamReader::newkeeps struson's default number guard (no exponent beyond ±99, no token over 100 characters). C# line values that still failparse::<u32>()are shown truncated to 32 characters; reader messages that echo a token are capped at 240 characters before they hit stderr. Pinned bya_huge_number_is_rejected_cheaply_with_a_short_message(50 000-digit leaf and1e+ 40 nines; error < 1000 bytes).IL2CPP unit tests: 15 passed on this tip, including the new case and the previous malformed/prefix/sentinel/300k suite.
No remaining P0–P3.
- Overall risk: Low
- Merge recommendation: Approve
- Most important issues to fix: None remaining
- Positives: The follow-up also capped struson's own
UnsupportedNumberValuetext (the 50k-digit test failed until that), which is the part of the P3 that would actually have shown up in a CI log.Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Stale comment
Re-review (
a9c734c)Prior P3 fully_addressed.
bab32eddrops the customReaderSettings(JsonStreamReader::new— default number-length/exponent cap stays on) and truncates a failed C# line token to 32 chars.a9c734ccaps the reader’s own message at 240 chars before it is interpolated into theInputInvalidtext, so aUnsupportedNumberValuecannot dump a huge token on stderr. New testa_huge_number_is_rejected_cheaply_with_a_short_messagecovers a 50 000-digit leaf and1e+ 40 nines; message stays under 1 000 bytes.
1e3is still rejected (exponent 3 is inside the reader cap, thenparse::<u32>()fails). Sentinel skip, empty-map warning, and the exit-11-before-pack path are unchanged.Ran:
cargo test --bin bugsee-cli symbols::il2cpp_linemap::tests— 13 passed ona9c734c.No remaining P0–P3.
- Overall risk: Low
- Merge recommendation: Approve. Stack base is on
main; this can retargetmain.- Most important issues to fix: None remaining.
- Positives: The optional P3 was the right tightening and is now pinned by a test that would fail if the error echoed the number.
Sent by Cursor Automation: Bugsee code review
There was a problem hiding this comment.
Re-review (a9c734c)
PR intent: debug-files upload --type il2cpp-linemap used to pack LineNumberMappings.json as-is, so a truncated, empty, binary, or wrong-shape file uploaded with exit 0 and every later IL2CPP crash failed to symbolicate with nothing pointing at the upload. This gates the document as a stream against {cpp_path: {cs_path: {cpp_line: cs_line}}} (non-negative u32 lines), fails InputInvalid (exit 11) before pack/POST (dry-run included), skips the documented top-level __debug-id__ sentinel, and still accepts {} with a warning.
What changed since 5c0e340: bab32ed + a9c734c address the open P3. JsonStreamReader::new keeps struson's default number guard (no exponent beyond ±99, no token over 100 chars). A C# line that fails parse::<u32>() shows at most 32 chars; any reader error detail is capped at 240 chars before it reaches stderr. Pinned by a_huge_number_is_rejected_cheaply_with_a_short_message (50k-digit leaf and a huge exponent).
Reviewed with surrounding code, not the diff alone: validate_mappings + tests; run_il2cpp_linemap_upload (validate before tempdir / pack_entries / presigned POST); InputInvalid → 11 (no integrator fallback); clap --help / README / CHANGELOG ### Changed; Embrace/Sentry shape ("2576": 24, __debug-id__ as {uuid: {}} or ignored); inject::mapjson still disables the number guard for a reason that does not apply here.
Adversarial checks that did not become findings: empty vs missing (11 vs 10); every strict prefix of the iOS fixture; BOM / two documents / trailing junk; string/null/float/exponent/u32-overflow values; __debug-id__ as string/number/null/object (skipped, still syntax-checked) vs __other__ (rejected); Sentry's {uuid: {}} sentinel (valid 3-level empty object even without the skip); TOCTOU between validate and pack (same class as every other finished-artefact read); empty {} / { "a.cpp": { "A.cs": {} } } warn and exit 0 (documented exception to “success means usable”). struson reads a number token before applying the 100-char guard, so a pathological leaf is still buffered internally — stderr no longer echoes it, and a real Unity map is many small integers, not one giant token.
Tests I ran (green): cargo test --bin bugsee-cli il2cpp (15); cargo test --test broken_files il2cpp (2); cargo test --test debug_files_flags il2cpp (3). Inspected debug-files upload --help — type text matches the gate. CI on this tip is green (test/e2e all platforms, clippy, rustfmt).
Findings
Prior P3 fully_addressed. No remaining P0–P3.
Verdict
- Overall risk: Low
- Merge recommendation: Approve. Behaviour change (corrupt maps 0 → 11) is changelogged; the gate sits before pack/HTTP; the stack base is on
main. - Most important to fix: None remaining.
- Positives: Call site fails before pack/HTTP in dry and real runs (
expect(0)on POST/PUT). Corrupt vs I/O split is correct. Sentinel skip landed with tests for “whatever value, still syntax-checked, only that exact key”. Streaming walk is iterative; 300k-entry test exists.--help, README, and CHANGELOG stay in sync. The follow-up correctly treated the number guard as the opposite ofmapjson, then capped struson's own error text when the 50k-digit test proved it echoes the token.
Sent by Cursor Automation: Bugsee code review


Stacked on #75 (→ #74 → #73 → #72 → #70). Retarget as those merge.
debug-files upload --type il2cpp-linemappackedLineNumberMappings.jsonas-is, so a truncated, empty, binary or wrong file uploaded "successfully" and every IL2CPP crash then failed to symbolicate with nothing pointing back at the upload. (This was the gap pinned as a characterisation test in #75, and left for a decision.)What it checks
il2cpp_linemap::validate_mappingsstreams the document with struson (these files run to tens of MB; nothing is held in memory) against the shape the symbolicator reads,{cpp_path: {cs_path: {cpp_line: cs_line}}}:u32;u32(no strings,null, floats, exponents, negatives).Corrupt content →
InputInvalid(exit 11) namingLineNumberMappings.json, the file path and the JSON path of the problem — in--dry-runtoo, before anything is packed or sent. A genuine I/O failure stays an I/O error (10). An empty map{}is accepted with a warning (a project can have none).Behaviour change (changelog
### Changed)Corrupt mappings used to exit 0. They now exit 11. Integrators that treated success as proof of a usable map can now rely on it; any that fed this command known-bad files will see the new failure.
--helpand README updated.Tests
u32values, non-numeric / empty / negative / over-u32line keys, 50 000-deep nesting.expect(0)on POST and PUT); the shipped android/ios fixtures still pass.-D warnings/ full suite (647 unit + integration) / e2e /cargo +1.88 checkgreen.🤖 Generated with Claude Code