"Parser-discipline audit of the non-NIF, non-ESM readers of untrusted game-archive input — BSA/BA2/CSG archives, BGSM/BGEM, Starfield CDB, Havok HKX packfiles, FaceGen sidecars, MenuXml parse side, Steam VDF/ACF"
Install
npx skillscat add matiaszanolli/byroredux/claude-commands-audit-parsers Install via the SkillsCat registry.
Parser Discipline Audit (archives, materials, packfiles, sidecars)
Read _audit-common.md and _audit-severity.md for shared protocol.
Archives and mod files are attacker-controllable. This skill audits reader discipline for formats no other audit owns; per-game content claims (parse rates, rendering) stay with the per-game audits; NIF blocks /audit-nif, ESM /audit-esm, .spt /audit-speedtree, .pex/.psc /audit-papyrus; HKX playback (cinematic systems, root motion, completion events) stays /audit-scripting Dim 5 — this skill owns the HKX decode.
| Crate | Reads | Consumer |
|---|---|---|
crates/bsa/src/ (archive/, ba2.rs, csg.rs, uvd.rs, safety.rs, naming.rs, read_at.rs) |
BSA v103/v104 zlib, v105 LZ4 frame; BA2 v1/2/3/7/8 GNRL + DX10 (DDS header reconstructed; v3 LZ4 block); FO4 .csg, .uvd header (docs/engine/archives.md, docs/engine/fo4-csg-format.md) |
byroredux/src/asset_provider/archive.rs, byroredux/src/cell_loader/precombined.rs |
crates/bgsm/src/ |
BGSM/BGEM v1–v22 + template chain | byroredux/src/asset_provider/material/{provider,merge}.rs |
crates/sfmaterial/src/ (reader.rs, index.rs) |
Starfield materialsbeta.cdb — generic object tree + the streaming per-material MaterialIndex |
byroredux/src/asset_provider/material/cdb.rs (header probe + MaterialIndex::build/lookup; each valid CDB's index builds on a named worker thread started at discover_starfield_cdbs, single-flight OnceLock slot, prefetch builds serialised — dd759b762, #5291), material/merge.rs (apply_cdb_material) |
crates/hkx/src/ |
Havok 2010 packfile, 32-bit LE (Skyrim LE) + 64-bit LE (SE) | byroredux/src/asset_provider/animation.rs (sole caller) |
crates/facegen/src/ |
.egm / .egt / .tri |
byroredux/src/npc_spawn/resumable/runtime.rs (EGM only) |
crates/menuxml/src/{parse,tex,font}.rs |
Oblivion/FO3/FNV menu XML (lenient scanner), its own DDS decoder (BC1/2/3 + masked 8/16/24/32-bpp) and .fnt fonts — parse robustness only, the rest is /audit-ui |
crates/menuxml/src/menu.rs |
crates/game-detect/src/{vdf,steam,catalog}.rs |
Steam libraryfolders.vdf / appmanifest_*.acf (policy: /audit-tooling Dim 5) |
launcher, byro-detect |
Contract every reader must meet (a finding violates one): (a) a file-controlled size is bounded before memory is reserved; (b) malformed bytes give Err with field context — never a panic, abort or unbounded loop; (c) a short/degraded decode is a documented Ok + warning or an error, never silent; (d) a real-data sweep exists and cannot go green vacuously.
Parameters (from $ARGUMENTS)
--focus <dimensions>: dimensions to run (default all 6).--crate <name>: one crate.- Delta-first: run each
First step, thengit log --since=<last AUDIT_PARSERS date> -- <Paths>; deep-audit only dimensions whose Paths changed.
Extra Per-Finding Fields
- Dimension: Size Discipline | Error Semantics | Version Gating | Corpus Gates | Decode-Consumer Wiring | I/O & Paths
- Trigger Input: the byte-level condition (field, value, file). Severity per
_audit-severity.md: abort/OOM/stack overflow reachable from an archive or mod file = HIGH (CRITICAL if UB); vanilla content failing to load = HIGH; silently wrong bytes = MEDIUM–HIGH; missing gate/test = LOW–MEDIUM.
Phase 1: Setup
mkdir -p /tmp/audit/parsers;gh issue list --repo matiaszanolli/ByroRedux --limit 200 --json number,title,state,labels > /tmp/audit/parsers/issues.json; dedup per_audit-common.md.- Read the newest
docs/audits/AUDIT_PARSERS_*.mdif any. cargo test -p byroredux-bsa -p byroredux-bgsm -p byroredux-sfmaterial -p byroredux-hkx -p byroredux-facegen -p byroredux-menuxml -p byroredux-game-detect; record counts. Real-data suites, data present, one crate at a time:BYROREDUX_REQUIRE_GAME_DATA=1 cargo test -p <crate> -- --ignored. NeverComponentDatabaseFile::parsethe vanilla CDB (~9.19 GiB tree, #4274);real_cdb.rsstreams.
Phase 2: Dimensions (max 3 concurrent agents)
Dimension 1: Untrusted-size discipline
Paths: crates/bsa/src/, crates/hkx/src/, crates/facegen/src/, crates/sfmaterial/src/{reader,index}.rs, crates/bgsm/src/reader.rs, crates/menuxml/src/{parse,tex,font}.rs, crates/game-detect/src/vdf.rs.
First step: grep -n 'with_capacity\|vec!\[0\|read_to_end\|as usize' <Paths>; trace each hit to the bound before it.
Guards (confirm live, not #[ignore]d): entry_count_rejects_attacker_u32_max, over_ratio_payload_is_rejected_at_the_ceiling (crates/bsa/src/safety.rs); malicious_bsa_folder_count_u32_max_rejected (crates/bsa/src/archive/tests.rs); malicious_file_count_u32_max_rejected_before_allocation, lz4_flex_is_pinned_to_the_safe_decoder (workspace lz4_flex 0.14 keeps safe-decode — feature list unchanged by the 20ad630a6 bump; off = heap overflow no catch_unwind catches, HIGH), lz4_decompress_is_panic_guarded (crates/bsa/src/ba2.rs); oversized_read_len_is_rejected_before_it_is_reserved (crates/bsa/src/csg.rs); decode_spline_animation_rejects_a_sample_count_bomb, decode_spline_animation_rejects_a_block_claiming_its_whole_clip (crates/hkx/src/animation.rs); rejects_morph_count_over_cap (crates/facegen/src/egm.rs); parse_with_limits_rejects_object_tree_before_materialising_it (sfmaterial).
Beyond the guards:
- One implementation of the ceilings —
MAX_ENTRY_COUNT10 M,MAX_CHUNK_BYTES1 GiB,MAX_RECORD_TOTAL_BYTES2 GiB,inflate_bounded/inflate_bounded_zlib. A reader passing a file-controlled length toVec::with_capacity, orread_to_endon a decoder with nodeclared + 1cap, is the regression; so is a second copy of the constants. - Record reservations are also file-relative (#4661):
capacity_hint(count, remaining_bytes, min_record_bytes)boundsread_general_records,read_dx10_recordsand the BSA folder/file tables by the bytes left. A new table reader reserving on the absolute cap alone is the regression. declared_size(BSA, BA2,asset_provider/archive.rs) hands file-controlled sizes to the streaming budget: BSA runs them throughchecked_chunk_size; BA2 sums rawunpacked_sizes with no ceiling — check no consumer allocates from it.- Sizes computed from data outside the archive (NIF-carried counts into
CsgArchive::read_psg, #3758) need their own overflow-checked bound. - File-value arithmetic (
count * stride,offset + len) ischecked_*or bounded by a preceding cap (hkxbone_count > 4096precedes its multiplies). - Nesting: menuxml (tiles
depth > 48, expressions> 64,MAX_READ_DEPTH32, include-cycle set;include_cycles_terminate;MenuFileSourceproviders must stay archive-backed); bgsm templates (DEPTH_LIMIT16 + visited set,resolve_breaks_self_reference_cycle). sfmaterial value nestingMAX_NESTING_DEPTH64 →Error::NestingTooDeep(#4657,self_referential_struct_is_rejected_instead_of_overflowing_the_stack);vdf::parse_entriesMAX_VDF_DEPTH32 (#4673,pathological_nesting_hits_the_depth_cap); menuxml includes re-enter atdepth + 1and are capped byMAX_INCLUDE_SPLICES256 (#4650) plus document-wide byte/tile budgets, since splice count alone let a 64 KiB fragment x 256 splices materialise 2.4 M tiles (#5314, 23fd95055:MAX_INCLUDE_BYTES256 KiB,MAX_INCLUDE_FRAGMENT_BYTES64 KiB checked before the per-document cache,MAX_DOCUMENT_TILES16,384); a fragment that cannot fit the splice budget is refused before it is cached (cachesNone), and every fetch, spliced or refused, is charged againstMAX_INCLUDE_FETCHED_BYTES(2x the splice budget) before any probe (fixed in 6a132a967, #5399:refused_includes_are_bounded_by_the_fetch_budget). Decoded menu textures are a second unbounded-retention site:MenuRendererkeys decodes by resolved archive path (one fetch + decode + charge for every spelling of a file) and charges each againstMAX_DECODED_TEXTURE_BYTES(1 GiB, from the DDS header viaRgba8::decoded_lenbefore allocating); over budget the filename resolves to nothing, cached, with one warning (5dce486b6, #5658 — vanilla totals 980 MiB Oblivion all-resolution-sets, so it never trips on shipped content). hkx strings are borrowed,read_cstrcapped at 256 B, and the absolute spline sample capMAX_TRANSFORM_SAMPLESis 1 M — 8x the vanilla census max of 124,821 (#5316, d8295d77c; the old 16 M admitted a 597 MiB decode) — and the spline frame count is bounded relative to the file's blocks (#4648/#4655) withmax_frames_per_blockcapped atMAX_FRAMES_PER_BLOCK256 (fixed in a65595815, #5006 — the one-block bypass;skyrim_se_spline_dimensions_census_stays_under_the_gate_ceilingsis the real-data guard) — a new per-entry owned copy of a shared string is the regression. MenuXml include budget + cycle checks run before the archive fetch and probes are memoised per resolved path (fixed in 2465740cc, #5007:self_including_fragment_costs_one_fetch,budget_exhausted_includes_never_fetch; known-open #5329 — that cycle check tests all three candidate spellings, so a lower-priority spelling on the stack rejects a distinct higher-priority include).MaterialIndex::buildparses the schema withParseLimits::unlimited()and streams theDBFileIndexcollections element-by-element — its bound is the per-instance decode, notparse_with_limits. A degraded join on a non-vanilla CDB now errors instead of yielding an empty/wrong index (#5320, 6489ae225:MissingDbFileIndex,RowInstanceMismatch,DuplicateDbFileIndex,UnsupportedDbFileIndexChunk); the skip path walksClass::read_orderlike the read path (#5323, 258ba2b90);cdb_material_indexwarns before memoizing a failed archive fetch (#5319, 8be8c3d3a). - MenuXml's own DDS decoder (
tex.rs) rejects dimensions > 8192 and sizes the uncompressed payload againstwidth × height × bppbefore allocating the RGBA canvas; a zero channel mask is an absent channel and sub-8-bit masks replicate by shift-OR (the old2*bits - 8underflowed — dev panic, release mis-decode), L8 keys offDDPF_LUMINANCE(fixed in 015e10549, #5377:truncated_payload_is_rejected_before_allocation,l8_grey_decodes_as_luminance_not_red). 16-bpp A4R4G4B4 / R5G6B5 decode through the same masks and the loader names "present but undecodable" separately from "not found" (fixed in 5c53ad9c7, #5400; the opt-inmenu_texture_header_census_decodespins zero undecodable vanilla menu headers). &strbyte-range slicing of disk-derived names panics mid-char (#3391); useas_bytes().
Dimension 2: Truncation, EOF and error semantics
Paths: Dimension 1's, plus byroredux/src/asset_provider/{archive,animation,material/provider}.rs, byroredux/src/npc_spawn/resumable/ (consumer Err arms).
First step: list non-test unwrap() / expect( / [idx] on file-derived indices in Paths; read each consumer's Err arm.
- Short decode is a deliberate
Ok+log::warn!(shipped padding deltas #622/#812); over-run isErr(InvalidData). Guards:short_decode_stays_ok_for_the_shipped_padding_deltas,decompress_chunk_zlib_short_stream_returns_actual_length,decompress_chunk_lz4_under_run_returns_actual_length_not_declared. A short non-final DX10 chunk is anErrnaming the entry (#4662,extract_dx10_rejects_a_short_non_final_chunk_naming_the_entry); only the final chunk stays lenient. A DX10chunk_hdr_len != 24is a hardInvalidDatain every build, and nodebug_assert!sits on a file-controlled field (fixed in cf2a36920, #5008:dx10_open_rejects_a_non_24_chunk_hdr_len_as_invalid_data,dx10_open_tolerates_non_monotonic_start_mip_without_panicking). An empty compressed BSA record (vanilla zero-byte.txtplaceholders: 35 Oblivion, 1 FO3, 1 FNV) is decided inBsaArchive::extractbefore any decoder — size header 0 with no stream →Ok(empty)on the zlib and LZ4-frame arms, a non-zero declared size with no stream →InvalidDatanaming the entry — because flate2 1.1.10 reports an empty zlib stream asUnexpectedEofwhere 1.1.9 returnedOk(0)(fixed in fa2af0e89, #5073/#5660;extract_returns_empty_for_a_zero_size_compressed_placeholder,extract_rejects_a_nonzero_size_with_no_compressed_stream;safety.rspins the decoder semantics the readers rely on). "Short decode staysOk" holds for complete streams only; a stream cut mid-block is an error. - A checksum-only zlib failure retries as raw DEFLATE; a corrupt body must still error (
corrupt_adler32_trailer_recovers_via_raw_deflate,corrupt_deflate_body_still_errors). - Errors carry context (bgsm
Error::UnexpectedEof { offset }, hkx labels,checked_*label). Consumer fall-through loops go throughextract_first/Archive::extract_or_warn(asset_provider/archive.rs, #4658): a non-NotFounderror warns once per (archive, entry) instead of reading as "missing" and silently falling back to the vanilla copy — a raw.extract(..).ok()loop is the regression (known-open #5009:byroredux/src/debug_load.rs'sresolve_nif_byteskeeps one, BSA-only and first-wins; owner/audit-tooling).Archive::open_shared(asset_provider/archive.rs, 5402b2530 #5288) is the process-wide opened-handle registry (validated by file length + mtime, capped at 1024) that the texture / material pools and the CDB index build now open through — a replaced archive must be re-read, not served stale (open_shared_reuses_an_unchanged_archive_and_rereads_a_replaced_one). - A bad asset costs the asset, not the cell, and a partial result must be signalled: hkx skips one bad annotation and keeps the clip (
read_annotations_skips_an_out_of_range_time_and_keeps_the_rest); menuxml truncates an over-deep subtree with a warning. Flag any silent partial result. - File-byte-reachable panics: NIF parse runs under
catch_unwindinbyroredux/src/streaming/pre_parse.rs; archive extract, hkx, bgsm, facegen and menuxml callers may run unguarded on the main thread.crates/facegen/src/egm.rsindexesbytes[offset]after its exact-size check — verify that precondition is unconditional.
Dimension 3: Version and generation gating
Paths: crates/bsa/src/archive/open.rs, crates/bsa/src/ba2.rs, crates/bgsm/src/{base,bgsm,bgem}.rs, crates/sfmaterial/src/reader.rs, crates/hkx/src/packfile.rs, crates/facegen/src/.
First step: grep -n 'version' <Paths>; every gate keys off the file's own version field, never a GameKind sniff.
- BSA: allowlist {103,104,105}, unknown =
Err; folder record 16 vs 24 B andu64offsets on v105; embed-name flag 0x100 only for >= 104; codec zlib v103/v104, LZ4 frame v105 — neverlz4_flex::block(synthetic_v105_block_codec_payload_is_rejected_by_frame_reader); size-word bit 31 has no sourced meaning and is not acted on (#3367). - BA2: exhaustive
matchover {1,2,3,7,8} (unknown_version_rejected); v2 header +8 B, v3 +12 B carryingcompression_method(0 zlib, 3 LZ4 block, elseErr:v3_unknown_compression_method_rejected); v1/v2/v7/v8 always zlib; per-chunkpacked_size == 0= stored raw regardless of codec; DX10 DDS header 148 B (build_dds_header_is_148_bytes) with cubemap, mip clamp and per-DXGI pitch/linear-size flag. - BGSM/BGEM: per-field
version >=/<gates; a version aboveNEWEST_KNOWN_VERSION(22) still decodes with the newest layout but surfacesversion_over_ceiling+unconsumed_bytes(#4664); strings decode lossily (#4672).material/provider.rslogs those diagnostics on the BGEM path, but the BGSM template resolver (crates/bgsm/src/template.rs) parses with the non-diagnosticparse_bgsm, so the BGSM warnings fire only in the depth-limit recovery arm (known-open #5010). - CDB: header errors (
BadMagic,UnsupportedVersion,BigEndianUnsupported); vocabularies pinned bychunk_type_recognized_set_is_pinned,builtin_type_recognized_set_is_pinned,class_flags_known_mask_is_pinned;probe_headertolerates unknown chunk FourCCs (#4273), the object-tree parse must reject them. User-class inline bytes are read in field-offset order (Class::read_order; XMCOLOR declares r,g,b,a at offsets 2,1,0,3 — fixed in 224a19372,read_order_reorders_xmcolor_shape); a declaration-order read is the regression. - HKX: the spline-clip dimension gate ties
mask_sizeto the file's own track counts aspad(4*transform_tracks + float_tracks, 4)(Havok pads the mask table to 4 B; the unpadded equality rejected 229 SE / 167 LE vanilla clips, #5659,decode_spline_animation_rejects_a_mask_size_off_the_padded_value).Packfile::parsegates magic, pointer width 4|8, little-endian,fileVersion == 8, reuse-padding 0,hk_2010*(#4332,rejects_packfiles_that_are_not_havok_2010_msvc_layout); offsets derive from pointer size (layout_walk_reproduces_the_skyrim_se_offsets); a track binding a bone the skeleton lacks is dropped with a warning inconvert_hkx_clip(#3013), never zip-truncated. - FaceGen: magics
FREGM002/FREGT003/FRTRI003; EGM demands exact file size (rejects_size_count_mismatch), so trailing bytes on a modded head are refused — intended? EGM deltas arescale × i16, not half-floats (#4653); EGT is planar signed chars (#4668); EGT header offset 8 is R = rows =height, 12 is C =width(fixed in 8f7acf3a7, #5014:non_square_header_maps_rows_to_height_and_columns_to_width).
Dimension 4: Corpus and baseline gates
Paths: crates/*/tests/*real*.rs, crates/bgsm/tests/parse_all.rs, crates/sfmaterial/tests/, crates/menuxml/tests/, crates/hkx/src/animation.rs (test module), .github/workflows/real-data-gates.yml.
First step: grep -rn '#\[ignore' crates/{bsa,bgsm,sfmaterial,hkx,facegen,menuxml} vs grep -n 'cargo test' .github/workflows/*.yml; diff the two.
- Strict lane (#3850):
BYROREDUX_REQUIRE_GAME_DATA=1makes an absent corpus panic and a setBYROREDUX_<GAME>_DATAoverride binding. Implemented incrates/bsa/tests/{bsa_real,ba2_real,csg_real}.rs,crates/bgsm/tests/parse_all.rs,crates/sfmaterial/tests/real_cdb.rs,crates/facegen/tests/parse_real_facegen.rs. - Vacuous-green sites (#4660 closed the known ones; verify no regression): the menuxml corpora (
crates/menuxml/tests/{vanilla,fnv,fo3}_corpus.rs) are#[ignore]d and resolve throughtests/common(binding override, strict on a missing install/archive, panic on an open failure); hkx'sanimations_archive, bgsm'sopen_archive_at, facegen'sGame::extract, the bsarequire_archivehelpers and the in-cratearchive_missinggate all fail underREQUIREinstead of skipping, and treat a present-but-unopenable archive as a failure. All six parser crates'--ignoredsuites are scheduled nightly inreal-data-gates.yml'sparsersjob (#4659) — but it targets abyroredux-game-dataself-hosted runner and none is registered (runner count 0, 2026-10-10), so every run is cancelled unstarted and no nightly result exists; run the suites locally; the Skyrim LE hkx parity test is--skipped there with a::warning::unlessBYROREDUX_SKYRIMLE_DATAis set. - Assertion strength:
real_cdb.rspins exact counts (97 classes / 1,438,780 values); bgsm holdsMIN_SUCCESS_RATE1.0 over FO4 and FO76Materials.ba2(FO76's two JSON exports are a named, counted bucket). #4665 added FO76 BA2/DX10, FO3 BSA, FNV MenuXml and Oblivion EGM sweeps. Tabulate (format x game) sweeps and list what is still missing. The #5326 hardening landed (d4352d477): the HKX census pins the SE clip count (6,126), bounds skips, and runs every clip throughdecode_spline_animationagainst a reject baseline (the first run measured 288 vanilla rejects — a real decoder bug, then fixed: #5659mask_sizeis Havok's 4-byte-padded size, eca0238e7, and #5662 a non-finite prop-bone track is dropped intoHkxAnimation::dropped_bonesrather than failing the clip, 9707cb367; the census now rejects 0 of 6,126), andmaterial_index_dlc_cdb_resolves_the_base_corpusasserts the five Nightmare texture slots under the strict lane. Known-open gate weaknesses (dedup, do not re-file): the Oblivion EGM test resolves its data dir by hand and skips green underREQUIRE, and the BGSM sweeps countOkonly (#5011); three of the four HKX dimension-gate fixtures leavemask_size0 and so cannot fail on their own clause (#5446). - ROADMAP compat-matrix rates are NIF-level; any BGSM/BA2/CDB rate claim in
ROADMAP.mdordocs/feature-matrix.mdneeds a dated measurement + sweep command. CI has no game data, so each format branch needs an in-memory fixture (build_v105_archive,PackfileBuilder,synth_egm,bgsm::tests::minimal_v2_bytes).
Dimension 5: Decode <-> consumer wiring
Paths: crates/bgsm/src/, byroredux/src/asset_provider/material/{merge,cdb}.rs, crates/facegen/src/lib.rs, byroredux/src/npc_spawn/resumable/runtime.rs, crates/hkx/src/animation.rs, byroredux/src/asset_provider/animation.rs, crates/bsa/src/uvd.rs.
First step: list pub fields of BgsmFile / BgemFile / HkxAnimation / EgmFile; grep each in its consumer.
- BGSM/BGEM fields vs
merge_external_material(byroredux/src/asset_provider/tests/bgsm_merge.rs): classify unconsumed fields renderer-relevant (finding) vs editor-only. The merge takes&mut ImportedMaterial; a widened signature is a NIFAL violation (/audit-nifal). - CDB (#3398 still OPEN for the rest; spike
docs/audits/SF_CDB_PHASE2_SPIKE_2026-08-29.md):MaterialIndex(224a19372) joins path → material → layer → texture set →MRTextureFileslots (SLOT_COLOR..SLOT_TRANSMISSIVE) plus per-slotTextureReplacement, alpha test,IsGlass,UseSSS, and (9bb6b7dce, #5277/#5283)EffectSettingsComponent.BlendingModeandEmissiveSettingsComponent(Enabled + the inline nestedEmittanceSettingstint / luminous emittance).apply_cdb_material(material/merge.rs, 978d25c19) forwards colour / normal / emissive slots, theSLOT_COLORreplacement only when no colour texture landed (never a tint), alpha test only whenHasOpacity = trueis authored on theParentchain (an absent field is the measured class defaultfalse, 0d698d16c, #5661 — issue still open),IsGlass→bgem_glass,"AlphaBlend"→has_alpha, (enabled emissive settings are captured but parked, 4667b884b #5503 — noemissive_color/emissive_mult); it stampsPbrMaterial::NO_SIGNAL_NEUTRALscalars on a hit (#5197). Parked by design: opacity / metal / rough / AO / transmissive slots, slot 6_height(a layer height-blend input, not parallax — 46dfdcc6c, #5381),MaterialParamFloat, non-colour replacements,UseSSS.ObjectInfo.Parentinheritance of the settings components is resolved (5c14ebdf8, #5502; the decode is pinned in the default lane since 7ceda787d, #5578); known-open: texture-set slots and flat colours are not Parent-inherited (2,051 of 10,158 referenced materials resolve with no texture) — the remaining half of #5502; additive-family blend modes stay unforwarded but are parked by name innifal.md(dbfae5a34; #5509 open).MergeOutcome::PresenceOnlynow means an index miss (apply_cdb_pbr_fallback,mat_path_lookup_miss_keeps_presence_only_and_no_textures). A loose.matJSON file that decodes wins over the CDB (material/loose_mat.rs, #4277, c2f28e06c); the decoder reads the installed layout —Objects[].Components[]with properties underData, string-typed bools and scalars,Data\stripped fromFileName,Indexviau8::try_from, all four colour channels required (fixed in faf09c6e6, #5396; vanilla ships none, four Creation archives carry 20). An undecodable file falls through to the CDB lookup instead of pre-empting it (#5436); both arms recordImportedTextureSource::Mat. Known-open #5525:parse_loose_matis a secondCdbMaterialproducer that reads noParentinheritance and noBlendingMode. Do not file the parked fields as new; a parked field leaking into a live slot is the finding. - FaceGen
.egt/.trihave no consumer — documented incrates/facegen/src/lib.rs(#3544, closed by doc correction); only EGM reachesapply_morphs. Re-file only if that claim changes. - UVD: envelope only, no consumer yet (
parse_uvd_headerhas no production caller;cell_loader/precombined.rsowns the future previs consumer; #3810, the envelope spike, is closed); payload undecoded by design. - Consumed-but-unparsed: a consumer
unwrap_or(default)standing in for an unexposed field. ESM/NIF cases (TXST DecalData #3638, SkinAttach) go to/audit-esm//audit-nif.
Dimension 6: I/O and path robustness
Paths: crates/bsa/src/{naming.rs,read_at.rs,archive/mod.rs,ba2.rs,csg.rs}, byroredux/src/asset_provider/{archive,texture,material/provider}.rs, byroredux/src/asset_provider/tests/, crates/game-detect/src/{vdf,steam}.rs, crates/menuxml/src/parse.rs.
First step: cargo test -p byroredux --bin byroredux asset_provider; read archive_precedence.rs and archive_siblings.rs.
- Key: both readers use
to_lowercase().replace('/', "\\")(Unicode fold); consumer normalisersnormalize_mesh_path,normalize_texture_path,normalize_material_path,strip_build_prefix,canonical_texture_key— check open-time and lookup keys agree and non-ASCII names do not diverge from ASCII-only folds elsewhere. - Siblings:
numeric_sibling_paths(pure; shared with the launcher validator):Foo.bsa-> 2..9,Foo0.bsa-> 1..9,Foo01.ba2-> 02..09, mid-series none; guardssiblings_*inarchive_siblings.rs;open_with_numeric_siblingsde-dups by case-fold. - Provider order is last-listed wins across a chain (#3637);
every_content_provider_resolves_collisions_last_winsscans providers for a first-wins loop;count_shadowed_entrieslogs shadowing. Within one archive a duplicate normalised name is counted at open and warned, last record wins (#4671, BSA and BA2). - File model: no mmap, no
Mutex<File>— positional reads viaread_at.rs::ReadAt(pread/seek_read), so extracts from one archive run concurrently on Unix (Windows serialises in the kernel, #4999); CSG's mutex guards only its chunk cache. A shared-cursor read reintroduced anywhere is the regression (concurrent_extracts_read_their_own_entries,synthetic_v105_concurrent_extracts_read_their_own_bytes,concurrent_reads_get_their_own_chunk_bytes, #5000). Bounds checks must fail on a replaced archive, not return garbage (extract_rejects_compressed_payload_too_short). - VDF/ACF: malformed input rejected, not half-read (
malformed_documents_are_rejected_rather_than_half_read);libraryfolders.vdffromsteamapps/+config/is unioned; ACFinstalldiris contained understeamapps/common/ininstalls_in_libraryvia the host-independent string predicateis_bare_install_dir(#4673; widened in aad5dfd75, #5016 —/,\,:,.,..rejected so Windows-rooted\X/D:Xcannot escape;only_a_bare_directory_name_is_a_valid_installdir); unreadable manifests are logged.
Phase 3: Merge and Cleanup
Write docs/audits/AUDIT_PARSERS_<TODAY>.md: Executive Summary (findings by severity; crates swept; real-data suites run vs skipped), deduplicated Findings, and a Gate Matrix (crate x {size caps, error policy, version gate, corpus sweep, CI lane}). Cross-audit dedup: NIF/ESM sizes /audit-nif Dim 1 / /audit-esm Dim 1; unsafe /audit-safety; MenuXml rendering /audit-ui. Then rm -rf /tmp/audit/parsers and suggest /audit-publish docs/audits/AUDIT_PARSERS_<TODAY>.md (labels: import-pipeline BSA/BA2/CSG/FaceGen, nifal BGSM/CDB, animation HKX, ui MenuXml, test-gap gates, tech-debt game-detect; game:* when title-specific).