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
- Yes. 134,165 of 134,166 entries (99.999%) classify from their leading bytes. The single holdout is
.vfont, which is deliberately obfuscated. - 133,657 entries are Source 2 resources, all sharing one header shape with
headerVersion == 12and a table of four-character block tags. - Block tags are the useful part: they say what a resource contains, and they map almost perfectly onto extensions —
PHYSappears only on models,SrMaonly onvcss_c. - The header's leading
u32is the declared resource size, but forvtex_c,vsnd_candvcsthe archived entry is larger: bulk data is appended past it. For textures that appended data is 99.9% of the bytes. - Reading all 134,166 headers takes ~12 seconds and is stored by
vpkdb.py sniff.
Findings
Source 2 resource header
The first 16 bytes, little-endian:
| offset | type | field |
|---|---|---|
| 0 | u32 | declared resource size |
| 4 | u16 | header version — 12 for every resource in the game |
| 6 | u16 | resource version — varies by type |
| 8 | u32 | block table offset, relative to offset 8 |
| 12 | u32 | block 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
| kind | entries | notes |
|---|---|---|
source2 | 133,657 | compiled resources |
empty | 408 | zero-length, all but two are shaders/**/*.ini |
text | 82 | .txt, .lua, .res, .css, .kv3, … |
ttf | 12 | fonts |
hdf5 | 2 | .sofa — spatial audio, an HDF5 container |
png | 2 | |
vccd | 2 | compiled closed captions, magic VCCD |
unknown | 1 | the obfuscated .vfont |
Block tags, and which extensions carry them
| block | files | carried by |
|---|---|---|
DATA | 133,657 | every resource |
RED2 | 133,647 | every resource but 10 |
CTRL | 84,423 | vsnd_c, vmdl_c only |
RERL | 35,668 | external references — many types |
MDAT | 5,043 | models only |
INSG | 4,453 | vmat_c only |
PHYS | 3,697 | models only |
MBUF | 2,540 | models only |
MVTX / MIDX | 2,503 each | models only |
SrMa | 446 | vcss_c only |
LaCo | 443 | vxml_c only |
TBUF | 313 | models only |
ANIM / ASEQ / AGRP | 241 each | models only |
SNAP | 109 | vsnap_c only |
FLCI | 99 | vdata_c only |
DSTF | 96 | models only |
MRPH | 77 | models only |
REDI | 10 | vtex_c only — the older editor-info block |
STAT | 5 | vts_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:
| ext | files | declared | appended | appended share |
|---|---|---|---|---|
vtex_c | 13,408 | 29.0 MB | 25,940.0 MB | 99.9% |
vsnd_c | 79,362 | 189.9 MB | 2,894.4 MB | 93.8% |
vcs | 754 | 2.7 MB | 59.9 MB | 95.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
- 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_cfiles as unknown. Validate arithmetically — the block table must fit inside the entry — not with a magic ceiling. - Read enough bytes to reach the end of the block table. A 192-byte window truncates it for 131 files.
vpkdb.py sniffre-reads those with a computed window. - The leading
u32is not the archived size forvtex_c,vsnd_candvcs. Using it as a file length will truncate 26 GB of texture data. - BOM-prefixed text fails a printable-ASCII check. Two
.txtfiles use UTF-8 and UTF-16LE BOMs; detect the BOM first. .vfontis deliberately obfuscated and has no usable leading magic.- Classification is by content, not extension — that is the point. Do not "fix" a disagreement by trusting the extension.
Open questions
- The meaning of
DSTF,INSG,LaCo,FLCI,TBUFandSTATis unverified.SrMa(source map),MVTX/MIDX(vertex/index buffers) andMRPH(morph) are plausible expansions only. - Whether the 10
vtex_cfiles still carryingREDIrather thanRED2are simply old assets was not investigated. - The appended-region hypothesis was not confirmed by decoding a texture's mip chain.
- Block offsets and lengths are parsed but not stored; only the tags are. Storing the sizes would show how bytes are distributed inside a resource.
- Nothing here validates that a resource's declared version matches what the engine expects — only that it is constant per extension in this build.
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.