How to add support for a new archive/installer/image format, end to end. Written from the patterns the codebase already uses — follow them and detection + extraction "just work".
- Detectors run over the file (in
cli/file_type.py), cheapest first:puremagic(MIME) → built-in signatures (data/signatures.json) → DIE → binwalk → Magika. Each emits MIME types and/or detection-name strings. - Filtering (
cli/detection_filter.py) drops generic noise viadata/detection_blacklist.json(e.g.application/octet-stream,pe). - Routing (
cli/formats/__init__.py): a MIME or detection string is looked up (lowercased) indata/handlers.json(mime_handlers/detection_handlers) to find the handler class. First detector with a mapped handler wins (early-exit;--bruteforcetries all). - The handler shells out to a bundled tool and extracts.
Key point: handlers.json and signatures.json are generated, not
hand-edited. They are built from each handler's detection_* class methods by
tools/generate_data.py. You declare detection in the handler, then regenerate.
Create cli/formats/format_<name>.py, subclassing BaseExtractor:
import os, subprocess, logging
from .base_extractor import BaseExtractor
class FormatFooHandler(BaseExtractor):
"""Handler for FOO archives (uses foo/foo.exe)."""
@classmethod
def detection_mimes(cls):
return ['application/x-foo'] # MIME types (puremagic / Magika)
@classmethod
def detection_names(cls):
return ['foo archive'] # DIE / binwalk / Magika strings (lowercased)
@classmethod
def detection_signatures(cls):
# Custom magic bytes for what the engines miss. name is auto-added to
# detection_names. patterns = list of {pos, hex} = AND; repeat the entry
# with the same name for OR.
return [{'name': 'foo archive', 'patterns': [{'pos': 0, 'hex': '464f4f00'}]}] # 'FOO\0'
def extract(self):
cmd = [os.path.join(self.extractors_path, 'foo', 'foo.exe'),
'x', self.target_file, self.extract_directory]
try:
self.run_command(cmd) # raises on non-zero; logs tool stderr at ERROR
return True
except subprocess.CalledProcessError:
return False
# optional: def list_contents(self): return "<listing>" or Noneself.target_file,self.extract_directory,self.extractors_path(=cli/bin/extractors/),self.cli_args.passwordare provided by the base.- Any of the three
detection_*methods may be omitted (base returns[]).
A file reaches your handler when ANY detector's output matches something your handler declares. You have three levers — use whichever the format actually gives you (often more than one, for redundancy):
| Method | Declares | Fill it when… | Source of truth |
|---|---|---|---|
detection_mimes |
MIME strings | puremagic or Magika already recognizes the format and emits a real MIME (e.g. application/x-rar). Cheapest path — no magic needed. |
run identify and read the [puremagic] MIME / [Magika] MIME lines |
detection_names |
detector name strings | DIE / binwalk / Magika name the format but there's no clean MIME (installers, SFX, packers, exotic archivers) | DIE signature DB + identify --debug |
detection_signatures |
your own magic bytes | nothing above catches it, but the file has a reliable fixed-offset magic (most niche/older formats). This is the fallback that makes detection self-contained. | TrID defs + the format spec |
So: mime = "an engine already knows it"; name = "an engine names it but no MIME"; signature = "I teach it the magic myself." All three are lowercased and compared case-insensitively.
DIE's real output string lives in its signature DB as meta("<type>", "<name>").
Grep for your format:
grep -rl -i "yourformat" cli/bin/detectors/die/db/
grep -rhoE 'meta\("[^"]*", ?"[^"]*"\)' cli/bin/detectors/die/db/**/*yourformat*.sgUse the lowercased <name> in detection_names.
- Gotcha:
file_type.determine_file_type_with_dieonly keeps values whosetype∈{sfx, archive, installer, packer}. If DIE tags your format as anything else (Library, Format, Partition, tool…) its name is dropped — so either add a magic signature, or widenrelevant_typesinfile_type.py.
TrID has ~21k defs, one XML per format under triddefs_xml/defs/<letter>/, with
<Pattern><Bytes>HEX</Bytes><Pos>N</Pos></Pattern> (<Pos> = decimal offset,
<Bytes> = uppercase hex). It's the most reliable magic source — it caught a
wrong EnCase Lx01 magic here. Search by extension/name and read the pattern:
ls triddefs_xml/defs/*/ | grep -i yourext
grep -oE '<Bytes>[0-9A-Fa-f]+</Bytes>|<Pos>[0-9]+</Pos>|<Ext>[^<]+</Ext>' \
triddefs_xml/defs/y/yourformat.trid.xmlConvert bytes to lowercase for the handler. Reject weak patterns (1–2 bytes,
all-zero, or a generic value like 04000000 alone — anchor it with a second
{pos,hex}).
Short keys / labels — the surest way is to run the CLI on a real sample and read what each detector prints:
python cli/main.py identify --debug path/to/sample.fooCopy the exact [binwalk] detect / [Magika] detect|MIME strings into
detection_names / detection_mimes.
- Prefer a distinctive multi-byte constant at a fixed offset.
- A generic lead (e.g.
04000000) needs a second anchor: use AND (two{pos,hex}in onepatternsgroup), like asar =04000000@0 and{"files"@16. Avoid short/common magics (a 2-byte or all-zero pattern will false-positive). - Multiple alternative magics for one format = repeat the entry with the same
name(they become OR groups).
- Add the class to
cli/formats/__init__.py: import it and append to_HANDLER_CLASSES. - Regenerate the data files:
This rewrites
python tools/generate_data.pycli/data/handlers.json+cli/data/signatures.jsonfrom all handlers. Commit those alongside the handler.
- Put the tool under
cli/bin/extractors/<tool>/(matches the path yourextract()builds). It ships beside the exe in the release zip. - Windows runtime gotcha: old tools may need a VC++ runtime via a
side-by-side manifest (error
WinError 14001). Ship the runtime app-local: aMicrosoft.VCxx.CRT/folder (or loose files +.manifest) beside the tool, matching the exact assembly identity the exe requests. See thealzip/layout. - Add the tool + its license to
THIRD-PARTY-NOTICES.mdand the Tools table informats.md.
If the format is handled by a 7-Zip plugin (tc4shell etc.) rather than a
standalone tool, don't write a new handler — add the detection to
Format7zExtHandler (cli/formats/format_7z_ext.py), which inherits 7-Zip's
extract(). Drop the plugin DLL in cli/bin/extractors/7z/Formats/ (or
Codecs/). Native 7-Zip formats go in Format7zHandler instead.
Wrapped-exe installers can't be told apart by content. Add the class name to
PE_INSTALLER_FALLBACK in cli/main.py — it's tried on any MZ file; the tool
self-validates and fails cleanly on a mismatch. (7-Zip is deliberately excluded.)
- Drop a real sample at
test/samples/<fmt>/test-file.<ext>— the battery walks every file undertest/samples/automatically, no registration needed. - Run it:
python test/run_battery.py # unit tests + extraction battery (Windows) python test/run_battery.py --unit-only # pure-python, runs anywhere - Add pure-logic unit tests under
test/cli/(seetest_formats_registry.py).
- Add a row to the right table in
formats.md(format, extension(s), handler); mark plugin-dependent formats with†. - Update
THIRD-PARTY-NOTICES.mdif you bundled a new tool.
-
cli/formats/format_<name>.pywithextract()+detection_* - Detection strings verified against DIE db / TrID (not guessed)
- Registered in
cli/formats/__init__.py -
python tools/generate_data.pyrun;handlers.json/signatures.jsoncommitted - Tool bundled under
cli/bin/extractors/(+ runtime if needed) - Sample in
test/samples/<fmt>/; battery green -
formats.md+THIRD-PARTY-NOTICES.mdupdated