KBA archive creator/extractor
  • C++ 94.9%
  • CMake 5.1%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-08-28 02:48:58 +00:00
docs SCR-00014: Implement KBXv2 2026-08-27 19:46:09 -07:00
src SCR-00014: Implement KBXv2 2026-08-27 19:46:09 -07:00
.gitignore Initial commit 2026-08-23 04:07:47 +00:00
CMakeLists.txt SCR-00014: Implement KBXv2 2026-08-27 19:46:09 -07:00
LICENSE Initial commit 2026-08-23 04:07:47 +00:00
README.md SCR-00014: Implement KBXv2 2026-08-27 19:46:09 -07:00

kbx

Create, inspect and extract karasu .kba asset archives.

A .kba is the archive a karasu game ships its content in: a fixed header, an index, and two data sections — one holding files verbatim, one holding fixed-size compressed blocks — mounted at startup through a PhysicsFS archiver the engine registers for it.

kbx is the only program that writes one. The build calls it to pack each game's assets.kba, and the engine's archiver (engine/src/assets/kba_archiver.cpp) is the only other implementation of the format — and it only reads. Everything else kbx does is for looking inside a shipped archive, pulling one asset out, or checking a build's output.

The format is specified in docs/kba-format.md. Read it before changing anything here: the writer, the engine's reader and the engine's mirror of the layout header all have to agree byte for byte, and that document is what they agree on.

Building

cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release
cmake --build build
./build/kbx --help

Needs a C++26 compiler (GCC 16 or Clang with #embed and std::print), zlib (CRC-32, and inflate for reading legacy v1 archives), and these from the karasu tree it lives in:

  • argparse — reused from 3rdparty/argparse when kbx is built as part of karasu, found at that sibling path when built standalone from a full checkout, or pointed at with -DKBX_ARGPARSE_DIR=<path>.
  • the obfuscation key — engine/src/assets/kba_obfuscation_key.bin, #embed-ed at compile time, or -DKBX_KBA_KEY_FILE=<path>. Both are checked at configure time and fail with an actionable message rather than producing a binary that cannot open a real archive.
  • lz4 and zstd — from 3rdparty/lz4 and 3rdparty/zstd, or the system packages, or neither: a build without one still reads and writes every archive that does not use it, and says which codec is missing when handed one that does. Only lz4's lib/ is compiled, and zstd is taken under its BSD-3-Clause option; both trees carry a GPL half that this build does not touch.

Inside the karasu build tree, cmake --build build --target kbx produces build/tools/kbx/kbx.

Commands

kbx a       create an archive
kbx x       extract an archive
kbx ls      list an archive
kbx cat     write one entry to stdout
kbx prev    extract file(s) to a temp folder and open them in the desktop
kbx sweep   remove leftover preview folders
kbx info    describe an archive's format and composition
kbx verify  check every entry against its stored CRC-32

kbx --help <command> prints help for one command and kbx --help-all prints it for every command at once.

Any archive argument may name a path inside the archive with a colon: assets.kba:engine/fonts/liberation. The whole argument is tried as a filename first, so a file whose own name contains a colon still works.

kbx a — create

kbx a assets                      # pack ./assets into assets.kba, uncompressed
kbx a assets -o game.kba          # name the archive
kbx a assets -c lz4               # compress the chunk section with LZ4
kbx a assets -c zstd -l 9         # ... or with Zstandard, at level 9
kbx a assets -c zstd --raw-ext png,ogg   # ... but keep already-compressed files verbatim
kbx a --add build/fonts=engine/fonts/liberation -o assets.kba
kbx a assets --plain              # no obfuscation wrapper (still mountable)

A positional folder contributes its contents at the archive root. --add SRC=DEST is repeatable; a SRC directory is packed under DEST, a SRC file becomes the entry DEST.

Nothing is compressed unless asked. A game reads assets through PhysicsFS while it runs, and a file in the raw section costs nothing to open — the archiver hands back a pointer into the mounted archive. A compressed file costs one block decode per chunk it spans. --compress none, the default, produces an archive with no compressed section at all.

Flag Effect
-o, --out FILE archive to write (default: <first path>.kba)
--add SRC=DEST pack SRC at archive path DEST, repeatable
-c, --compress CODEC none (default), lz4 or zstd
-l, --level N codec effort: 1-12 lz4 (HC mode; omit for fast lz4), 1-19 zstd
--raw-ext EXT keep this extension uncompressed even with a codec set, repeatable and comma-separated
--chunk-size BYTES uncompressed bytes per block, K/M suffix allowed (default 256K)
--plain write without the obfuscation wrapper
-v, --verbose list every entry as it is packed

kbx x — extract

kbx x assets.kba                        # into ./assets/
kbx x assets.kba -o out                 # into ./out/
kbx x assets.kba '*.ttf'                # only the fonts, wherever they live
kbx x assets.kba 'engine/**' -o out     # only the engine namespace
kbx x assets.kba:engine/fonts -o fonts  # that subtree, rooted at fonts/
kbx x assets.kba --dry-run              # say what would be written

Patterns are globs: ? one character, * any run within one path segment, ** any run across segments. A pattern with no / is matched against the file name alone; a pattern naming a directory selects everything under it.

Entry names are checked before anything is written — absolute paths, .. components, drive letters and backslashes are refused, naming the entry. An archive is untrusted input.

kbx ls — list

kbx ls assets.kba                          # one level
kbx ls -a assets.kba                       # the whole archive as a tree
kbx ls assets.kba:engine/fonts/liberation  # one level, somewhere inside
kbx ls -l assets.kba                       # mode, method, sizes, ratio, date

-b/--bytes prints exact byte counts, --ascii draws the tree without box-drawing characters.

Directories in the output were reconstructed from entry names: a .kba stores file entries only.

kbx prev — look at something

kbx prev assets.kba:icons/save.png      # open it in whatever views PNGs here
kbx prev assets.kba 'icons/*.svg'       # extract them all, open the folder
kbx prev assets.kba:game.json -n        # extract and print the path, open nothing
kbx prev assets.kba:tex.ktx2 --dir ./x  # extract somewhere you chose

Extracts into an OS temp directory ($TMPDIR, GetTempPath, or the sandbox's own private temp) and hands the result to the desktop's registered handler. On Linux that is xdg-open, then gio open, then the desktop-specific openers — the first two being the ones the XDG Desktop Portal hooks, so a preview still works inside a Flatpak or Snap where kbx cannot see the user's applications at all. macOS uses open; Windows uses the shell's own association lookup. $KBX_OPENER overrides the lot.

One match opens the file; several open the folder they landed in, because thirty viewer windows is not a preview. --each opens them individually, up to a small cap.

Cleanup happens on a later run, not this one. The handler opens the file after kbx has already exited, so deleting on the way out would race it. The directory is named kbx-preview-<pid> instead, and any subsequent kbx invocation — whatever it was asked to do — removes preview folders whose owning process is gone. A folder whose creator is still running is never touched, so PID reuse errs toward keeping a folder rather than deleting one in use.

One consequence is worth knowing, because it is not hypothetical: kbx prev exits the moment it has handed the path over, so its own PID is already gone while the viewer is still starting up. A kbx command run in that window collects the folder and the viewer finds nothing there. Set $KBX_PREVIEW_TTL to a number of seconds to additionally require a folder to have gone untouched that long before it is collected; it is 0 by default.

Executable types are refused. An archive is untrusted input and the desktop runs a .desktop file, a script or an .exe rather than displaying it, so previewing one is not a way to execute it — --force if you mean it. Extracted files are written 0600 into an owner-only directory, never executable.

kbx sweep — collect leftovers on demand

kbx sweep            # remove folders whose owning process has exited
kbx sweep -n         # show what that would remove, and remove nothing
kbx sweep --force    # also take folders whose owner is still running

Every kbx run already does the default sweep, so this is for seeing what is there before anything goes, reclaiming space without running some unrelated command for its side effect, and clearing a folder whose owner somehow outlived its usefulness. It is the one command that does not auto-sweep first — a dry run that had already deleted what it then offers to delete would be worthless.

kbx cat, kbx info, kbx verify

kbx cat assets.kba:game.json | jq .
kbx info assets.kba
kbx verify assets.kba

info answers the container-level questions — which format generation it is, whether it is obfuscated, how the data splits between the raw and chunk sections, which codecs the chunks actually used, and how many chunks the worst-placed file makes a reader decode. verify decodes every entry and checks it against its stored CRC-32, which is the check the engine never makes on its own: PhysicsFS reads a member when something asks for it, so damage in a corner nothing has loaded yet surfaces as a bug report rather than as a failed mount.

Exit codes

Code Meaning
0 success
1 the command ran and failed
2 the command line was wrong

Source layout

One .h/.cpp pair per responsibility; main.cpp is a thin driver.

src/main.cpp          entry point, nothing else
src/kbx/cli.*         parser assembly and dispatch
src/kbx/cmd_*.cpp     one file per command: its flags and its behaviour
src/kbx/archive.*     format detection, and one entry view over all of them
src/kbx/kba_format.h  the v2 on-disk layout — the authoritative C++ statement
src/kbx/kba_read.*    v2 index parsing, chunk lookup, file extraction
src/kbx/kba_write.*   v2 building: placement, chunking, index
src/kbx/codec.*       one block in, one block out, per codec
src/kbx/obfuscation.* the key, the XOR pass, and the legacy v1 wrapper
src/kbx/zip_read.*    reading legacy v1 archives and plain ZIPs
src/kbx/deflate.*     the only zlib calls in the tool
src/kbx/tree.*        the folder structure a flat entry list implies
src/kbx/spec.*        archive:path parsing, path hygiene, zip-slip checks
src/kbx/pattern.*     glob matching
src/kbx/preview_store.* where previews go, and collecting dead ones
src/kbx/open_external.* handing a path to the desktop, per platform
src/kbx/file_io.*     whole-file reads, atomic writes, directory walks
src/kbx/format.*      sizes, modes, methods, timestamps for display

Errors are values, not exceptions: everything that can fail returns std::expected with a message naming what failed, and a command's job is to print it and pick an exit code.

License

MIT. See LICENSE.