全言語の実装が同一に振る舞うことを機械的に検証する仕組み。 本ライブラリの存在意義そのものなので、実装より先にこの枠組みを作る。
spec/testdata/
├─ nbt/ 入力バイナリ (.nbt)
├─ concat/ NBTが複数連なったバイナリ (.nbt)
├─ anvil/ ベクタごとのディレクトリに .mca / .mcc
├─ world/ チャンク1つ分のNBT (.nbt)
└─ expect/ 正規化JSONの期待値(ベクタID + .json)
spec/testdata/manifest.jsonは、全ベクタの一覧と、各ベクタが検証する内容を持つ。
{
"vectors": [
{
"id": "nbt/all_tags",
"kind": "nbt",
"input": "nbt/all_tags.nbt",
"format": "java",
"compression": "none",
"description": "全13タグを含む。TAG_EndはCompoundの終端として現れる",
"expect": "expect/nbt/all_tags.json",
"roundtrip": true
}
]
}読み込みが失敗することを確かめるベクタは、expectの代わりにexpect_error(期待するErrorCode)を持つ。
roundtrip: falseのベクタは、「読めるが、書き戻すとバイトが変わる」ことが仕様上正しいもの(第三者ツールが書いた非正準なデータなど)を表す。
読み込みが失敗するベクタと、連なったNBTのベクタもroundtrip: falseになる。
入力バイナリを読み、00 共通規約 6章の正規化JSONへ変換し、expect/のファイルと文字列として完全一致することを確認する。
全言語で一致させるため、JSONの出力規則を次のように厳密に定める。
- キーの順序は本仕様に書かれた順(
type→element_type→value→mutf8) - 区切りは
,と:(空白なし) - 非ASCII文字は
\uXXXXへエスケープする - 末尾に改行を1つ付ける
期待値ファイルを持つのは、読み込みに成功するNBTのベクタだけである。 Anvil・World・連なったNBTのベクタは、2.3のクロス言語一致で確かめる。
read(bytes) -> write() -> bytes'でbytes == bytes'を確認する。
- 圧縮ありのベクタは、展開後のバイト列で比較する(圧縮結果はzlib実装のバージョンで変わるため)
roundtrip: falseのベクタはこの検証をスキップする
spec/run-conformance.shが全言語それぞれのCLI検証ツールを起動し、同じ入力に対する出力(正規化JSONと再書き出しバイト列)を相互にdiffする。
各言語は次のインターフェースを持つ検証ツールを提供する。
<runner> decode <入力パス> <出力JSONパス> [--format network]
<runner> encode <入力パス> <出力バイナリパス> [--format network]
<runner> snbt <入力パス> <出力SNBTパス> [--format network]
<runner> nbt-list <入力パス> <出力テキストパス> [--format network]
<runner> region-list <入力.mcaパス> <出力テキストパス>
<runner> region-rewrite <入力.mcaパス> <出力.mcaパス>
<runner> chunk-report <入力チャンクnbtパス> <出力テキストパス>
<runner> chunk-edit <入力チャンクnbtパス> <出力nbtパス>
<runner> version
nbt-listは連なったNBT(10 3.1)をread_bytes_allとread_bytes_atの両方で読み、個数と各NBTの開始・終了位置を書き出す。
chunk-reportは、チャンクの全4096ブロック(と4×4×4のバイオーム)を1つずつ読み出して種類ごとに数え上げる。パレットとビット詰めの取り出しを端から端まで通すので、どこか1か所でも境界の扱いを誤れば集計値が変わる。
chunk-editは決まった手順(パレット拡張・ビット幅の再計算・compact()・高さマップの無効化)でチャンクを編集し、無圧縮NBTとして書き出す。全言語でバイト単位に一致しなければならない。
| 言語 | 起動方法 |
|---|---|
| C# | dotnet csharp/tests/SpringNBTLibrary.Conformance/bin/Release/net8.0/springnbt-conformance.dll |
| Java | java -cp java/target/classes:java/target/test-classes io.github.scriptarts.springnbt.conformance.Conformance |
| TypeScript | node typescript/dist/src/conformance.js |
| Python | python -m spring_nbt_library.conformance |
| Rust | rust/target/release/examples/conformance |
spec/tools/run_conformance.pyが各言語のビルドと起動を引き受ける。実装が無い言語は自動的に検証対象から外れるので、言語を1つずつ追加していく途中でも走らせられる。
合成ベクタだけでは、実際のMinecraftが書き出すデータを網羅しきれない。
spec/tools/scan_world.pyが手元のワールドを丸ごと読んで、次を確認する。
python3 spec/tools/scan_world.py "<ワールドのパス>" --verbosescan_world.pyは次の処理を行う。
- すべての
.dat/.nbtを読み、ラウンドトリップ(読む→書く→バイト一致)を検証 - すべての
.mcaのヘッダを解析し、全チャンクを展開してNBTとして読み、同じく検証 - セクタの重複・不正オフセット・ファイル長の非整列を検出
- パレット長から求めたビット幅と、実際の
data長が一致するかを検証(31) - ルート直下キー・セクションキー・
Statusの出現数を集計し、仕様書とのズレを見つける - Worldレイヤで全チャンクを解釈し直し、書き戻したバイト列が原本と一致するかを検証。さらに一部のチャンクは全ブロック・全バイオームを1つずつ読み出す(
--block-sampleで件数を変えられる)
このツールは一切書き込まない。 ただし、Minecraftを終了させてから実行すること。
ワールドのデータ自体は、個人のセーブデータであるためリポジトリに取り込まない。 検証したい人が自分のワールドを指定して走らせる形にしてある。
| 日付 | 対象 | 結果 |
|---|---|---|
| 2026-08-29 | Java版26.2の実ワールド(DataVersion 4903) | .dat 23個 + チャンク3,717個 = 3,740件すべてで読み込みとバイト一致のラウンドトリップに成功。失敗0 |
| 2026-08-29 | 同ワールドから抽出した代表14ファイル × 5言語 | 正規化JSONとSNBTが全言語で完全一致。書き戻したバイト列も70件すべて原本と一致 |
| 2026-08-29 | 同ワールドのリージョンファイル12個 × 5言語 | チャンク一覧と詰め直したバイト列が全言語で完全一致(3,717チャンク)。開いて無変更で書き戻すと、バイト単位で原本と一致 |
| 2026-08-29 | 同ワールドをWorldレイヤ(MinecraftWorld / Chunk / PalettedContainer)で解釈 |
3,481チャンク・83,544セクションを解釈し、NBTへ書き戻したバイト列が全件原本と一致。うち40チャンクは全ブロックを1つずつ読み出し(393万ブロック、79種類)、バイオームも含めて成功。失敗0 |
パレット付きコンテナ(31)については、83,544セクション = 167,088個のコンテナすべてで、パレット長から求めたビット幅と実際のdata長が一致することを確認した。
この走査で、40 ワールドのディレクトリ構成が26.xで大きく変わっていること(dimensions/への集約、players/への改名、level.datからのデータ分離)が分かり、仕様書を全面的に書き直した。
| ID | 検証内容 |
|---|---|
nbt/hello_world |
最小のCompound。ルート名が空でない |
nbt/all_tags |
全13タグ(TAG_EndはCompoundの終端として現れる) |
nbt/nested_deep |
ネスト深さ500(上限512の直下) |
nbt/nested_too_deep |
ネスト深さ600 → LIMIT_EXCEEDED |
nbt/empty_list |
空リスト(要素型End) |
nbt/empty_list_typed |
空リストだが要素型がByte(第三者ツール由来) |
nbt/numeric_bounds |
各整数型の最小値・最大値 |
nbt/float_specials |
+0.0 -0.0 Infinity -Infinity NaN |
nbt/mutf8_nul |
U+0000を含む文字列 |
nbt/mutf8_supplementary |
補助文字(絵文字)を含む文字列 |
nbt/mutf8_lone_surrogate |
孤立サロゲート |
nbt/mutf8_max_length |
65535バイトちょうどの文字列 |
nbt/gzip / nbt/zlib / nbt/uncompressed |
3種の圧縮を自動判定して読む |
nbt/network_format |
無名ルート(1.20.2+) |
nbt/truncated |
途中で切れた入力 → MALFORMED_DATA |
nbt/huge_declared_length |
長さ0x7FFFFFFFの宣言 → MALFORMED_DATA |
nbt/unknown_tag_id |
タグID 13 → MALFORMED_DATA |
nbt/negative_length |
長さフィールドが負値 → MALFORMED_DATA |
nbt/trailing_bytes |
ルートの後に余分なバイトがある → MALFORMED_DATA |
nbt/mutf8_lone_surrogate_key |
Compoundのキーが孤立サロゲート → MALFORMED_DATA |
nbt/mutf8_raw_nul |
MUTF-8に反する素の0x00を含む文字列 → MALFORMED_DATA |
| ID | 検証内容 |
|---|---|
concat/three_tags |
同じNBTが3つ連なっている |
concat/mixed |
大きさの違うNBTが3つ連なっている |
concat/single |
NBTが1つだけ |
| ID | 検証内容 |
|---|---|
anvil/empty |
全エントリ0のリージョン |
anvil/single_chunk |
チャンク1つ |
anvil/fragmented |
隙間のある配置。読み書き後も他チャンクのデータがおかしくならない |
anvil/external_mcc |
.mccへ退避されたチャンク |
anvil/mixed_compression |
圧縮ID 1 / 2 / 3が混在 |
anvil/lz4 |
圧縮ID 4(LZ4)。1ブロック / 2ブロック連結 / 無圧縮ブロック / 重なりのあるマッチ |
anvil/lz4_bad_magic |
LZ4Blockのマジックがおかしい → MALFORMED_DATA |
anvil/bad_offset |
ヘッダ領域を指すオフセット → MALFORMED_DATA |
anvil/overlapping_sectors |
2チャンクが同じセクタを指す → MALFORMED_DATA |
anvil/unaligned_length |
ファイル長が4096の倍数でない → MALFORMED_DATA |
anvil/offset_out_of_file |
オフセットがファイル末尾より後ろを指す → MALFORMED_DATA |
| ID | 検証内容 |
|---|---|
world/palette_1 |
パレット1要素(dataなし)のセクション |
world/palette_5 |
ビット幅4、端数なし |
world/palette_17 |
ビット幅5、最後のlongに端数 |
world/multi_section |
セクション3個。バイオームも2種(ビット幅1) |
world/palette_unused |
参照されていないパレット要素が2つある。compact()で減る |
world/block_entities |
block_entities / block_ticks / fluid_ticksを持つチャンク |
world/proto_chunk |
生成途中のチャンク(Statusがminecraft:fullでない) |
world/palette_index_out_of_range |
添字がパレットの範囲外を指す → MALFORMED_DATA |
world/bitstorage_wrong_length |
dataの長さがパレット長から求めたビット幅と合わない → MALFORMED_DATA |
ライブラリのバグがそのまま期待値にならないよう、次の順で作る。
spec/tools/build_testdata.pyが、入力ベクタ(NBT・連なったNBT・Anvil・World)とmanifest.jsonをすべて生成する。このスクリプトはライブラリを使わず、仕様書の記述だけを根拠にした独立の最小NBTライタでバイト列を組み立てる。大きなベクタ(nested_deepなど)もここで作るnbt/hello_worldの期待値(expect/nbt/hello_world.json)だけは手で書くspec/tools/run_conformance.py --generate-expectで、基準実装(C#)の出力からNBTベクタのexpect/を生成する。手書きのhello_worldは上書きせず、基準実装の出力と一致するかを確かめる- 生成した期待値と全言語の出力を比べ、一致することを確認する(2章)
つまり、ライブラリを使わずに作った入力ベクタと手書きの期待値が信頼の起点であり、基準実装の出力は期待値を増やすためにだけ使う。