- C++ 94.9%
- CMake 5.1%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
|
|
||
| docs | ||
| src | ||
| .gitignore | ||
| CMakeLists.txt | ||
| LICENSE | ||
| README.md | ||
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/argparsewhen 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/lz4and3rdparty/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'slib/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.