An asset-package compiler implemented in Odin, per SPEC.md. It reads an
asset source map (MJSON) and produces a single encrypted, integrity-checked,
platform-independent .pak file that games load directly.
mpack [options] <source_map>
Requires Odin. Only the console + core: libraries
are used (no third-party code).
make # or: odin build .
make vet # odin vet .
make test # builds, then runs test/run_tests.sh (all green)
make cleanmpack --root project --out build/assets --signing-key <hex64> assets/packages/units.mjson
mpack --gen-key ~/keys/pak # writes ~/keys/pak (private) and ~/keys/pak.pub
mpack --inspect build/assets/x.pak --public-key $(cat ~/keys/pak.pub)| Flag | Meaning |
|---|---|
--root <dir> |
Project root. Map paths and entry patterns are project-root-relative. Defaults to the current directory. |
--out <dir> |
Directory for <FNV32a(map path)>.pak. Created if needed. |
--signing-key <hex64> |
Ed25519 private seed signing the ToC. |
--gen-key <path> |
Generate a fresh Ed25519 keypair: seed at <path>, public key at <path>.pub. |
--inspect <file.pak> |
Verify a package instead of packing. |
--public-key <hex64> |
Enable the --inspect Ed25519 signature check. |
--no-overwrite |
Refuse to replace an existing <hash>.pak. |
--verbose |
Report per-package details on stderr. |
Exit codes: 0 success, 1 packaging/verification/I-O failure, 2 bad CLI
arguments.
A source map is MJSON (Bitsquid SJSON) at a project-root-relative path such as
assets/packages/units.mjson:
{
assets = [
{
pattern = "data/units/unit1.bin",
alias = "data/units/unit1.alias.bin",
},
{ pattern = "data/units/*.atlas", no_compress = true, no_encrypt = true },
{ pattern = "data/ui/main.raw", no_encrypt = true },
],
}
pattern(required) - a project-root-relative file path. If it contains glob metacharacters (*,?,[) it is expanded (ascending) into one entry per match; a glob that matches nothing is an error.alias(optional) - an extra logical key that resolves to the same payload bytes and inherits the entry'sno_compress/no_encryptflags. Not allowed on glob patterns (there is no single payload to share).no_compress- store the payload raw; otherwise zlib (level 9), kept only when strictly smaller.no_encrypt- store the payload in the plaintext tail (its 16-byte MAC tag is zeroed; the runtime detects plaintext slots by the all-zero tag).
Every entry's key (pattern, expanded match, or alias) is hashed with FNV32a
and must be unique within the map; any collision is reported. Across maps,
<root>/.mpack-registry records every hash -> map path pair. A map whose
hash is already claimed by a different path aborts before any .pak is
written.
Mirrors SPEC.md; the authoritative layout is the spec.
- Header (56 bytes) -
"MPAK", version (1), 16-byte ToC key seed, 12-byte ToC nonce, 16-byte ToC Poly1305 tag, u32 LE ToC size. - ToC (encrypted) - plaintext: u32 LE entry count, u32 hashes in ascending
order (for runtime binary search), 72-byte descriptors
(
offset, uncompressed, compressedu32 LE,seed, nonce, sha256), a 64-byte Ed25519 signature over everything that precedes it, then zero padding soheader + toc_size ≡ 0 (mod 64). The ToC is sealed with ChaCha20-Poly1305, authenticating the header (with its tag field zeroed) as AAD. - Payloads - one slot per entry: data-bytes (compressed or raw) + 16-byte MAC tag, padded to a 64-byte boundary. Encrypted entries come first (in declaration order), then the plaintext tail (declaration order). Descriptor sizes describe only the data. All integers are little-endian.
--inspect re-derives each per-entry key from its own seed, decrypts,
decompresses, and checks each SHA-256, then checks the ToC tag (and optionally
the Ed25519 signature).
SPEC.md leaves these decisions open; the game runtime must match.
- Key derivation. The spec's derived key material is 16 bytes, but
ChaCha20-Poly1305 (RFC 8439) requires a 256-bit key.
derive_keyproduces the specified 16 bytes and expands them to 32 withSHA-256(derived). Rotating every key in a project means changingKDF_TABLE_SEED(crypto.odin) in both this tool and the runtime. - Cascading tables. The 16 x 256 substitution tables are generated
deterministically from
KDF_TABLE_SEED(splitmix64 + Fisher-Yates) at first use. Odd seed bytes are nibble-swapped then two's-complement negated; even bytes pass through all sixteen tables. - Per-slot tag. Each slot stores a 16-byte Poly1305 tag. Plaintext
(
no_encrypt) slots carry an all-zero tag, which is how the runtime distinguishes a plaintext slot from an encrypted one. Encrypted-slot data is authenticated with empty AAD. - ToC AAD. The 56-byte header with the tag field zeroed is the ToC AEAD AAD; the computed tag is then written into the header.
- Dev signing key. Without
--signing-key, packages are signed with the public dev seed0001...1f. Such packages must not ship; pack unconditionally prints a warning.
config.odin constants, Asset, Options
util.odin FNV32a hashing, hex, u32 LE access, errorf
crypto.odin key derivation, tables, AEAD + Ed25519 wrappers, entropy
map_pack.odin MJSON parsing, glob/alias expansion, collision checks
pack.odin layout, ToC build/sign/encrypt, slot writing, output
registry.odin <root>/.mpack-registry load/check/commit
inspect.odin verifier
main.odin CLI, gen-key, exit codes, pack arena
test/ fixture project + end-to-end suite (run_tests.sh)
All allocations in a pack cycle come from a single virtual arena threaded as
allocator; arena memory is never freed individually.