Repository navigation
v0.1.0
The first public shape of the library. It is a work in progress: the classic
.cbh family is read and exported, .cbv/.cbz archives unarchive in full, and
2CBH yields tags but no moves. README.md § "What is not supported yet" is the
authoritative list of what is missing.
Added
- The consumer façade (
cbvault::bridge).Database::opentakes a base name or
any member file, resolves siblings case-insensitively, validates the mandatory
set (.cbh .cbg .cba .cbp .cbt .cbc .cbs) and reports the generation, the record
count and which optional members were found.Database::headerslists games
from.cbhand the namebases alone and never opens the moves file; asserted
two ways, by renaming.cbgaway after open and for a tag scan. - A sink-based conversion contract.
GameSinkdeclares once per run what it
needs (wants_keys,wants_annotations) and is then handed one entirely borrowed
GameRefper game in ascending game-number order.for_each_gamewalks
sequentially;convert_parallelcuts the id space into 8,192-record chunks and
has one writer stage deliver them in id order, so the sink sees the same
games, in the same order, with the same payloads, at one thread or ten — asserted
over 4,096 fixture games at 1, 2, 3 and 10 threads and batch sizes 1, 7 and
8,192. A sink that only counts games reaches the allocator zero times per
game, asserted with a per-thread counting allocator. - Tag search over the raw set.
Entities::find_playerand friends resolve a
name to an id once;scan/scan_rangethen compare that id per record and
return matches in ascending game order. ChessBase's tournament namebase is not a
valid BST, so a descent that misses falls back to a verified scan of the 10 MB
.cbt;Found::viasays which path answered. - Position search, unindexed.
for_each_position_keyreplays the source and
reports where a Polyglot key occurs, in parallel, withPositionQuery::every
for progress andPositionQuery::cancel_withfor a cancel that takes effect
within one chunk. Measured at 19.9 M plies/s on one thread, 109.6 M/s on ten. - The
cbvaultCLI with four commands:info,verify,pgnand
archive list/archive extract, all with stable--json. The PGN report
always goes to stderr so stdout stays pure PGN. - PGN export, sequential and Rayon-parallel, byte-identical either way.
export_parallel,export_range,export_range_fromandexport_span.
407,350 of 419,385 games match a ChessBase export byte for byte (97.1 %);
§10 ofdocs/format-spec.mdlists the five deliberate deviations. .cbvarchive container and codec, complete. Magic, the 173-byte member
table, the self-describing member count derived from the geometry, per-record
validation against the table's own redundant 32/64-bit copies, and the block
framing. All four block modes are decoded — stored, LZ, Huffman, and
Huffman-then-LZ — so extraction yields 3,871 of 3,871 members and 100 % of
the reference archive's 3.61 GB, including the 512 MB.cbhand the 1.25 GB
.cbj. Listing touches no part of the data pool. Extracting the archive takes
7.1 s on four threads (504 MB/s decoded), against 70.1–72.6 s foruncbvon
the same machine.- Parity with
uncbvmeasured, not asserted. Every member of every corpus —
twic1134.cbv, the 3,871-member reference archive and the three.cbzsamples
— is byte-identical to the reference process's own output, in both
directions (including files the reference wrote that the table does not name).
Reached under a documented two-room clean room; see
docs/research/03-clean-room-audit.md. .cbz, fully. DES in ECB with three key rules depending on the
password's length — as-is at eight bytes, repeat below, fold above —
each verified against the reference's own sample for that rule. The password
check is one eight-byte read, so opening a protected archive never deciphers it.- A 2CBH reader (
cbvault_format::twocbh): fixed 192-byte records, headers,
annotations, and record framing verified over 220,418 records with zero
violations.GameMoves::is_decoded()isfalseby design — the move codec is
not decoded, so nomoves2is fabricated from it. - DES (FIPS 46-3) implemented and checked against the standard's published
test vectors. - The test-only fixture builder (
cbvault-fixtures,publish = false), so
the format round-trips are exercised in CI with no real database.
Changed
- The chess core is
gigachessand onlygigachess. The ported ancestor's
chesscorelayer was replaced wholesale: move generation, legality, FEN/SAN/UCI,
Chess960 and incremental Polyglot Zobrist all come fromgigachess, and the
internal move currency is its 16-bitmoves2. A CI job fails the build if a
second chess implementation appears in the tree. - Moved to
gigachess0.1.9 and let the engine own the null move, so a CBH
pass (0xffffin themoves2stream) is handled byMove::NULLrather than by
cbvault code. - The walk dispatches on what the sink asked for, not per game: no keys means
the fastplay_fastmake, keys means the hash-maintainingplay_hashed,
annotations off means.cbais never opened. Asking for keys costs 4–8 % of a
pass, which is roughly 12× cheaper than the second pass it replaces. - The PGN export was optimised from 171.9 s (64,899 records/s) to
128.1–130.0 s (87,029 records/s) single-threaded, and to 19.7–20.6 s at ten
threads, for byte-identical output — the SAN body inlined into its node, no
per-gameStringorVec, one buffer and onewrite_allper game, tag heads
as string literals, and an ASCII fast path in the name buffer. - The project is named
cbvault, including every crate, path, binary, env var
and document. Format names keep theircbhspelling (cbvault_format::cbh).
Fixed
- The
.cbvkey derivation for a password that is not eight bytes was wrong,
and wrong silently: a short password was zero-padded and a long one truncated,
where the format repeats and folds. Two of the reference's three.cbzsamples
were unopenable as a result. A wrong DES key is still a valid key, so it
deciphers to noise rather than failing — the existing test had asserted the
wrong behaviour. Fixed, and all three rules are now pinned by tests against the
reference's own samples. - A stale-cache bug in the fast make, where a pass turn was validated against
acheckersvalueplay_fasthad left behind. Fixed ingigachessand
adopted here. - SAN over-disambiguation, which was placing a file or rank hint on every
candidate. The writer now emits the minimal qualifier per the SAN standard,
verified equivalent to the per-candidate test over 13,908,447 moves with zero
differences. Gold parity is unchanged. is_legalon a null move was answered by the wrong question, testing the
opponent's king; it now has a dedicated branch upstream.
Known limitations
These are real and are not fixed; each is described in README.md § "What is not
supported yet".
.cbhreading beats the ancestor it was ported from on every path and every
thread count. Both at the same thread count on 11,151,119 records:verify
39.84 s against 46.79 s single-threaded (1.17x), PGN export 94.43 s against
142.44 s single-threaded (1.51x) and 11.23 s against 33.69 s at ten threads
(3.0x). Both spec budgets are met: parallel export at most 25 s at 8+ threads
and under 8 GiB (13.00 s, 3,107 MB), single-threaded at least 65,000
records/s (94.43 s, 1.8x inside it). Memory is the one place the ancestor
leads, and it is mapped pages rather than allocation:cbtoolhas no mmap
dependency and preads into caller buffers, so it holds 13-14 MB where we hold
1,520 MB of whichcat .cbg > /dev/nullshows 1 MB is accounted as the page
cache rather than resident memory.- Memory clarified against the ancestor's, by measurement rather than
inference. The ancestor has nommapdependency andpreads, so its
resident set is its buffers: 13-14 MB against our 1,520 MB. A control
(cat .cbg > /dev/null, reading all 1.25 GB) measures 1 MB, which shows the
pages are file-backed and evictable rather than ours. The mapping still wins on
time - 39.5 s against 46.3 s single-threaded - so this is a trade-off, not a
defect. DbFile::read_exact, and the allocation it removes.Wide::offsetsread
24 bytes per record throughDbFile::read, which allocates aVec— 11
million times on a whole-database walk, the hottest path in the.cbj
reader. It now reads into a stack buffer.cbvault pgnalso reports
peak_writer, the library's own high-water mark for a worker's writer
buffers, which measures 0 MB after all 11.1 M games.- The 2CBH
.2cbgmove codec is not decoded, and a specific list of 2CBH
header fields remains unknown. A 2CBH database yields tags and annotations but
nomoves2. Database::openrefuses a 2CBH set with a typedMissingFile; reading 2CBH
behind the same façade and the same sink is not done.- A game with variations costs one allocation each, in the decoder's per-game
variation stack. Filterhas no "starts from a set-up position" predicate; the bit is in the
move record and a tag search does not open the moves file.- No fuzzing has been done, no crate is published to crates.io, and the
.cbvcontainer layout is verified on two containers and one 1.74 GB archive.
Notes
- No licensed database content is in this repository. The performance figures
inREADME.mdcome from an 11.1 M-record ChessBase set the maintainer holds a
licence for. Every test that needs it is gated on an environment variable and
skips with a visible "this test did not run; it did not pass" message, so CI is
never silently green for want of data. Database paths are git-ignored. - Read-only, permanently. cbvault never opens a source file for writing, never
creates or deletes anything in a database's directory, and never writes bytes it
did not decode. It is not affiliated with, endorsed by or connected to ChessBase;
ChessBase formats are the input and the implementation is original.
Full Changelog: https://github.com/itshak/cbvault/commits/v0.1.0