Deadlock Research
verifiednote 0006build 66792026-08-15headersmagicsource2metadata

Identifying file formats from headers

Question

Can the format of an archived file be identified from its leading bytes, without decompiling it — and is that worth storing as searchable metadata?

Summary

Findings

Source 2 resource header

The first 16 bytes, little-endian:

offsettypefield
0u32declared resource size
4u16header version — 12 for every resource in the game
6u16resource version — varies by type
8u32block table offset, relative to offset 8
12u32block count

Each block table entry is 12 bytes: a 4-character ASCII tag, u32 offset, u32 length.

Resource version is constant per extension, so it is a property of the type rather than of individual files: vcs 70, vrr_c 17, vjs_c 4, vcss_c/vxml_c 3, vmix_c/ vsvg_c/vts_c 2, vmat_c/vmdl_c 1, vdata_c/vnmclip_c 0.

What is in the archives

kindentriesnotes
source2133,657compiled resources
empty408zero-length, all but two are shaders/**/*.ini
text82.txt, .lua, .res, .css, .kv3, …
ttf12fonts
hdf52.sofa — spatial audio, an HDF5 container
png2
vccd2compiled closed captions, magic VCCD
unknown1the obfuscated .vfont

Block tags, and which extensions carry them

blockfilescarried by
DATA133,657every resource
RED2133,647every resource but 10
CTRL84,423vsnd_c, vmdl_c only
RERL35,668external references — many types
MDAT5,043models only
INSG4,453vmat_c only
PHYS3,697models only
MBUF2,540models only
MVTX / MIDX2,503 eachmodels only
SrMa446vcss_c only
LaCo443vxml_c only
TBUF313models only
ANIM / ASEQ / AGRP241 eachmodels only
SNAP109vsnap_c only
FLCI99vdata_c only
DSTF96models only
MRPH77models only
REDI10vtex_c only — the older editor-info block
STAT5vts_c only

195 distinct block layouts exist. The four most common cover 91% of files: RED2,DATA,CTRL (79,362), RERL,RED2,DATA (26,110), RED2,DATA (16,383), RERL,RED2,DATA,INSG (4,276).

Observed, not inferred: the table above reports which extensions carry each tag. The meaning of most tags is not established here — see Open Questions.

Appended bulk data

For most resources the declared size equals the archived entry size. Three types append data past it:

extfilesdeclaredappendedappended share
vtex_c13,40829.0 MB25,940.0 MB99.9%
vsnd_c79,362189.9 MB2,894.4 MB93.8%
vcs7542.7 MB59.9 MB95.7%

Overall 93,524 of 133,657 resources carry appended data and 40,133 do not.

Observed deltas for textures include 5,592,400, 2,796,200 and 1,398,096 bytes — close to the 4/3 ratio of a full mip chain over its base level. Inferred: the appended region is mip/sample/shader-variant payload that the engine streams separately from the resource header. Not confirmed by parsing the payload itself.

Reproduce

python tools/vpkdb.py build
python tools/vpkdb.py sniff          # ~12s for 134,166 entries

python tools/vpkdb.py sql "SELECT kind, count(*) n FROM header GROUP BY kind ORDER BY n DESC"
python tools/vpkdb.py sql "
  SELECT f.ext, h.res_version, count(*) n FROM header h JOIN file f ON f.id=h.file_id
  WHERE h.kind='source2' GROUP BY f.ext, h.res_version ORDER BY f.ext"
python tools/vpkdb.py sql "
  SELECT f.ext, count(*) n, sum(h.payload) appended, sum(h.hdr_size) declared
  FROM header h JOIN file f ON f.id=h.file_id WHERE h.payload > 0 GROUP BY f.ext"

Block-tag frequency and the extensions carrying each tag:

python -c "
import sqlite3, collections
c = sqlite3.connect('file:db/deadlock.db?mode=ro', uri=True)
m = collections.defaultdict(collections.Counter)
for b, e in c.execute('SELECT h.blocks, f.ext FROM header h JOIN file f ON f.id=h.file_id WHERE h.blocks IS NOT NULL'):
    for t in set(b.split(',')): m[t][e] += 1
for t in sorted(m, key=lambda t: -sum(m[t].values())):
    print(f'{t:<7}{sum(m[t].values()):>8,}  ' + ', '.join(f'{e}:{n}' for e, n in m[t].most_common(3)))"

Browsable at /explorer/: the format, resource version and block chips appear in the detail pane, and the block dropdown filters the whole listing.

Gotchas

  1. Block count is not small. Complex models carry up to 193 blocks (one set per mesh/LOD). An early cap of 32 silently misclassified 61 vmdl_c files as unknown. Validate arithmetically — the block table must fit inside the entry — not with a magic ceiling.
  2. Read enough bytes to reach the end of the block table. A 192-byte window truncates it for 131 files. vpkdb.py sniff re-reads those with a computed window.
  3. The leading u32 is not the archived size for vtex_c, vsnd_c and vcs. Using it as a file length will truncate 26 GB of texture data.
  4. BOM-prefixed text fails a printable-ASCII check. Two .txt files use UTF-8 and UTF-16LE BOMs; detect the BOM first.
  5. .vfont is deliberately obfuscated and has no usable leading magic.
  6. Classification is by content, not extension — that is the point. Do not "fix" a disagreement by trusting the extension.

Open questions

Sources

Derived entirely from the local install at build 6679. Block tag names are read from the files themselves; no external format documentation was consulted, which is why most semantics are left open above.