commit 8422307c70a7be027df1c3c44d2502ea6598f38f Author: caiowakamatsu Date: Sat Dec 13 02:41:19 2025 -0300 initial commit (compound / primitive - list) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..567609b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +build/ diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..e7dac4c --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 4.2.0) +project(nbtpp) + +set(CMAKE_CXX_STANDARD 23) + +add_subdirectory(source) +add_subdirectory(test) + diff --git a/source/CMakeLists.txt b/source/CMakeLists.txt new file mode 100644 index 0000000..f1fa456 --- /dev/null +++ b/source/CMakeLists.txt @@ -0,0 +1,6 @@ +add_library(nbtpp STATIC) +target_sources(nbtpp + PUBLIC + FILE_SET nbtpp_modules TYPE CXX_MODULES FILES + nbt.ixx +) diff --git a/source/nbt.ixx b/source/nbt.ixx new file mode 100644 index 0000000..1d1479b --- /dev/null +++ b/source/nbt.ixx @@ -0,0 +1,242 @@ +module; + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +export module nbtpp; + +namespace nbtpp { + +export enum class tag : std::uint8_t { + end = 0, + i8 = 1, + i16 = 2, + i32 = 3, + i64 = 4, + f32 = 5, + f64 = 6, + i8_arr = 7, + string = 8, + list = 9, + compound = 10, + i32_arr = 11, + i64_arr = 12, +}; + +void verify(tag required, tag t) { + if (required != t) { + throw std::runtime_error("unexpected type on access"); + } +} + +template void verify_type(tag t) {} + +export struct node { + std::string_view name = {}; + tag type = tag::end; + + std::uint32_t next = std::numeric_limits::max(); + +private: + [[nodiscard]] const std::byte *get_data() const { + return reinterpret_cast(name.data() + name.size()); + } + + template [[nodiscard]] T read() const { + verify_type(type); + auto value = T(); + std::memcpy(&value, get_data(), sizeof(T)); + return std::byteswap(value); + } + + template [[nodiscard]] T read() const { + verify_type(type); + auto data = std::array(); + std::memcpy(data.data(), get_data(), sizeof(T)); + std::reverse(data.begin(), data.end()); + return std::bit_cast(data); + } + +public: + template [[nodiscard]] T as() const; + + [[nodiscard]] std::size_t advance_size() const { + using enum tag; + switch (type) { + case i8: + return 1; + case i16: + return 2; + case i32: + return 4; + case i64: + return 8; + case f32: + return 4; + case f64: + return 8; + case string: { + const auto size = read(); + return size + sizeof(std::uint16_t); + } + case i8_arr: + case i32_arr: + case i64_arr: { + const auto size = read(); + return size + sizeof(std::int32_t); + }; + default: + throw std::runtime_error("not expected to work"); + } + } +}; + +template <> +std::vector node::as>() const { + auto length = std::int32_t(); + std::memcpy(&length, get_data(), 4); + length = std::byteswap(length); + + auto list = std::vector(length); + std::memcpy(list.data(), get_data() + 4, length); + return list; +} + +template <> std::int8_t node::as() const { + return read(); +} + +template <> std::int16_t node::as() const { + return read(); +} + +template <> std::int32_t node::as() const { + return read(); +} + +template <> std::int64_t node::as() const { + return read(); +} + +template <> std::string node::as() const { + const auto length = read(); + auto str = std::string(length, ' '); + std::memcpy(str.data(), get_data() + 2, length); + return str; +} + +template <> float node::as() const { return read(); } + +template <> double node::as() const { return read(); } + +template <> void verify_type(tag t) { verify(tag::i8, t); } + +template <> void verify_type(tag t) { verify(tag::i16, t); } + +template <> void verify_type(tag t) { verify(tag::i32, t); } + +template <> void verify_type(tag t) { verify(tag::i64, t); } + +template <> void verify_type(tag t) { verify(tag::f32, t); } + +template <> void verify_type(tag t) { verify(tag::f64, t); } + +template <> void verify_type>(tag t) { + verify(tag::i8_arr, t); +} + +template <> void verify_type(tag t) { + verify(tag::string, t); +} + +template <> void verify_type>(tag t) { verify(tag::list, t); } + +template <> void verify_type>(tag t) { + verify(tag::i32_arr, t); +} + +template <> void verify_type>(tag t) { + verify(tag::i64_arr, t); +} + +void parse_node_header(std::span nodes, std::int64_t &cursor, + const std::byte *bytes) { + nodes[cursor].type = std::bit_cast(*bytes); + + if (nodes[cursor].type == tag::end) { + auto name_length = std::uint16_t(); + std::memcpy(&name_length, bytes + 1, sizeof(std::uint16_t)); + name_length = std::byteswap(name_length); + nodes[cursor].name = std::string_view( + reinterpret_cast(bytes + 3), name_length); + } +} + +export [[nodiscard]] std::vector parse(std::span nbt) { + const auto node_count = nbt.size() / 4; + auto nodes = std::make_unique(node_count); + auto span = std::span(nodes.get(), nodes.get() + node_count); + + auto cursor = std::int64_t(); + auto node_index = std::int64_t(0); + + const auto read_name = [&] -> std::string_view { + auto name_length = std::uint16_t(); + std::memcpy(&name_length, &nbt[cursor], sizeof(std::uint16_t)); + cursor += sizeof(std::uint16_t); + name_length = std::byteswap(name_length); + + const auto string_start = reinterpret_cast(&nbt[cursor]); + cursor += name_length; + + return std::string_view(string_start, name_length); + }; + + auto stack = std::array(); + auto stack_ptr = std::int16_t(); + + do { + const auto node_type = std::bit_cast(nbt[cursor++]); + if (node_type == tag::end) { + const auto parent_index = stack[stack_ptr--]; + nodes[parent_index].next = node_index; + continue; + } + + const auto node_name = read_name(); + + if (node_type == tag::compound) { + stack[stack_ptr++] = node_index; + nodes[node_index++] = { + .name = node_name, + .type = node_type, + }; + continue; + } else { + // Primitive node + nodes[node_index++] = { + .name = node_name, + .type = node_type, + }; + const auto size = nodes[node_index - 1].advance_size(); + std::println("advancing by {} for node {}", size, node_name); + cursor += size; + } + + } while (stack_ptr > 0); + + return {nodes.get(), nodes.get() + node_index}; +} + +} // namespace nbtpp diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 0000000..9e5b021 --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,13 @@ +include(FetchContent) + +FetchContent_Declare( + Catch2 + GIT_REPOSITORY https://github.com/catchorg/Catch2.git + GIT_TAG v3.8.1 +) + +FetchContent_MakeAvailable(Catch2) + +add_executable(tests test.cpp) +target_link_libraries(tests PRIVATE Catch2::Catch2WithMain nbtpp) + diff --git a/test/data/bigtest.nbt b/test/data/bigtest.nbt new file mode 100644 index 0000000..a2021df Binary files /dev/null and b/test/data/bigtest.nbt differ diff --git a/test/data/hello_world.nbt b/test/data/hello_world.nbt new file mode 100644 index 0000000..f3f5e21 Binary files /dev/null and b/test/data/hello_world.nbt differ diff --git a/test/data/random_nbt.nbt b/test/data/random_nbt.nbt new file mode 100644 index 0000000..29c626b Binary files /dev/null and b/test/data/random_nbt.nbt differ diff --git a/test/test.cpp b/test/test.cpp new file mode 100644 index 0000000..caf4785 --- /dev/null +++ b/test/test.cpp @@ -0,0 +1,34 @@ +#include +#include +#include + +import nbtpp; + +[[nodiscard]] std::vector load_binary(std::filesystem::path path) { + auto stream = std::ifstream(path, std::ios::binary); + + const auto file_size = std::filesystem::file_size(path); + + auto buffer = std::vector(file_size); + stream.read(reinterpret_cast(buffer.data()), file_size); + + return buffer; +} + +TEST_CASE("hello world", "[nbt.read]") { + const auto data = ::load_binary("test/data/hello_world.nbt"); + const auto nodes = nbtpp::parse(data); + REQUIRE(nodes.size() == 2); + REQUIRE(nodes[0].type == nbtpp::tag::compound); + REQUIRE(nodes[0].name == "hello world"); + REQUIRE(nodes[1].type == nbtpp::tag::string); + REQUIRE(nodes[1].name == "name"); + REQUIRE(nodes[1].as() == "Bananrama"); +} + +/* + TAG_Compound('hello world'): 1 entry + { + TAG_String('name'): 'Bananrama' + } + */