compact binary serialization format with built-in compression

feat: initial commit

+1
.gitignore
···
···
+
/target
+923
Cargo.lock
···
···
+
# This file is automatically @generated by Cargo.
+
# It is not intended for manual editing.
+
version = 4
+
+
[[package]]
+
name = "addr2line"
+
version = "0.24.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
+
dependencies = [
+
"gimli",
+
]
+
+
[[package]]
+
name = "adler2"
+
version = "2.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+
[[package]]
+
name = "android-tzdata"
+
version = "0.1.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
+
+
[[package]]
+
name = "android_system_properties"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+
dependencies = [
+
"libc",
+
]
+
+
[[package]]
+
name = "autocfg"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+
+
[[package]]
+
name = "backtrace"
+
version = "0.3.75"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
+
dependencies = [
+
"addr2line",
+
"cfg-if",
+
"libc",
+
"miniz_oxide",
+
"object",
+
"rustc-demangle",
+
"windows-targets",
+
]
+
+
[[package]]
+
name = "backtrace-ext"
+
version = "0.2.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50"
+
dependencies = [
+
"backtrace",
+
]
+
+
[[package]]
+
name = "bumpalo"
+
version = "3.19.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
+
+
[[package]]
+
name = "byteorder"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+
[[package]]
+
name = "cc"
+
version = "1.2.33"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3ee0f8803222ba5a7e2777dd72ca451868909b1ac410621b676adf07280e9b5f"
+
dependencies = [
+
"shlex",
+
]
+
+
[[package]]
+
name = "cesu8"
+
version = "1.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+
[[package]]
+
name = "cfg-if"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
+
+
[[package]]
+
name = "chrono"
+
version = "0.4.41"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
+
dependencies = [
+
"android-tzdata",
+
"iana-time-zone",
+
"js-sys",
+
"num-traits",
+
"wasm-bindgen",
+
"windows-link",
+
]
+
+
[[package]]
+
name = "console"
+
version = "0.15.11"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8"
+
dependencies = [
+
"encode_unicode",
+
"libc",
+
"once_cell",
+
"windows-sys",
+
]
+
+
[[package]]
+
name = "core-foundation-sys"
+
version = "0.8.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+
[[package]]
+
name = "crc32fast"
+
version = "1.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+
dependencies = [
+
"cfg-if",
+
]
+
+
[[package]]
+
name = "encode_unicode"
+
version = "1.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+
[[package]]
+
name = "equivalent"
+
version = "1.0.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+
[[package]]
+
name = "flate2"
+
version = "1.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d"
+
dependencies = [
+
"crc32fast",
+
"miniz_oxide",
+
]
+
+
[[package]]
+
name = "getrandom"
+
version = "0.2.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
+
dependencies = [
+
"cfg-if",
+
"libc",
+
"wasi",
+
]
+
+
[[package]]
+
name = "gimli"
+
version = "0.31.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
+
+
[[package]]
+
name = "hashbrown"
+
version = "0.15.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+
+
[[package]]
+
name = "hermit-abi"
+
version = "0.5.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+
[[package]]
+
name = "iana-time-zone"
+
version = "0.1.63"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
+
dependencies = [
+
"android_system_properties",
+
"core-foundation-sys",
+
"iana-time-zone-haiku",
+
"js-sys",
+
"log",
+
"wasm-bindgen",
+
"windows-core",
+
]
+
+
[[package]]
+
name = "iana-time-zone-haiku"
+
version = "0.1.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+
dependencies = [
+
"cc",
+
]
+
+
[[package]]
+
name = "indexmap"
+
version = "2.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
+
dependencies = [
+
"equivalent",
+
"hashbrown",
+
]
+
+
[[package]]
+
name = "insta"
+
version = "1.43.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "154934ea70c58054b556dd430b99a98c2a7ff5309ac9891597e339b5c28f4371"
+
dependencies = [
+
"console",
+
"once_cell",
+
"similar",
+
]
+
+
[[package]]
+
name = "is-terminal"
+
version = "0.4.16"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
+
dependencies = [
+
"hermit-abi",
+
"libc",
+
"windows-sys",
+
]
+
+
[[package]]
+
name = "is_ci"
+
version = "1.2.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45"
+
+
[[package]]
+
name = "js-sys"
+
version = "0.3.77"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f"
+
dependencies = [
+
"once_cell",
+
"wasm-bindgen",
+
]
+
+
[[package]]
+
name = "libc"
+
version = "0.2.175"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
+
+
[[package]]
+
name = "log"
+
version = "0.4.27"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
+
+
[[package]]
+
name = "lz4"
+
version = "1.28.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a20b523e860d03443e98350ceaac5e71c6ba89aea7d960769ec3ce37f4de5af4"
+
dependencies = [
+
"lz4-sys",
+
]
+
+
[[package]]
+
name = "lz4-sys"
+
version = "1.11.1+lz4-1.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "6bd8c0d6c6ed0cd30b3652886bb8711dc4bb01d637a68105a3d5158039b418e6"
+
dependencies = [
+
"cc",
+
"libc",
+
]
+
+
[[package]]
+
name = "memchr"
+
version = "2.7.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
+
+
[[package]]
+
name = "miette"
+
version = "5.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "59bb584eaeeab6bd0226ccf3509a69d7936d148cf3d036ad350abe35e8c6856e"
+
dependencies = [
+
"backtrace",
+
"backtrace-ext",
+
"is-terminal",
+
"miette-derive",
+
"once_cell",
+
"owo-colors",
+
"supports-color",
+
"supports-hyperlinks",
+
"supports-unicode",
+
"terminal_size",
+
"textwrap",
+
"thiserror 1.0.69",
+
"unicode-width",
+
]
+
+
[[package]]
+
name = "miette-derive"
+
version = "5.10.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "49e7bc1560b95a3c4a25d03de42fe76ca718ab92d1a22a55b9b4cf67b3ae635c"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "miniz_oxide"
+
version = "0.8.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+
dependencies = [
+
"adler2",
+
]
+
+
[[package]]
+
name = "nbt2"
+
version = "0.1.0"
+
dependencies = [
+
"chrono",
+
"flate2",
+
"insta",
+
"lz4",
+
"thiserror 2.0.15",
+
"uuid",
+
]
+
+
[[package]]
+
name = "nbt2_migrate"
+
version = "0.1.0"
+
dependencies = [
+
"insta",
+
"nbt2",
+
"nbtree_core",
+
"thiserror 2.0.15",
+
"uuid",
+
]
+
+
[[package]]
+
name = "nbtree_core"
+
version = "0.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8b997801ac5ce31a845d736bfe838113079c3e56cff1ed498813e793c6c6959c"
+
dependencies = [
+
"byteorder",
+
"cesu8",
+
"indexmap",
+
"miette",
+
"num-traits",
+
"thiserror 2.0.15",
+
"unicode_names2",
+
]
+
+
[[package]]
+
name = "num-traits"
+
version = "0.2.19"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+
dependencies = [
+
"autocfg",
+
]
+
+
[[package]]
+
name = "object"
+
version = "0.36.7"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
+
dependencies = [
+
"memchr",
+
]
+
+
[[package]]
+
name = "once_cell"
+
version = "1.21.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
+
+
[[package]]
+
name = "owo-colors"
+
version = "3.5.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
+
+
[[package]]
+
name = "phf"
+
version = "0.11.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+
dependencies = [
+
"phf_shared",
+
]
+
+
[[package]]
+
name = "phf_codegen"
+
version = "0.11.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
+
dependencies = [
+
"phf_generator",
+
"phf_shared",
+
]
+
+
[[package]]
+
name = "phf_generator"
+
version = "0.11.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+
dependencies = [
+
"phf_shared",
+
"rand",
+
]
+
+
[[package]]
+
name = "phf_shared"
+
version = "0.11.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+
dependencies = [
+
"siphasher",
+
]
+
+
[[package]]
+
name = "ppv-lite86"
+
version = "0.2.21"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+
dependencies = [
+
"zerocopy",
+
]
+
+
[[package]]
+
name = "proc-macro2"
+
version = "1.0.101"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "quote"
+
version = "1.0.40"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
+
dependencies = [
+
"proc-macro2",
+
]
+
+
[[package]]
+
name = "rand"
+
version = "0.8.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
+
dependencies = [
+
"libc",
+
"rand_chacha",
+
"rand_core",
+
]
+
+
[[package]]
+
name = "rand_chacha"
+
version = "0.3.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
+
dependencies = [
+
"ppv-lite86",
+
"rand_core",
+
]
+
+
[[package]]
+
name = "rand_core"
+
version = "0.6.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
dependencies = [
+
"getrandom",
+
]
+
+
[[package]]
+
name = "rustc-demangle"
+
version = "0.1.26"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace"
+
+
[[package]]
+
name = "rustversion"
+
version = "1.0.22"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+
[[package]]
+
name = "shlex"
+
version = "1.3.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+
[[package]]
+
name = "similar"
+
version = "2.7.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa"
+
+
[[package]]
+
name = "siphasher"
+
version = "1.0.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+
[[package]]
+
name = "smawk"
+
version = "0.3.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+
[[package]]
+
name = "supports-color"
+
version = "2.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89"
+
dependencies = [
+
"is-terminal",
+
"is_ci",
+
]
+
+
[[package]]
+
name = "supports-hyperlinks"
+
version = "2.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f84231692eb0d4d41e4cdd0cabfdd2e6cd9e255e65f80c9aa7c98dd502b4233d"
+
dependencies = [
+
"is-terminal",
+
]
+
+
[[package]]
+
name = "supports-unicode"
+
version = "2.1.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f850c19edd184a205e883199a261ed44471c81e39bd95b1357f5febbef00e77a"
+
dependencies = [
+
"is-terminal",
+
]
+
+
[[package]]
+
name = "syn"
+
version = "2.0.106"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "terminal_size"
+
version = "0.1.17"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
+
dependencies = [
+
"libc",
+
"winapi",
+
]
+
+
[[package]]
+
name = "textwrap"
+
version = "0.15.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
+
dependencies = [
+
"smawk",
+
"unicode-linebreak",
+
"unicode-width",
+
]
+
+
[[package]]
+
name = "thiserror"
+
version = "1.0.69"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+
dependencies = [
+
"thiserror-impl 1.0.69",
+
]
+
+
[[package]]
+
name = "thiserror"
+
version = "2.0.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "80d76d3f064b981389ecb4b6b7f45a0bf9fdac1d5b9204c7bd6714fecc302850"
+
dependencies = [
+
"thiserror-impl 2.0.15",
+
]
+
+
[[package]]
+
name = "thiserror-impl"
+
version = "1.0.69"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "thiserror-impl"
+
version = "2.0.15"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "44d29feb33e986b6ea906bd9c3559a856983f92371b3eaa5e83782a351623de0"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "unicode-ident"
+
version = "1.0.18"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
+
+
[[package]]
+
name = "unicode-linebreak"
+
version = "0.1.5"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+
[[package]]
+
name = "unicode-width"
+
version = "0.1.14"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
+
+
[[package]]
+
name = "unicode_names2"
+
version = "2.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "d189085656ca1203291e965444e7f6a2723fbdd1dd9f34f8482e79bafd8338a0"
+
dependencies = [
+
"phf",
+
"unicode_names2_generator",
+
]
+
+
[[package]]
+
name = "unicode_names2_generator"
+
version = "2.0.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1262662dc96937c71115228ce2e1d30f41db71a7a45d3459e98783ef94052214"
+
dependencies = [
+
"phf_codegen",
+
"rand",
+
]
+
+
[[package]]
+
name = "uuid"
+
version = "1.18.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
+
dependencies = [
+
"js-sys",
+
"wasm-bindgen",
+
]
+
+
[[package]]
+
name = "wasi"
+
version = "0.11.1+wasi-snapshot-preview1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+
[[package]]
+
name = "wasm-bindgen"
+
version = "0.2.100"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5"
+
dependencies = [
+
"cfg-if",
+
"once_cell",
+
"rustversion",
+
"wasm-bindgen-macro",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-backend"
+
version = "0.2.100"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
+
dependencies = [
+
"bumpalo",
+
"log",
+
"proc-macro2",
+
"quote",
+
"syn",
+
"wasm-bindgen-shared",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-macro"
+
version = "0.2.100"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407"
+
dependencies = [
+
"quote",
+
"wasm-bindgen-macro-support",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-macro-support"
+
version = "0.2.100"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
"wasm-bindgen-backend",
+
"wasm-bindgen-shared",
+
]
+
+
[[package]]
+
name = "wasm-bindgen-shared"
+
version = "0.2.100"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d"
+
dependencies = [
+
"unicode-ident",
+
]
+
+
[[package]]
+
name = "winapi"
+
version = "0.3.9"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+
dependencies = [
+
"winapi-i686-pc-windows-gnu",
+
"winapi-x86_64-pc-windows-gnu",
+
]
+
+
[[package]]
+
name = "winapi-i686-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+
[[package]]
+
name = "winapi-x86_64-pc-windows-gnu"
+
version = "0.4.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+
[[package]]
+
name = "windows-core"
+
version = "0.61.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
+
dependencies = [
+
"windows-implement",
+
"windows-interface",
+
"windows-link",
+
"windows-result",
+
"windows-strings",
+
]
+
+
[[package]]
+
name = "windows-implement"
+
version = "0.60.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "windows-interface"
+
version = "0.59.1"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+
+
[[package]]
+
name = "windows-link"
+
version = "0.1.3"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+
[[package]]
+
name = "windows-result"
+
version = "0.3.4"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+
dependencies = [
+
"windows-link",
+
]
+
+
[[package]]
+
name = "windows-strings"
+
version = "0.4.2"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+
dependencies = [
+
"windows-link",
+
]
+
+
[[package]]
+
name = "windows-sys"
+
version = "0.59.0"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+
dependencies = [
+
"windows-targets",
+
]
+
+
[[package]]
+
name = "windows-targets"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+
dependencies = [
+
"windows_aarch64_gnullvm",
+
"windows_aarch64_msvc",
+
"windows_i686_gnu",
+
"windows_i686_gnullvm",
+
"windows_i686_msvc",
+
"windows_x86_64_gnu",
+
"windows_x86_64_gnullvm",
+
"windows_x86_64_msvc",
+
]
+
+
[[package]]
+
name = "windows_aarch64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+
[[package]]
+
name = "windows_aarch64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+
[[package]]
+
name = "windows_i686_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+
[[package]]
+
name = "windows_i686_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+
[[package]]
+
name = "windows_i686_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+
[[package]]
+
name = "windows_x86_64_gnu"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+
[[package]]
+
name = "windows_x86_64_gnullvm"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+
[[package]]
+
name = "windows_x86_64_msvc"
+
version = "0.52.6"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+
[[package]]
+
name = "zerocopy"
+
version = "0.8.26"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
+
dependencies = [
+
"zerocopy-derive",
+
]
+
+
[[package]]
+
name = "zerocopy-derive"
+
version = "0.8.26"
+
source = "registry+https://github.com/rust-lang/crates.io-index"
+
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
+
dependencies = [
+
"proc-macro2",
+
"quote",
+
"syn",
+
]
+42
Cargo.toml
···
···
+
[package]
+
name = "nbt2"
+
version = "0.1.0"
+
edition = "2024"
+
license = "MIT OR Apache-2.0"
+
rust-version = "1.89"
+
description = "A spiritual successor to the NBT file format created by Mojang."
+
+
[workspace]
+
members = ["crates/*"]
+
resolver = "3"
+
+
[dependencies]
+
chrono.workspace = true
+
flate2 = { workspace = true, optional = true }
+
lz4 = { workspace = true, optional = true }
+
thiserror.workspace = true
+
uuid.workspace = true
+
+
[workspace.dependencies]
+
chrono = "0.4"
+
flate2 = "1.1"
+
insta = "1.43"
+
lz4 = "1.28"
+
nbtree_core = "0.1"
+
thiserror = "2"
+
uuid = "1.18"
+
+
[dev-dependencies]
+
insta.workspace = true
+
+
[features]
+
default = ["gzip", "lz4"]
+
gzip = ["dep:flate2"]
+
zlib = ["gzip"]
+
lz4 = ["dep:lz4"]
+
+
[lints]
+
workspace = true
+
+
[workspace.lints.rust]
+
unsafe_code = "forbid"
+53
Justfile
···
···
+
FEATURES := "--all-features"
+
NO_DEFAULTS := "--no-default-features"
+
TARGETS := "--all-targets"
+
NO_DEPS := "--no-deps"
+
+
alias d := doc
+
alias do := doc-open
+
alias l := lint
+
alias ok := ci
+
alias t := test
+
alias un := udeps
+
+
default:
+
@just -l
+
+
# Tests and lints.
+
ci: lint test
+
+
# Clean build artifacts.
+
clean:
+
cargo clean
+
+
# Generates documentation.
+
doc:
+
cargo doc {{ NO_DEPS }}
+
+
# Generates documentation and opens it.
+
doc-open:
+
cargo doc {{ NO_DEPS }} --open
+
+
# Check formatting and run clippy on all targets with all features.
+
lint: check
+
cargo fmt --all -- --check
+
cargo clippy {{ FEATURES }} -- -D warnings
+
+
# Format and fix clippy on all targets with all features
+
lintmut:
+
cargo fmt --all
+
cargo clippy {{ FEATURES }} --fix
+
+
# Run all tests, or just one module.
+
test:
+
cargo test --doc && \
+
cargo nextest r {{ FEATURES }} --no-tests pass --workspace --no-fail-fast; \
+
+
# Runs `cargo-udeps` on all targets.
+
udeps:
+
cargo +nightly udeps {{ TARGETS }}
+
+
# Runs `cargo check`.
+
check:
+
cargo check {{ NO_DEFAULTS }}
+
cargo check
+422
SPECIFICATION.md
···
···
+
# NBT2 Specification
+
+
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD",
+
"SHOULD NOT", "RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be
+
interpreted as described in [RFC 2119][rfc2119].
+
+
## 0. Table of Contents
+
+
<!-- TOC start (generated with https://github.com/derlin/bitdowntoc) -->
+
+
- [1. Overview](#1-overview)
+
- [1.1 Key Changes](#11-key-changes)
+
- [2. Data Representation](#2-data-representation)
+
- [2.1 Numeric Encoding](#21-numeric-encoding)
+
- [2.2 String Encoding](#22-string-encoding)
+
- [3. Type System](#3-type-system)
+
- [3.1 Type Identifiers](#31-type-identifiers)
+
- [3.2 Array-Compatible Types](#32-array-compatible-types)
+
- [3.3 Map Key Compatible Types](#33-map-key-compatible-types)
+
- [4. Binary Encoding](#4-binary-encoding)
+
- [4.1 Primitive Types](#41-primitive-types)
+
- [4.1.1 Integer Types](#411-integer-types)
+
- [4.1.2 Floating-Point Types](#412-floating-point-types)
+
- [4.1.3 Boolean](#413-boolean)
+
- [4.2 String Type](#42-string-type)
+
- [4.3 Option Type](#43-option-type)
+
- [4.4 List Type (Heterogeneous)](#44-list-type-heterogeneous)
+
- [4.5 Map Type](#45-map-type)
+
- [4.6 Array Type (Homogeneous)](#46-array-type-homogeneous)
+
- [4.7 Timestamp Type](#47-timestamp-type)
+
- [4.8 UUID Type](#48-uuid-type)
+
- [5. File Format](#5-file-format)
+
- [5.1 File Structure](#51-file-structure)
+
- [5.2 Header Format](#52-header-format)
+
- [5.2.1 Magic Bytes](#521-magic-bytes)
+
- [5.2.1.1 File Extension and MIME type](#5211-file-extension-and-mime-type)
+
- [5.2.2 Version](#522-version)
+
- [5.2.3 Flags](#523-flags)
+
- [5.2.4 Compression Method](#524-compression-method)
+
- [5.2.5 Payload Length](#525-payload-length)
+
- [5.3 Payload](#53-payload)
+
- [6. Example](#6-example)
+
- [7. Conformance Requirements](#7-conformance-requirements)
+
+
<!-- TOC end -->
+
+
<!-- TOC --><a name="1-overview"></a>
+
+
## 1. Overview
+
+
NBT2 (Named Binary Tag version 2) is a binary serialization format designed to
+
address limitations of the original NBT format while maintaining simplicity and
+
efficiency.
+
+
<!-- TOC --><a name="11-key-changes"></a>
+
+
### 1.1 Key Changes
+
+
- Explicit `Option` type for nullable values
+
- Support for both heterogeneous lists and homogeneous arrays
+
- Clear distinction between signed and unsigned integer types
+
- Built-in support for timestamps, UUIDs, and boolean values
+
+
<!-- TOC --><a name="2-data-representation"></a>
+
+
## 2. Data Representation
+
+
<!-- TOC --><a name="21-numeric-encoding"></a>
+
+
### 2.1 Numeric Encoding
+
+
- Unsigned integers: Standard binary representation
+
- Signed integers: Two's complement representation
+
- Floating-point: IEEE 754 standard (binary32 for f32, binary64 for f64)
+
- Endianness: Determined by file header flags (see Section 4.2.4)
+
+
<!-- TOC --><a name="22-string-encoding"></a>
+
+
### 2.2 String Encoding
+
+
All strings MUST be encoded as UTF-8. Alternate encoding schemes, such as Java's
+
[Modified UTF-8][mutf8], are explicitly NOT supported.
+
+
<!-- TOC --><a name="3-type-system"></a>
+
+
## 3. Type System
+
+
<!-- TOC --><a name="31-type-identifiers"></a>
+
+
### 3.1 Type Identifiers
+
+
| ID | Type | Description | Size (bytes) |
+
| ---- | --------- | ----------------------------- | ------------ |
+
| 0x00 | u8 | Unsigned 8-bit integer | 1 |
+
| 0x01 | i8 | Signed 8-bit integer | 1 |
+
| 0x02 | u16 | Unsigned 16-bit integer | 2 |
+
| 0x03 | i16 | Signed 16-bit integer | 2 |
+
| 0x04 | u32 | Unsigned 32-bit integer | 4 |
+
| 0x05 | i32 | Signed 32-bit integer | 4 |
+
| 0x06 | u64 | Unsigned 64-bit integer | 8 |
+
| 0x07 | i64 | Signed 64-bit integer | 8 |
+
| 0x08 | f32 | 32-bit IEEE 754 float | 4 |
+
| 0x09 | f64 | 64-bit IEEE 754 float | 8 |
+
| 0x0A | bool | Boolean value | 1 |
+
| 0x0B | String | UTF-8 encoded string | Variable |
+
| 0x0C | Option | Optional value container | Variable |
+
| 0x0D | List | Heterogeneous array | Variable |
+
| 0x0E | Map | Key-value dictionary | Variable |
+
| 0x0F | Array | Homogeneous typed array | Variable |
+
| 0x10 | Timestamp | Unix timestamp (milliseconds) | 8 |
+
| 0x11 | UUID | 128-bit UUID | 16 |
+
+
**Note**: Type IDs 0x12-0xFF are reserved for future expansion.
+
+
<!-- TOC --><a name="32-array-compatible-types"></a>
+
+
### 3.2 Array-Compatible Types
+
+
The following types are valid as `Array` element types:
+
+
- All integer types: `u8`, `i8`, `u16`, `i16`, `u32`, `i32`, `u64`, `i64`
+
- All floating-point types: `f32`, `f64`
+
- Boolean type: `bool`
+
+
Complex types (`String`, `Option`, `List`, `Map`, `Array`, `Timestamp`, `UUID`)
+
MUST use the heterogeneous `List` type.
+
+
<!-- TOC --><a name="33-map-key-compatible-types"></a>
+
+
### 3.3 Map Key Compatible Types
+
+
All types are valid as `Map` key types except:
+
+
- `Option`
+
- `List`
+
- `Map`
+
- `Array`
+
+
<!-- TOC --><a name="4-binary-encoding"></a>
+
+
## 4. Binary Encoding
+
+
<!-- TOC --><a name="41-primitive-types"></a>
+
+
### 4.1 Primitive Types
+
+
<!-- TOC --><a name="411-integer-types"></a>
+
+
#### 4.1.1 Integer Types
+
+
```
+
[type_id: u8] [value: T]
+
```
+
+
Where `T` is the appropriately-sized integer in the file's endianness.
+
+
<!-- TOC --><a name="412-floating-point-types"></a>
+
+
#### 4.1.2 Floating-Point Types
+
+
```
+
[type_id: u8] [value: T]
+
```
+
+
Where `T` follows IEEE 754 encoding in the file's endianness.
+
+
<!-- TOC --><a name="413-boolean"></a>
+
+
#### 4.1.3 Boolean
+
+
```
+
[type_id: u8] [value: u8]
+
```
+
+
- `0x00`: false
+
- `0x01`: true
+
- All other values are invalid
+
+
<!-- TOC --><a name="42-string-type"></a>
+
+
### 4.2 String Type
+
+
```
+
[type_id: u8] [length: u32] [utf8_data: [u8; length]]
+
```
+
+
- `length`: Number of bytes in UTF-8 encoding
+
- `utf8_data`: Valid UTF-8 byte sequence
+
- Empty strings have length 0 and no data bytes
+
+
<!-- TOC --><a name="43-option-type"></a>
+
+
### 4.3 Option Type
+
+
```
+
[type_id: u8] [inner_type_id: u8] [discriminant: u8] [payload?]
+
```
+
+
- `inner_type_id`: Type ID of the contained value
+
- `discriminant`:
+
- `0x00`: None (no payload follows)
+
- `0x01`: Some (payload follows)
+
- `payload`: Present only when discriminant is `0x01`
+
+
**Examples:**
+
+
- `Option<u32>::None`: `[0x0C] [0x04] [0x00]`
+
- `Option<u32>::Some(42)`: `[0x0C] [0x04] [0x01] [42, 0, 0, 0]` (little-endian)
+
+
<!-- TOC --><a name="44-list-type-heterogeneous"></a>
+
+
### 4.4 List Type (Heterogeneous)
+
+
```
+
[type_id: u8] [length: u32] [element_1] [element_2] ... [element_n]
+
```
+
+
- `length`: Number of elements
+
- Each element is a complete typed value (type_id + data)
+
- Elements may have different types
+
+
**Example:** List `[42u8, "hello", true]`
+
+
```
+
[0x0D] // List type
+
[3, 0, 0, 0] // 3 elements
+
[0x00] [42] // u8: 42
+
[0x0B] [5, 0, 0, 0] [h, e, l, l, o] // String: "hello"
+
[0x0A] [1] // bool: true
+
```
+
+
<!-- TOC --><a name="45-map-type"></a>
+
+
### 4.5 Map Type
+
+
```
+
[type_id: u8] [length: u32] [pair_1] [pair_2] ... [pair_n]
+
```
+
+
Each key-value pair:
+
+
```
+
[key] [value]
+
```
+
+
- `length`: Number of key-value pairs
+
- Both `key` and `value` are complete typed values
+
- `key` MUST be a map key compatible type (see Section 3.3)
+
- Keys SHOULD be unique (behavior for duplicate keys is undefined)
+
+
**Example:** Map `{42u8: "answer", "pi": 3.14f32}`
+
+
```
+
[0x0E] [2, 0, 0, 0] // Map with 2 pairs
+
[0x00] [42] [0x0B] [6, 0, 0, 0] [a, n, s, w, e, r] // 42u8 -> "answer"
+
[0x0B] [2, 0, 0, 0] [p, i] [0x08] [0xC3, 0xF5, 0x48, 0x40] // "pi" -> 3.14f32
+
```
+
+
<!-- TOC --><a name="46-array-type-homogeneous"></a>
+
+
### 4.6 Array Type (Homogeneous)
+
+
```
+
[type_id: u8] [length: u32] [element_type_id: u8] [element_1] ... [element_n]
+
```
+
+
- `length`: Number of elements
+
- `element_type_id`: Must be an array-compatible type (see Section 3.2)
+
- Elements are stored as raw values (no type_id prefix per element)
+
+
**Example:** `i32` array `[1, 2, 3]`
+
+
```
+
[0x0F] // Array type
+
[3, 0, 0, 0] // 3 elements
+
[0x05] // element type: i32
+
[1, 0, 0, 0] // 1
+
[2, 0, 0, 0] // 2
+
[3, 0, 0, 0] // 3
+
```
+
+
<!-- TOC --><a name="47-timestamp-type"></a>
+
+
### 4.7 Timestamp Type
+
+
```
+
[type_id: u8] [value: i64]
+
```
+
+
- `value`: Milliseconds since Unix epoch
+
- Negative values represent times before the epoch
+
+
<!-- TOC --><a name="48-uuid-type"></a>
+
+
### 4.8 UUID Type
+
+
```
+
[type_id: u8] [bytes: [u8; 16]]
+
```
+
+
- `bytes`: 16 bytes representing the UUID in **big-endian** byte order
+
- Follows [RFC 4122][rfc4122] standard binary representation
+
- Byte order is independent of file endianness flag
+
+
**Example:** UUID `550e8400-e29b-41d4-a716-446655440000`
+
+
```
+
[0x11] // UUID type
+
[0x55, 0x0e, 0x84, 0x00, 0xe2, 0x9b, 0x41, 0xd4, // UUID bytes
+
0xa7, 0x16, 0x44, 0x66, 0x55, 0x44, 0x00, 0x00] // (big-endian)
+
```
+
+
<!-- TOC --><a name="5-file-format"></a>
+
+
## 5. File Format
+
+
<!-- TOC --><a name="51-file-structure"></a>
+
+
### 5.1 File Structure
+
+
```
+
[header] [payload]
+
```
+
+
<!-- TOC --><a name="52-header-format"></a>
+
+
### 5.2 Header Format
+
+
```
+
[magic: [u8; 4]] [version: u8] [flags: u8] [compression: u8] [payload_length: u32]
+
```
+
+
<!-- TOC --><a name="521-magic-bytes"></a>
+
+
#### 5.2.1 Magic Bytes
+
+
Fixed 4-byte signature: `NBT2` (0x4E, 0x42, 0x54, 0x32).
+
+
<!-- TOC --><a name="5211-file-extension-and-mime-type"></a>
+
+
##### 5.2.1.1 File Extension and MIME type
+
+
The recommended file extension is `.nbt2`. The recommended MIME type is
+
`application/x-nbt2`.
+
+
<!-- TOC --><a name="522-version"></a>
+
+
#### 5.2.2 Version
+
+
- `0x01`: NBT2.1 (current version)
+
- Future versions increment this value
+
+
<!-- TOC --><a name="523-flags"></a>
+
+
#### 5.2.3 Flags
+
+
8-bit flag field:
+
+
- Bit 0: Endianness (0 = little-endian, 1 = big-endian)
+
- Bits 1-7: Reserved (MUST be zero)
+
+
<!-- TOC --><a name="524-compression-method"></a>
+
+
#### 5.2.4 Compression Method
+
+
- `0x00`: No compression
+
- `0x01`: Gzip compression ([RFC 1952][rfc1952])
+
- `0x02`: Zlib compression ([RFC 1950][rfc1950])
+
- `0x03`: LZ4 compression
+
- `0x04-0xFF`: Reserved for future compression methods
+
+
<!-- TOC --><a name="525-payload-length"></a>
+
+
#### 5.2.5 Payload Length
+
+
- Length of payload in bytes (u32, in file's endianness)
+
- For compressed files: length of compressed data
+
- For uncompressed files: length of raw NBT2 data
+
+
<!-- TOC --><a name="53-payload"></a>
+
+
### 5.3 Payload
+
+
The payload contains a single root typed value, typically a `Map`. Unlike NBT,
+
the root value has no name.
+
+
<!-- TOC --><a name="6-example"></a>
+
+
## 6. Example
+
+
Complete uncompressed little-endian file containing `{"test": 42i32}`:
+
+
```
+
[0x4E, 0x42, 0x54, 0x32] // Magic: "NBT2"
+
[0x01] // Version: 2.1
+
[0x00] // Flags: little-endian
+
[0x00] // Compression: none
+
[23, 0, 0, 0] // Payload length: 23 bytes
+
+
// Payload: Map with one entry
+
[0x0E] // Map type
+
[1, 0, 0, 0] // 1 key-value pair
+
[0x0B] [4, 0, 0, 0] [t, e, s, t] // String key: "test"
+
[0x05] [42, 0, 0, 0] // i32 value: 42
+
```
+
+
<!-- TOC --><a name="7-conformance-requirements"></a>
+
+
## 7. Conformance Requirements
+
+
- Implementations MUST reject files with invalid magic bytes
+
- Implementations MUST support at least version `0x01`
+
- Unknown compression methods SHOULD be rejected
+
- Invalid UTF-8 in strings MUST be rejected
+
- Boolean values other than 0x00/0x01 MUST be rejected
+
- Reserved flag bits MUST be zero in generated files
+
+
[rfc2119]: https://www.rfc-editor.org/rfc/rfc2119
+
[mutf8]: https://docs.oracle.com/en/java/javase/23/docs/api/java.base/java/io/DataInput.html#modified-utf-8
+
[rfc1950]: https://www.rfc-editor.org/rfc/rfc1950
+
[rfc1952]: https://www.rfc-editor.org/rfc/rfc1952
+
[rfc4122]: https://www.rfc-editor.org/rfc/rfc4122
+20
crates/migrate/Cargo.toml
···
···
+
[package]
+
name = "nbt2_migrate"
+
version = "0.1.0"
+
edition = "2024"
+
license = "MIT OR Apache-2.0"
+
rust-version = "1.89"
+
description = "Data transformations for the NBT2 format."
+
+
[dependencies]
+
nbtree_core.workspace = true
+
thiserror.workspace = true
+
uuid.workspace = true
+
+
nbt2 = { path = "../..", version = "0.1.0" }
+
+
[dev-dependencies]
+
insta.workspace = true
+
+
[lints]
+
workspace = true
+6
crates/migrate/README.md
···
···
+
# nbt2_migrate
+
+
Provides data transformations for the NBT2 format.
+
+
In the future this should provide utilities for converting between NBT2
+
versions, but for now it just contains a simple NBT->NBT2 conversion.
+5
crates/migrate/src/lib.rs
···
···
+
//! Data transformations for the NBT2 format.
+
//!
+
//! In the future this should provide utilities for converting between NBT2
+
//! versions, but for now it just contains a simple NBT->NBT2 conversion.
+
pub mod nbt;
+163
crates/migrate/src/nbt.rs
···
···
+
//! Conversion utilities for converting NBT tags to NBT2 format.
+
use nbt2::{Tag, TagKind};
+
use nbtree_core::nbt::{Tag as NBTTag, TagId as NBTTagKind};
+
use uuid::Uuid;
+
+
#[derive(Debug, Clone, thiserror::Error)]
+
pub enum Error {
+
#[error("Cannot convert NBT kind '{0:?}' to NBT2.")]
+
CannotConvertKind(NBTTagKind),
+
}
+
+
pub type Result<T> = core::result::Result<T, Error>;
+
+
#[derive(Debug, Clone)]
+
pub struct ConversionOptions {
+
/// Whether the converter should attempt to read Int Arrays with 4 items or
+
/// Long Arrays with 2 items as UUIDs (strictly v4).
+
pub attempt_to_convert_uuids: bool,
+
}
+
+
impl Default for ConversionOptions {
+
fn default() -> Self {
+
Self {
+
attempt_to_convert_uuids: false,
+
}
+
}
+
}
+
+
impl ConversionOptions {
+
pub fn attempt_to_convert_uuids(mut self, convert: bool) -> Self {
+
self.attempt_to_convert_uuids = convert;
+
self
+
}
+
}
+
+
/// Converts an NBT tag to its NBT2 equivalent.
+
///
+
/// This function performs a recursive conversion of NBT data structures
+
/// to NBT2 format, handling the differences between the two formats:
+
///
+
/// - NBT byte/int/long arrays become NBT2 `Array<i8>`/`Array<i32>`/`Array<i64>`
+
/// - NBT lists of primitive types become NBT2 homogeneous `Array`s
+
/// - NBT lists of complex types become NBT2 heterogeneous `List`s
+
/// - NBT compounds become NBT2 `Map`s with string keys
+
pub fn convert_tag(tag: NBTTag, opts: &ConversionOptions) -> Result<Tag> {
+
match tag {
+
NBTTag::End => Err(Error::CannotConvertKind(NBTTagKind::End)),
+
NBTTag::Byte(byte) => Ok(Tag::I8(byte)),
+
NBTTag::Short(short) => Ok(Tag::I16(short)),
+
NBTTag::Int(int) => Ok(Tag::I32(int)),
+
NBTTag::Long(long) => Ok(Tag::I64(long)),
+
NBTTag::Float(float) => Ok(Tag::F32(float)),
+
NBTTag::Double(double) => Ok(Tag::F64(double)),
+
NBTTag::ByteArray(bytes) => Ok(Tag::Array(
+
TagKind::I8,
+
bytes.into_iter().map(Tag::I8).collect(),
+
)),
+
NBTTag::String(string) => Ok(Tag::String(string)),
+
NBTTag::List(tag_kind, tags) => match tag_kind {
+
NBTTagKind::Byte
+
| NBTTagKind::Short
+
| NBTTagKind::Int
+
| NBTTagKind::Long
+
| NBTTagKind::Float
+
| NBTTagKind::Double => {
+
let converted_kind = match tag_kind {
+
NBTTagKind::Byte => TagKind::I8,
+
NBTTagKind::Short => TagKind::I16,
+
NBTTagKind::Int => TagKind::I32,
+
NBTTagKind::Long => TagKind::I64,
+
NBTTagKind::Float => TagKind::F32,
+
NBTTagKind::Double => TagKind::F64,
+
_ => unreachable!(
+
"tag_kind should be one of the primitive types, got: {:?} despite already matching.",
+
tag_kind
+
),
+
};
+
+
let converted_tags: core::result::Result<Vec<Tag>, _> = tags
+
.into_iter()
+
.map(|tag| convert_tag(tag, opts))
+
.collect();
+
+
Ok(Tag::Array(converted_kind, converted_tags?))
+
}
+
_ => {
+
let converted_tags: Result<Vec<Tag>> = tags
+
.into_iter()
+
.map(|tag| convert_tag(tag, opts))
+
.collect();
+
Ok(Tag::List(converted_tags?))
+
}
+
},
+
NBTTag::Compound(index_map) => {
+
let converted_map: Result<Vec<(Tag, Tag)>> = index_map
+
.into_iter()
+
.map(|(key, value)| {
+
Ok((Tag::String(key), convert_tag(*value, opts)?))
+
})
+
.collect();
+
Ok(Tag::Map(converted_map?))
+
}
+
NBTTag::IntArray(items) => {
+
if items.len() == 4 && opts.attempt_to_convert_uuids {
+
let bytes: [u8; 16] = [
+
(items[0] >> 24) as u8,
+
(items[0] >> 16) as u8,
+
(items[0] >> 8) as u8,
+
items[0] as u8,
+
(items[1] >> 24) as u8,
+
(items[1] >> 16) as u8,
+
(items[1] >> 8) as u8,
+
items[1] as u8,
+
(items[2] >> 24) as u8,
+
(items[2] >> 16) as u8,
+
(items[2] >> 8) as u8,
+
items[2] as u8,
+
(items[3] >> 24) as u8,
+
(items[3] >> 16) as u8,
+
(items[3] >> 8) as u8,
+
items[3] as u8,
+
];
+
+
let uuid = Uuid::from_bytes(bytes);
+
+
if uuid.get_version() == Some(uuid::Version::Random) {
+
return Ok(Tag::Uuid(uuid));
+
} else {
+
return Ok(Tag::Array(
+
TagKind::I32,
+
items.into_iter().map(Tag::I32).collect(),
+
));
+
}
+
} else {
+
Ok(Tag::Array(
+
TagKind::I32,
+
items.into_iter().map(Tag::I32).collect(),
+
))
+
}
+
}
+
NBTTag::LongArray(items) => {
+
if items.len() == 2 && opts.attempt_to_convert_uuids {
+
let most = items[0] as u64; // treating as raw bits
+
let least = items[1] as u64; // ditto
+
let uuid = Uuid::from_u64_pair(most, least);
+
+
if uuid.get_version() == Some(uuid::Version::Random) {
+
Ok(Tag::Uuid(uuid))
+
} else {
+
Ok(Tag::Array(
+
TagKind::I64,
+
items.into_iter().map(Tag::I64).collect(),
+
))
+
}
+
} else {
+
Ok(Tag::Array(
+
TagKind::I64,
+
items.into_iter().map(Tag::I64).collect(),
+
))
+
}
+
}
+
}
+
}
+18
crates/migrate/tests/snapshots/test_nbt__int_array_uuid_does_not_convert.snap
···
···
+
---
+
source: crates/migrate/tests/test_nbt.rs
+
expression: tags
+
---
+
[
+
I32(
+
-1983734701,
+
),
+
I32(
+
-284420573,
+
),
+
I32(
+
-1832185991,
+
),
+
I32(
+
-1828022334,
+
),
+
]
+12
crates/migrate/tests/snapshots/test_nbt__long_array_uuid_does_not_convert.snap
···
···
+
---
+
source: crates/migrate/tests/test_nbt.rs
+
expression: tags
+
---
+
[
+
I64(
+
-8520075660724783581,
+
),
+
I64(
+
-7869178909067405374,
+
),
+
]
+155
crates/migrate/tests/test_nbt.rs
···
···
+
use std::str::FromStr;
+
+
use insta::assert_debug_snapshot;
+
use nbt2;
+
use nbt2_migrate::nbt::ConversionOptions;
+
use nbtree_core::nbt;
+
use uuid::Uuid;
+
+
#[test]
+
fn test_int_array_uuid_does_convert() {
+
let nbt_tag = nbt::Tag::IntArray(vec![
+
-1983734701,
+
-284408285,
+
-1832185991,
+
-1828022334,
+
]);
+
+
let converted = nbt2_migrate::nbt::convert_tag(
+
nbt_tag,
+
&ConversionOptions::default().attempt_to_convert_uuids(true),
+
)
+
.unwrap();
+
+
if let nbt2::Tag::Uuid(uuid) = converted {
+
assert_eq!(
+
uuid,
+
Uuid::from_str("89c29c53-ef0c-4623-92cb-0f79930a97c2").unwrap()
+
);
+
} else {
+
panic!(
+
"Expected NBT2 tag kind of Uuid, got {0:?}",
+
converted.kind()
+
)
+
}
+
}
+
+
#[test]
+
fn test_int_array_uuid_does_not_convert() {
+
let nbt_tag = nbt::Tag::IntArray(vec![
+
-1983734701,
+
-284420573, // not version 4
+
-1832185991,
+
-1828022334,
+
]);
+
+
let converted = nbt2_migrate::nbt::convert_tag(
+
nbt_tag,
+
&ConversionOptions::default().attempt_to_convert_uuids(true),
+
)
+
.unwrap();
+
+
if let nbt2::Tag::Array(kind, tags) = converted {
+
assert_eq!(kind, nbt2::TagKind::I32);
+
assert_eq!(tags.len(), 4);
+
+
assert_debug_snapshot!(tags);
+
} else {
+
panic!(
+
"Expected NBT2 tag kind of Array, got {0:?}",
+
converted.kind()
+
)
+
}
+
}
+
+
#[test]
+
fn test_int_array_wrong_length_does_not_convert() {
+
let nbt_tag = nbt::Tag::IntArray(vec![
+
-1983734701,
+
-284408285,
+
-1832185991,
+
-1828022334,
+
123456789,
+
]);
+
let converted = nbt2_migrate::nbt::convert_tag(
+
nbt_tag,
+
&ConversionOptions::default().attempt_to_convert_uuids(true),
+
)
+
.unwrap();
+
if let nbt2::Tag::Array(kind, tags) = converted {
+
assert_eq!(kind, nbt2::TagKind::I32);
+
assert_eq!(tags.len(), 5);
+
} else {
+
panic!(
+
"Expected NBT2 tag kind of Array, got {0:?}",
+
converted.kind()
+
)
+
}
+
}
+
+
#[test]
+
fn test_long_array_uuid_does_convert() {
+
let nbt_tag =
+
nbt::Tag::LongArray(vec![-8520075660724779485, -7869178909067405374]);
+
let converted = nbt2_migrate::nbt::convert_tag(
+
nbt_tag,
+
&ConversionOptions::default().attempt_to_convert_uuids(true),
+
)
+
.unwrap();
+
if let nbt2::Tag::Uuid(uuid) = converted {
+
assert_eq!(
+
uuid,
+
Uuid::from_str("89c29c53-ef0c-4623-92cb-0f79930a97c2").unwrap()
+
);
+
} else {
+
panic!(
+
"Expected NBT2 tag kind of Uuid, got {0:?}",
+
converted.kind()
+
)
+
}
+
}
+
+
#[test]
+
fn test_long_array_uuid_does_not_convert() {
+
let nbt_tag = nbt::Tag::LongArray(vec![
+
-8520075660724783581, // not version 4
+
-7869178909067405374,
+
]);
+
let converted = nbt2_migrate::nbt::convert_tag(
+
nbt_tag,
+
&ConversionOptions::default().attempt_to_convert_uuids(true),
+
)
+
.unwrap();
+
if let nbt2::Tag::Array(kind, tags) = converted {
+
assert_eq!(kind, nbt2::TagKind::I64);
+
assert_debug_snapshot!(tags);
+
} else {
+
panic!(
+
"Expected NBT2 tag kind of Array, got {0:?}",
+
converted.kind()
+
)
+
}
+
}
+
+
#[test]
+
fn test_long_array_wrong_length_does_not_convert() {
+
let nbt_tag = nbt::Tag::LongArray(vec![
+
-8520075660724779485,
+
-7869178909067405374,
+
123456789,
+
]);
+
let converted = nbt2_migrate::nbt::convert_tag(
+
nbt_tag,
+
&ConversionOptions::default().attempt_to_convert_uuids(true),
+
)
+
.unwrap();
+
if let nbt2::Tag::Array(kind, tags) = converted {
+
assert_eq!(kind, nbt2::TagKind::I64);
+
assert_eq!(tags.len(), 3);
+
} else {
+
panic!(
+
"Expected NBT2 tag kind of Array, got {0:?}",
+
converted.kind()
+
)
+
}
+
}
+1
rustfmt.toml
···
···
+
max_width = 80
+9
src/len.rs
···
···
+
pub const BYTE: usize = 1;
+
pub const INT_8: usize = BYTE;
+
pub const INT_16: usize = BYTE * 2;
+
pub const INT_32: usize = BYTE * 4;
+
pub const INT_64: usize = BYTE * 8;
+
pub const FLOAT_32: usize = BYTE * 4;
+
pub const FLOAT_64: usize = BYTE * 8;
+
pub const UUID: usize = 16;
+
pub const MAGIC_BYTES: usize = BYTE * 4;
+266
src/lib.rs
···
···
+
pub(crate) mod len;
+
pub mod reader;
+
pub mod writer;
+
+
/// Kinds of [`Tag`]s.
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+
#[repr(u8)]
+
pub enum TagKind {
+
U8 = 0x00,
+
I8 = 0x01,
+
U16 = 0x02,
+
I16 = 0x03,
+
U32 = 0x04,
+
I32 = 0x05,
+
U64 = 0x06,
+
I64 = 0x07,
+
F32 = 0x08,
+
F64 = 0x09,
+
Bool = 0x0A,
+
String = 0x0B,
+
Option = 0x0C,
+
List = 0x0D,
+
Map = 0x0E,
+
Array = 0x0F,
+
Timestamp = 0x10,
+
Uuid = 0x11,
+
}
+
+
impl TagKind {
+
pub fn from_byte(byte: u8) -> Option<TagKind> {
+
match byte {
+
0x00 => Some(TagKind::U8),
+
0x01 => Some(TagKind::I8),
+
0x02 => Some(TagKind::U16),
+
0x03 => Some(TagKind::I16),
+
0x04 => Some(TagKind::U32),
+
0x05 => Some(TagKind::I32),
+
0x06 => Some(TagKind::U64),
+
0x07 => Some(TagKind::I64),
+
0x08 => Some(TagKind::F32),
+
0x09 => Some(TagKind::F64),
+
0x0A => Some(TagKind::Bool),
+
0x0B => Some(TagKind::String),
+
0x0C => Some(TagKind::Option),
+
0x0D => Some(TagKind::List),
+
0x0E => Some(TagKind::Map),
+
0x0F => Some(TagKind::Array),
+
0x10 => Some(TagKind::Timestamp),
+
0x11 => Some(TagKind::Uuid),
+
_ => None,
+
}
+
}
+
+
pub fn valid_for_array_element(&self) -> bool {
+
matches!(
+
self,
+
TagKind::U8
+
| TagKind::I8
+
| TagKind::U16
+
| TagKind::I16
+
| TagKind::U32
+
| TagKind::I32
+
| TagKind::U64
+
| TagKind::I64
+
| TagKind::F32
+
| TagKind::F64
+
| TagKind::Bool
+
)
+
}
+
+
pub fn valid_for_map_key(&self) -> bool {
+
!matches!(
+
self,
+
TagKind::Option | TagKind::List | TagKind::Array | TagKind::Map
+
)
+
}
+
}
+
+
/// NBT2 Tags.
+
#[derive(Debug, Clone, PartialEq)]
+
pub enum Tag {
+
U8(u8),
+
I8(i8),
+
U16(u16),
+
I16(i16),
+
U32(u32),
+
I32(i32),
+
U64(u64),
+
I64(i64),
+
F32(f32),
+
F64(f64),
+
Bool(bool),
+
String(String),
+
Option(TagKind, Option<Box<Tag>>),
+
List(Vec<Tag>),
+
Map(Vec<(Tag, Tag)>),
+
/// Holds a list of `Tag`s of one kind.
+
Array(TagKind, Vec<Tag>),
+
Timestamp(chrono::DateTime<chrono::Utc>),
+
Uuid(uuid::Uuid),
+
}
+
+
macro_rules! simple_tag_constructors {
+
( $( $fn_name:ident : $variant:ident ( $ty:ty ) ),* $(,)? ) => {
+
$(
+
#[doc = concat!("Creates a new [`Tag::", stringify!($variant), "`].")]
+
pub fn $fn_name(value: $ty) -> Self {
+
Tag::$variant(value)
+
}
+
)*
+
};
+
}
+
+
impl Tag {
+
simple_tag_constructors! {
+
new_u8: U8(u8),
+
new_i8: I8(i8),
+
new_u16: U16(u16),
+
new_i16: I16(i16),
+
new_u32: U32(u32),
+
new_i32: I32(i32),
+
new_u64: U64(u64),
+
new_i64: I64(i64),
+
new_f32: F32(f32),
+
new_f64: F64(f64),
+
new_bool: Bool(bool),
+
}
+
+
/// Creates a new [`Tag::String`].
+
pub fn new_string<S: Into<String>>(s: S) -> Self {
+
Tag::String(s.into())
+
}
+
+
/// Creates a new [`Tag::Option`] containing the given `Tag`.
+
pub fn new_option(kind: TagKind, value: Option<Tag>) -> Self {
+
Tag::Option(kind, value.map(Box::new))
+
}
+
+
/// Creates a new [`Tag::List`] from a vector of `Tag`s.
+
pub fn new_list(values: Vec<Tag>) -> Self {
+
Tag::List(values)
+
}
+
+
/// Creates a new [`Tag::Map`] from a vector of key-value pairs.
+
pub fn new_map(entries: Vec<(Tag, Tag)>) -> Self {
+
Tag::Map(entries)
+
}
+
+
/// Creates a new [`Tag::Array`] of the specified [`TagKind`] and elements.
+
pub fn new_array(kind: TagKind, elements: Vec<Tag>) -> Self {
+
Tag::Array(kind, elements)
+
}
+
+
/// Creates a new [`Tag::Timestamp`] from a [`chrono::DateTime<Utc>`].
+
pub fn new_timestamp(value: chrono::DateTime<chrono::Utc>) -> Self {
+
Tag::Timestamp(value)
+
}
+
+
/// Creates a new [`Tag::Uuid`] from a [`uuid::Uuid`].
+
pub fn new_uuid(value: uuid::Uuid) -> Self {
+
Tag::Uuid(value)
+
}
+
+
/// Get this `Tag`'s [`TagKind`].
+
pub fn kind(&self) -> TagKind {
+
match self {
+
Tag::U8 { .. } => TagKind::U8,
+
Tag::I8 { .. } => TagKind::I8,
+
Tag::U16 { .. } => TagKind::U16,
+
Tag::I16 { .. } => TagKind::I16,
+
Tag::U32 { .. } => TagKind::U32,
+
Tag::I32 { .. } => TagKind::I32,
+
Tag::U64 { .. } => TagKind::U64,
+
Tag::I64 { .. } => TagKind::I64,
+
Tag::F32 { .. } => TagKind::F32,
+
Tag::F64 { .. } => TagKind::F64,
+
Tag::Bool { .. } => TagKind::Bool,
+
Tag::String { .. } => TagKind::String,
+
Tag::Option { .. } => TagKind::Option,
+
Tag::List { .. } => TagKind::List,
+
Tag::Map { .. } => TagKind::Map,
+
Tag::Array { .. } => TagKind::Array,
+
Tag::Timestamp { .. } => TagKind::Timestamp,
+
Tag::Uuid { .. } => TagKind::Uuid,
+
}
+
}
+
}
+
+
/// File header for NBT2 files.
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+
pub struct FileHeader {
+
pub version: u8,
+
pub little_endian: bool,
+
pub compression: CompressionMethod,
+
pub payload_length: u32,
+
}
+
+
impl FileHeader {
+
pub const MAGIC_BYTES: [u8; len::MAGIC_BYTES] = [b'N', b'B', b'T', b'2'];
+
+
pub const CURRENT_VERSION: u8 = 1;
+
+
pub fn new(compression: CompressionMethod, payload_length: u32) -> Self {
+
Self {
+
version: Self::CURRENT_VERSION,
+
little_endian: true,
+
compression,
+
payload_length,
+
}
+
}
+
}
+
+
/// Compression methods supported by NBT2.
+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+
#[repr(u8)]
+
pub enum CompressionMethod {
+
None = 0x00,
+
Gzip = 0x01,
+
Zlib = 0x02,
+
Lz4 = 0x03,
+
}
+
+
impl CompressionMethod {
+
pub fn from_byte(byte: u8) -> Option<CompressionMethod> {
+
match byte {
+
0x00 => Some(CompressionMethod::None),
+
0x01 => Some(CompressionMethod::Gzip),
+
0x02 => Some(CompressionMethod::Zlib),
+
0x03 => Some(CompressionMethod::Lz4),
+
_ => None,
+
}
+
}
+
}
+
+
/// Errors that can occur during NBT2 operations.
+
#[derive(Debug, thiserror::Error)]
+
pub enum Error {
+
#[error("Unexpected end of file")]
+
UnexpectedEof,
+
#[error("Invalid magic bytes sequence '{0:02X?}'")]
+
InvalidMagic([u8; len::MAGIC_BYTES]),
+
#[error("Unsupported version 0x{0:x}")]
+
UnsupportedVersion(u8),
+
#[error("Unsupported compression method {0:?}")]
+
UnsupportedCompression(CompressionMethod),
+
#[error("Unknown compression method type 0x{0:x}")]
+
UnknownCompression(u8),
+
#[error("Invalid tag ID 0x{0:x}")]
+
InvalidTagKind(u8),
+
#[error("Encountered an invalid UTF-8 sequence: {0}")]
+
InvalidUtf8(#[from] std::string::FromUtf8Error),
+
#[error("Invalid boolean value 0x{0:x}")]
+
InvalidBoolValue(u8),
+
#[error("Invalid Option discriminant 0x{0:x}")]
+
InvalidOptionDiscriminant(u8),
+
#[error("Invalid Option kind 0x{0:?}, expected 0x{1:?}")]
+
InvalidOptionKind(TagKind, TagKind),
+
#[error("Invalid element kind for Array tag '{0:?}'")]
+
InvalidArrayTagKind(TagKind),
+
#[error("IO error: {0}")]
+
IoError(#[from] std::io::Error),
+
#[error("Milliseconds for Timestamp are out of range: {0}")]
+
MillisecondsOutOfRange(i64),
+
}
+
+
pub type Result<T> = core::result::Result<T, Error>;
+322
src/reader.rs
···
···
+
use crate::{CompressionMethod, Error, FileHeader, Result, Tag, TagKind, len};
+
use std::io::Read;
+
+
use chrono::DateTime;
+
#[cfg(feature = "gzip")]
+
use flate2::read::{GzDecoder, ZlibDecoder};
+
#[cfg(feature = "lz4")]
+
use lz4::Decoder;
+
use uuid::Uuid;
+
+
pub struct Reader {
+
reader: Box<dyn Read>,
+
position: usize,
+
header: FileHeader,
+
}
+
+
impl Reader {
+
/// Create a Reader that automatically wraps the underlying reader in a
+
/// decompressor based on the compression method in the header.
+
pub fn new<R: Read + 'static>(mut reader: R) -> Result<Self> {
+
let header = Self::read_header(&mut reader)?;
+
+
let reader: Box<dyn Read> = match header.compression {
+
CompressionMethod::None => Box::new(reader),
+
CompressionMethod::Gzip => {
+
#[cfg(feature = "gzip")]
+
{
+
Box::new(GzDecoder::new(reader))
+
}
+
#[cfg(not(feature = "gzip"))]
+
{
+
return Err(Error::UnsupportedCompression(
+
header.compression,
+
));
+
}
+
}
+
CompressionMethod::Zlib => {
+
#[cfg(feature = "gzip")]
+
{
+
Box::new(ZlibDecoder::new(reader))
+
}
+
#[cfg(not(feature = "gzip"))]
+
{
+
return Err(Error::UnsupportedCompression(
+
header.compression,
+
));
+
}
+
}
+
CompressionMethod::Lz4 => {
+
#[cfg(feature = "lz4")]
+
{
+
Box::new(Decoder::new(reader).map_err(Error::IoError)?)
+
}
+
#[cfg(not(feature = "lz4"))]
+
{
+
return Err(Error::UnsupportedCompression(
+
header.compression,
+
));
+
}
+
}
+
};
+
+
Ok(Self {
+
reader,
+
position: 0,
+
header,
+
})
+
}
+
+
fn read_header<R: Read + 'static>(reader: &mut R) -> Result<FileHeader> {
+
let mut magic_bytes = [0u8; len::MAGIC_BYTES];
+
reader
+
.read_exact(&mut magic_bytes)
+
.map_err(|_| Error::UnexpectedEof)?;
+
if magic_bytes != FileHeader::MAGIC_BYTES {
+
return Err(Error::InvalidMagic(magic_bytes));
+
}
+
+
let mut version_byte = [0u8; len::BYTE];
+
reader.read_exact(&mut version_byte)?;
+
let version = version_byte[0];
+
if version != FileHeader::CURRENT_VERSION {
+
return Err(Error::UnsupportedVersion(version));
+
}
+
+
let mut flag_byte = [0u8; len::BYTE];
+
reader.read_exact(&mut flag_byte)?;
+
let little_endian = flag_byte[0] == 0;
+
+
let mut compression_byte = [0u8; len::BYTE];
+
reader.read_exact(&mut compression_byte)?;
+
let compression = CompressionMethod::from_byte(compression_byte[0])
+
.ok_or(Error::UnknownCompression(compression_byte[0]))?;
+
+
let mut length_bytes = [0u8; len::INT_32];
+
reader.read_exact(&mut length_bytes)?;
+
let payload_length = if little_endian {
+
u32::from_le_bytes(length_bytes)
+
} else {
+
u32::from_be_bytes(length_bytes)
+
};
+
+
Ok(FileHeader {
+
version,
+
little_endian,
+
compression,
+
payload_length,
+
})
+
}
+
+
pub fn file_header(&self) -> &FileHeader {
+
&self.header
+
}
+
+
pub fn read_tag(&mut self) -> Result<Tag> {
+
let kind = self.read_tag_kind()?;
+
self.read_tag_of_kind(kind)
+
}
+
+
fn read_tag_of_kind(&mut self, kind: TagKind) -> Result<Tag> {
+
match kind {
+
TagKind::U8 => Ok(Tag::U8(self.read_u8()?)),
+
TagKind::I8 => Ok(Tag::I8(self.read_i8()?)),
+
TagKind::U16 => Ok(Tag::U16(self.read_u16()?)),
+
TagKind::I16 => Ok(Tag::I16(self.read_i16()?)),
+
TagKind::U32 => Ok(Tag::U32(self.read_u32()?)),
+
TagKind::I32 => Ok(Tag::I32(self.read_i32()?)),
+
TagKind::U64 => Ok(Tag::U64(self.read_u64()?)),
+
TagKind::I64 => Ok(Tag::I64(self.read_i64()?)),
+
TagKind::F32 => Ok(Tag::F32(self.read_f32()?)),
+
TagKind::F64 => Ok(Tag::F64(self.read_f64()?)),
+
TagKind::Bool => Ok(Tag::Bool(self.read_bool()?)),
+
TagKind::String => Ok(Tag::String(self.read_string()?)),
+
TagKind::Option => Ok(self.read_option()?),
+
TagKind::List => Ok(self.read_list()?),
+
TagKind::Map => Ok(self.read_map()?),
+
TagKind::Array => Ok(self.read_array()?),
+
TagKind::Timestamp => Ok(self.read_timestamp()?),
+
TagKind::Uuid => Ok(self.read_uuid()?),
+
}
+
}
+
+
fn read_option(&mut self) -> Result<Tag> {
+
let kind_byte = self.read_u8()?;
+
let kind = TagKind::from_byte(kind_byte)
+
.ok_or(Error::InvalidTagKind(kind_byte))?;
+
let discriminant = self.read_u8()?;
+
match discriminant {
+
0x00 => Ok(Tag::Option(kind, None)),
+
0x01 => {
+
let inner = self.read_tag_of_kind(kind)?;
+
if kind != inner.kind() {
+
// shouldn't happen but doesnt hurt to check
+
return Err(Error::InvalidOptionKind(inner.kind(), kind));
+
}
+
Ok(Tag::Option(kind, Some(Box::new(inner))))
+
}
+
_ => Err(Error::InvalidOptionDiscriminant(discriminant)),
+
}
+
}
+
+
fn read_list(&mut self) -> Result<Tag> {
+
let length = self.read_u32()? as usize;
+
let mut elements = Vec::with_capacity(length);
+
+
for _ in 0..length {
+
elements.push(self.read_tag()?);
+
}
+
+
Ok(Tag::List(elements))
+
}
+
+
fn read_array(&mut self) -> Result<Tag> {
+
let length = self.read_u32()? as usize;
+
let element_kind = self.read_tag_kind()?;
+
+
if !element_kind.valid_for_array_element() {
+
return Err(Error::InvalidArrayTagKind(element_kind));
+
}
+
+
let mut elements = Vec::with_capacity(length);
+
+
for _ in 0..length {
+
elements.push(self.read_tag_of_kind(element_kind)?);
+
}
+
+
Ok(Tag::Array(element_kind, elements))
+
}
+
+
fn read_map(&mut self) -> Result<Tag> {
+
let length = self.read_u32()? as usize;
+
let mut elements = Vec::with_capacity(length);
+
+
for _ in 0..length {
+
let key = self.read_tag()?;
+
let value = self.read_tag()?;
+
+
elements.push((key, value));
+
}
+
+
Ok(Tag::Map(elements))
+
}
+
+
fn read_timestamp(&mut self) -> Result<Tag> {
+
let ms = self.read_i64()?;
+
let dt = DateTime::from_timestamp_millis(ms)
+
.ok_or(Error::MillisecondsOutOfRange(ms))?;
+
Ok(Tag::Timestamp(dt))
+
}
+
+
fn read_uuid(&mut self) -> Result<Tag> {
+
Ok(Tag::Uuid(Uuid::from_bytes(
+
self.read_bytes::<{ len::UUID }>()?,
+
)))
+
}
+
+
// primitives
+
+
fn read_exact(&mut self, buf: &mut [u8]) -> Result<()> {
+
self.reader
+
.read_exact(buf)
+
.map_err(|_| Error::UnexpectedEof)?;
+
self.position += buf.len();
+
Ok(())
+
}
+
+
fn read_bytes<const N: usize>(&mut self) -> Result<[u8; N]> {
+
let mut buf = [0u8; N];
+
self.read_exact(&mut buf)?;
+
Ok(buf)
+
}
+
+
fn read_bytes_vec(&mut self, n: usize) -> Result<Vec<u8>> {
+
let mut buf = vec![0u8; n];
+
self.read_exact(&mut buf)?;
+
Ok(buf)
+
}
+
+
fn read_tag_kind(&mut self) -> Result<TagKind> {
+
let byte = self.read_u8()?;
+
TagKind::from_byte(byte).ok_or(Error::InvalidTagKind(byte))
+
}
+
+
fn read_bool(&mut self) -> Result<bool> {
+
match self.read_u8()? {
+
0 => Ok(false),
+
1 => Ok(true),
+
v => Err(Error::InvalidBoolValue(v)),
+
}
+
}
+
+
fn read_u8(&mut self) -> Result<u8> {
+
Ok(self.read_bytes::<{ len::INT_8 }>()?[0])
+
}
+
+
fn read_u16(&mut self) -> Result<u16> {
+
let bytes = self.read_bytes::<{ len::INT_16 }>()?;
+
Ok(if self.header.little_endian {
+
u16::from_le_bytes(bytes)
+
} else {
+
u16::from_be_bytes(bytes)
+
})
+
}
+
+
fn read_u32(&mut self) -> Result<u32> {
+
let bytes = self.read_bytes::<{ len::INT_32 }>()?;
+
Ok(if self.header.little_endian {
+
u32::from_le_bytes(bytes)
+
} else {
+
u32::from_be_bytes(bytes)
+
})
+
}
+
+
fn read_u64(&mut self) -> Result<u64> {
+
let bytes = self.read_bytes::<{ len::INT_64 }>()?;
+
Ok(if self.header.little_endian {
+
u64::from_le_bytes(bytes)
+
} else {
+
u64::from_be_bytes(bytes)
+
})
+
}
+
+
fn read_f32(&mut self) -> Result<f32> {
+
let bytes = self.read_bytes::<{ len::FLOAT_32 }>()?;
+
Ok(if self.header.little_endian {
+
f32::from_le_bytes(bytes)
+
} else {
+
f32::from_be_bytes(bytes)
+
})
+
}
+
+
fn read_f64(&mut self) -> Result<f64> {
+
let bytes = self.read_bytes::<{ len::FLOAT_64 }>()?;
+
Ok(if self.header.little_endian {
+
f64::from_le_bytes(bytes)
+
} else {
+
f64::from_be_bytes(bytes)
+
})
+
}
+
+
fn read_i8(&mut self) -> Result<i8> {
+
Ok(self.read_u8()? as i8)
+
}
+
+
fn read_i16(&mut self) -> Result<i16> {
+
Ok(self.read_u16()? as i16)
+
}
+
+
fn read_i32(&mut self) -> Result<i32> {
+
Ok(self.read_u32()? as i32)
+
}
+
+
fn read_i64(&mut self) -> Result<i64> {
+
Ok(self.read_u64()? as i64)
+
}
+
+
fn read_string(&mut self) -> Result<String> {
+
let length = self.read_u32()? as usize;
+
let bytes = self.read_bytes_vec(length)?;
+
String::from_utf8(bytes).map_err(Error::InvalidUtf8)
+
}
+
}
+309
src/writer.rs
···
···
+
use crate::{CompressionMethod, Error, FileHeader, Result, Tag, TagKind, len};
+
use std::io::{Seek, Write};
+
+
use chrono::{DateTime, Utc};
+
use uuid::Uuid;
+
+
struct RawWriter<W: Write + Seek> {
+
writer: W,
+
position: usize,
+
header: FileHeader,
+
}
+
+
impl<W: Write + Seek> RawWriter<W> {
+
fn new(writer: W, compression: CompressionMethod) -> Result<Self> {
+
let header = FileHeader {
+
version: FileHeader::CURRENT_VERSION,
+
little_endian: true,
+
compression,
+
payload_length: 0,
+
};
+
+
Ok(Self {
+
writer,
+
position: 0,
+
header,
+
})
+
}
+
+
fn write_tag(&mut self, tag: &Tag) -> Result<()> {
+
self.write_tag_kind(tag.kind())?;
+
self.write_tag_of_kind(tag)?;
+
+
self.header.payload_length = self.position as u32;
+
+
Ok(())
+
}
+
+
fn write_tag_kind(&mut self, kind: TagKind) -> Result<()> {
+
self.write_u8(kind as u8)
+
}
+
+
fn write_tag_of_kind(&mut self, tag: &Tag) -> Result<()> {
+
match tag {
+
Tag::U8(v) => self.write_u8(*v),
+
Tag::I8(v) => self.write_i8(*v),
+
Tag::U16(v) => self.write_u16(*v),
+
Tag::I16(v) => self.write_i16(*v),
+
Tag::U32(v) => self.write_u32(*v),
+
Tag::I32(v) => self.write_i32(*v),
+
Tag::U64(v) => self.write_u64(*v),
+
Tag::I64(v) => self.write_i64(*v),
+
Tag::F32(v) => self.write_f32(*v),
+
Tag::F64(v) => self.write_f64(*v),
+
Tag::Bool(v) => self.write_bool(*v),
+
Tag::String(s) => self.write_string(s),
+
Tag::Option(kind, opt) => self.write_option(kind, opt.as_deref()),
+
Tag::List(list) => self.write_list(list),
+
Tag::Array(kind, array) => self.write_array(*kind, array),
+
Tag::Map(map) => self.write_map(map),
+
Tag::Timestamp(dt) => self.write_timestamp(dt),
+
Tag::Uuid(uuid) => self.write_uuid(uuid),
+
}
+
}
+
+
fn write_u8(&mut self, v: u8) -> Result<()> {
+
self.writer.write_all(&[v]).map_err(Error::IoError)?;
+
self.position += len::INT_8;
+
Ok(())
+
}
+
+
fn write_i8(&mut self, v: i8) -> Result<()> {
+
self.write_u8(v as u8)
+
}
+
+
fn write_u16(&mut self, v: u16) -> Result<()> {
+
let bytes = if self.header.little_endian {
+
v.to_le_bytes()
+
} else {
+
v.to_be_bytes()
+
};
+
self.writer.write_all(&bytes).map_err(Error::IoError)?;
+
self.position += len::INT_16;
+
Ok(())
+
}
+
+
fn write_i16(&mut self, v: i16) -> Result<()> {
+
self.write_u16(v as u16)
+
}
+
+
fn write_u32(&mut self, v: u32) -> Result<()> {
+
let bytes = if self.header.little_endian {
+
v.to_le_bytes()
+
} else {
+
v.to_be_bytes()
+
};
+
self.writer.write_all(&bytes).map_err(Error::IoError)?;
+
self.position += len::INT_32;
+
Ok(())
+
}
+
+
fn write_i32(&mut self, v: i32) -> Result<()> {
+
self.write_u32(v as u32)
+
}
+
+
fn write_u64(&mut self, v: u64) -> Result<()> {
+
let bytes = if self.header.little_endian {
+
v.to_le_bytes()
+
} else {
+
v.to_be_bytes()
+
};
+
self.writer.write_all(&bytes).map_err(Error::IoError)?;
+
self.position += len::INT_64;
+
Ok(())
+
}
+
+
fn write_i64(&mut self, v: i64) -> Result<()> {
+
self.write_u64(v as u64)
+
}
+
+
fn write_f32(&mut self, v: f32) -> Result<()> {
+
self.write_u32(v.to_bits())
+
}
+
+
fn write_f64(&mut self, v: f64) -> Result<()> {
+
self.write_u64(v.to_bits())
+
}
+
+
fn write_bool(&mut self, v: bool) -> Result<()> {
+
self.write_u8(if v { 1 } else { 0 })
+
}
+
+
fn write_string(&mut self, s: &str) -> Result<()> {
+
self.write_u32(s.len() as u32)?;
+
self.writer
+
.write_all(s.as_bytes())
+
.map_err(Error::IoError)?;
+
self.position += s.len();
+
Ok(())
+
}
+
+
fn write_option(
+
&mut self,
+
kind: &TagKind,
+
value: Option<&Tag>,
+
) -> Result<()> {
+
self.write_tag_kind(*kind)?;
+
match value {
+
Some(tag) => {
+
self.write_bool(true)?;
+
self.write_tag_of_kind(tag)
+
}
+
None => self.write_bool(false),
+
}
+
}
+
+
fn write_list(&mut self, list: &[Tag]) -> Result<()> {
+
self.write_u32(list.len() as u32)?;
+
for tag in list {
+
self.write_tag(tag)?;
+
}
+
Ok(())
+
}
+
+
fn write_array(&mut self, kind: TagKind, array: &[Tag]) -> Result<()> {
+
self.write_u32(array.len() as u32)?;
+
self.write_tag_kind(kind)?;
+
for tag in array {
+
self.write_tag_of_kind(tag)?;
+
}
+
Ok(())
+
}
+
+
fn write_map(&mut self, map: &[(Tag, Tag)]) -> Result<()> {
+
self.write_u32(map.len() as u32)?;
+
for (key, value) in map {
+
self.write_tag(key)?;
+
self.write_tag(value)?;
+
}
+
Ok(())
+
}
+
+
fn write_timestamp(&mut self, dt: &DateTime<Utc>) -> Result<()> {
+
let timestamp = dt.timestamp_millis();
+
self.write_i64(timestamp)
+
}
+
+
fn write_uuid(&mut self, uuid: &Uuid) -> Result<()> {
+
self.writer
+
.write_all(uuid.as_bytes())
+
.map_err(Error::IoError)?;
+
self.position += len::UUID;
+
Ok(())
+
}
+
}
+
+
pub struct Writer<W: Write> {
+
inner: W,
+
raw_writer: RawWriter<std::io::Cursor<Vec<u8>>>,
+
compression: CompressionMethod,
+
}
+
+
impl<W: Write> Writer<W> {
+
pub fn new(writer: W, compression: CompressionMethod) -> Result<Self> {
+
let buffer = std::io::Cursor::new(Vec::new());
+
let raw_writer = RawWriter::new(buffer, CompressionMethod::None)?;
+
Ok(Self {
+
inner: writer,
+
raw_writer,
+
compression,
+
})
+
}
+
+
pub fn write_tag(&mut self, tag: &Tag) -> Result<()> {
+
self.raw_writer.write_tag(tag)
+
}
+
+
pub fn finish(mut self) -> Result<W> {
+
let uncompressed = self.raw_writer.writer.into_inner();
+
let compressed = match self.compression {
+
CompressionMethod::None => uncompressed.clone(),
+
CompressionMethod::Gzip => {
+
#[cfg(feature = "gzip")]
+
{
+
let mut encoder = flate2::GzBuilder::new()
+
.mtime(0)
+
.operating_system(0)
+
.write(Vec::new(), flate2::Compression::fast());
+
encoder.write_all(&uncompressed).map_err(Error::IoError)?;
+
encoder.finish().map_err(Error::IoError)?
+
}
+
#[cfg(not(feature = "gzip"))]
+
{
+
return Err(Error::UnsupportedCompression(
+
self.compression,
+
));
+
}
+
}
+
CompressionMethod::Zlib => {
+
#[cfg(feature = "gzip")]
+
{
+
let mut encoder = flate2::write::ZlibEncoder::new(
+
Vec::new(),
+
flate2::Compression::default(),
+
);
+
encoder.write_all(&uncompressed).map_err(Error::IoError)?;
+
encoder.finish().map_err(Error::IoError)?
+
}
+
#[cfg(not(feature = "gzip"))]
+
{
+
return Err(Error::UnsupportedCompression(
+
self.compression,
+
));
+
}
+
}
+
CompressionMethod::Lz4 => {
+
#[cfg(feature = "lz4")]
+
{
+
let mut encoder = lz4::EncoderBuilder::new()
+
.level(4)
+
.build(Vec::new())
+
.map_err(Error::IoError)?;
+
encoder.write_all(&uncompressed).map_err(Error::IoError)?;
+
let (compressed, _) = encoder.finish();
+
compressed
+
}
+
#[cfg(not(feature = "lz4"))]
+
{
+
return Err(Error::UnsupportedCompression(
+
self.compression,
+
));
+
}
+
}
+
};
+
+
let header = FileHeader {
+
version: FileHeader::CURRENT_VERSION,
+
little_endian: true,
+
compression: self.compression,
+
payload_length: compressed.len() as u32,
+
};
+
+
// write header
+
self.inner
+
.write_all(&FileHeader::MAGIC_BYTES)
+
.map_err(Error::IoError)?;
+
self.inner
+
.write_all(&[header.version])
+
.map_err(Error::IoError)?;
+
self.inner
+
.write_all(&[!header.little_endian as u8])
+
.map_err(Error::IoError)?;
+
self.inner
+
.write_all(&[header.compression as u8])
+
.map_err(Error::IoError)?;
+
let payload_bytes = if header.little_endian {
+
header.payload_length.to_le_bytes()
+
} else {
+
header.payload_length.to_be_bytes()
+
};
+
self.inner
+
.write_all(&payload_bytes)
+
.map_err(Error::IoError)?;
+
+
// write payload
+
self.inner.write_all(&compressed).map_err(Error::IoError)?;
+
+
Ok(self.inner)
+
}
+
}
tests/fixtures/hello_world.nbt2

This is a binary file and will not be displayed.

+16
tests/snapshots/test_io__read_hello_world-2.snap
···
···
+
---
+
source: tests/test_io.rs
+
expression: hello_world
+
---
+
Map(
+
[
+
(
+
String(
+
"message",
+
),
+
String(
+
"Hello World",
+
),
+
),
+
],
+
)
+10
tests/snapshots/test_io__read_hello_world.snap
···
···
+
---
+
source: tests/test_io.rs
+
expression: reader.file_header()
+
---
+
FileHeader {
+
version: 1,
+
little_endian: true,
+
compression: None,
+
payload_length: 33,
+
}
+91
tests/tags/mod.rs
···
···
+
use std::{f32, f64, i8, i16, u8, u16};
+
+
use chrono::DateTime;
+
use nbt2::{Tag, TagKind};
+
use uuid::Uuid;
+
+
pub const HELLO_WORLD_BYTES: &[u8] =
+
include_bytes!("../fixtures/hello_world.nbt2");
+
+
pub fn hello_world() -> Tag {
+
Tag::new_map(vec![(
+
Tag::new_string("message"),
+
Tag::new_string("Hello World"),
+
)])
+
}
+
+
pub fn bigtest() -> Tag {
+
Tag::new_map(vec![
+
(Tag::new_string("a u8"), Tag::new_u8(u8::MAX)),
+
(Tag::new_string("an i8"), Tag::new_i8(i8::MIN)),
+
(Tag::new_string("a u16"), Tag::new_u16(u16::MAX)),
+
(Tag::new_string("an i16"), Tag::new_i16(i16::MIN)),
+
(Tag::new_string("a u32"), Tag::new_u32(u32::MAX)),
+
(Tag::new_string("an i32"), Tag::new_i32(i32::MIN)),
+
(Tag::new_string("a u64"), Tag::new_u64(u64::MAX)),
+
(Tag::new_string("an i64"), Tag::new_i64(i64::MIN)),
+
(Tag::new_string("an f32"), Tag::new_f32(f32::consts::PI)),
+
(Tag::new_string("an f64"), Tag::new_f64(f64::consts::PI)),
+
(Tag::new_string("a boolean (true)"), Tag::new_bool(true)),
+
(Tag::new_string("a boolean (false)"), Tag::new_bool(false)),
+
(
+
Tag::new_string("a string"),
+
Tag::new_string("hello, world!"),
+
),
+
(
+
Tag::new_string("an optional value"),
+
Tag::new_option(TagKind::F64, Some(Tag::new_f64(f64::consts::TAU))),
+
),
+
(
+
Tag::new_string("a list"),
+
Tag::new_list(vec![
+
Tag::new_string("hello"),
+
Tag::new_u8(42),
+
Tag::new_bool(true),
+
]),
+
),
+
(
+
Tag::new_string("a map where keys are u8.. except one!"),
+
Tag::new_map(vec![
+
(Tag::new_u8(42), Tag::new_string("the meaning of life")),
+
(Tag::new_u8(69), Tag::new_string("the funny number")),
+
(
+
Tag::new_string("boo!"),
+
Tag::new_map(vec![(
+
Tag::new_string("is_scared"),
+
Tag::new_bool(true),
+
)]),
+
),
+
(Tag::new_u8(u8::MAX), Tag::new_string("the maximum")),
+
]),
+
),
+
(
+
Tag::new_string("an array of booleans"),
+
Tag::new_array(
+
TagKind::Bool,
+
vec![
+
Tag::new_bool(true),
+
Tag::new_bool(true),
+
Tag::new_bool(true),
+
Tag::new_bool(false),
+
Tag::new_bool(true),
+
Tag::new_bool(true),
+
Tag::new_bool(true),
+
],
+
),
+
),
+
(
+
Tag::new_string("the time im making this file"),
+
Tag::new_timestamp(
+
DateTime::from_timestamp_millis(1755598018230).unwrap(),
+
),
+
),
+
(
+
Tag::new_string("my minecraft account's uuid"),
+
Tag::new_uuid(
+
Uuid::parse_str("89c29c53-ef0c-4623-92cb-0f79930a97c2")
+
.unwrap(),
+
),
+
),
+
])
+
}
+61
tests/test_io.rs
···
···
+
mod tags;
+
+
use std::io::Cursor;
+
+
use insta::assert_debug_snapshot;
+
use nbt2::{CompressionMethod, reader::Reader, writer::Writer};
+
+
#[test]
+
fn test_read_hello_world() {
+
let mut reader = Reader::new(Cursor::new(tags::HELLO_WORLD_BYTES)).unwrap();
+
+
assert_debug_snapshot!(reader.file_header());
+
+
let hello_world = reader.read_tag().unwrap();
+
+
assert_debug_snapshot!(hello_world);
+
}
+
+
#[test]
+
fn test_write_hello_world() {
+
let bytes: Vec<u8> = {
+
let mut writer =
+
Writer::new(Vec::new(), CompressionMethod::None).unwrap();
+
writer.write_tag(&tags::hello_world()).unwrap();
+
writer.finish().unwrap()
+
};
+
+
assert_eq!(tags::HELLO_WORLD_BYTES, bytes.as_slice())
+
}
+
+
#[inline(always)]
+
fn test_round_trip_bt(compression: CompressionMethod) {
+
let bytes: Vec<u8> = {
+
let mut writer = Writer::new(Vec::new(), compression).unwrap();
+
writer.write_tag(&tags::bigtest()).unwrap();
+
writer.finish().unwrap()
+
};
+
let read_tag = Reader::new(Cursor::new(bytes)).unwrap().read_tag().unwrap();
+
+
assert_eq!(tags::bigtest(), read_tag, "compression: {:?}", compression);
+
}
+
+
#[test]
+
fn test_wr_round_trip_bt_none() {
+
test_round_trip_bt(CompressionMethod::None);
+
}
+
+
#[test]
+
fn test_wr_round_trip_bt_gzip() {
+
test_round_trip_bt(CompressionMethod::Gzip);
+
}
+
+
#[test]
+
fn test_wr_round_trip_bt_zlib() {
+
test_round_trip_bt(CompressionMethod::Zlib);
+
}
+
+
#[test]
+
fn test_wr_round_trip_bt_lz4() {
+
test_round_trip_bt(CompressionMethod::Lz4);
+
}