initial commit (compound / primitive - list)

This commit is contained in:
2025-12-13 02:41:19 -03:00
commit 8422307c70
9 changed files with 304 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
build/

8
CMakeLists.txt Normal file
View File

@@ -0,0 +1,8 @@
cmake_minimum_required(VERSION 4.2.0)
project(nbtpp)
set(CMAKE_CXX_STANDARD 23)
add_subdirectory(source)
add_subdirectory(test)

6
source/CMakeLists.txt Normal file
View File

@@ -0,0 +1,6 @@
add_library(nbtpp STATIC)
target_sources(nbtpp
PUBLIC
FILE_SET nbtpp_modules TYPE CXX_MODULES FILES
nbt.ixx
)

242
source/nbt.ixx Normal file
View File

@@ -0,0 +1,242 @@
module;
#include <algorithm>
#include <bit>
#include <concepts>
#include <cstddef>
#include <cstdint>
#include <cstring>
#include <limits>
#include <memory>
#include <print>
#include <span>
#include <stack>
#include <string_view>
#include <vector>
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 <typename T> void verify_type(tag t) {}
export struct node {
std::string_view name = {};
tag type = tag::end;
std::uint32_t next = std::numeric_limits<std::uint32_t>::max();
private:
[[nodiscard]] const std::byte *get_data() const {
return reinterpret_cast<const std::byte *>(name.data() + name.size());
}
template <std::integral T> [[nodiscard]] T read() const {
verify_type<T>(type);
auto value = T();
std::memcpy(&value, get_data(), sizeof(T));
return std::byteswap(value);
}
template <std::floating_point T> [[nodiscard]] T read() const {
verify_type<T>(type);
auto data = std::array<std::byte, sizeof(T)>();
std::memcpy(data.data(), get_data(), sizeof(T));
std::reverse(data.begin(), data.end());
return std::bit_cast<T>(data);
}
public:
template <typename T> [[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<std::uint16_t>();
return size + sizeof(std::uint16_t);
}
case i8_arr:
case i32_arr:
case i64_arr: {
const auto size = read<std::int32_t>();
return size + sizeof(std::int32_t);
};
default:
throw std::runtime_error("not expected to work");
}
}
};
template <>
std::vector<std::int8_t> node::as<std::vector<std::int8_t>>() const {
auto length = std::int32_t();
std::memcpy(&length, get_data(), 4);
length = std::byteswap(length);
auto list = std::vector<std::int8_t>(length);
std::memcpy(list.data(), get_data() + 4, length);
return list;
}
template <> std::int8_t node::as<std::int8_t>() const {
return read<std::int8_t>();
}
template <> std::int16_t node::as<std::int16_t>() const {
return read<std::int16_t>();
}
template <> std::int32_t node::as<std::int32_t>() const {
return read<std::int32_t>();
}
template <> std::int64_t node::as<std::int64_t>() const {
return read<std::int64_t>();
}
template <> std::string node::as<std::string>() const {
const auto length = read<std::uint16_t>();
auto str = std::string(length, ' ');
std::memcpy(str.data(), get_data() + 2, length);
return str;
}
template <> float node::as<float>() const { return read<float>(); }
template <> double node::as<double>() const { return read<double>(); }
template <> void verify_type<std::int8_t>(tag t) { verify(tag::i8, t); }
template <> void verify_type<std::int16_t>(tag t) { verify(tag::i16, t); }
template <> void verify_type<std::int32_t>(tag t) { verify(tag::i32, t); }
template <> void verify_type<std::int64_t>(tag t) { verify(tag::i64, t); }
template <> void verify_type<float>(tag t) { verify(tag::f32, t); }
template <> void verify_type<double>(tag t) { verify(tag::f64, t); }
template <> void verify_type<std::vector<std::int8_t>>(tag t) {
verify(tag::i8_arr, t);
}
template <> void verify_type<std::string_view>(tag t) {
verify(tag::string, t);
}
template <> void verify_type<std::vector<node>>(tag t) { verify(tag::list, t); }
template <> void verify_type<std::vector<std::int32_t>>(tag t) {
verify(tag::i32_arr, t);
}
template <> void verify_type<std::vector<std::int64_t>>(tag t) {
verify(tag::i64_arr, t);
}
void parse_node_header(std::span<node> nodes, std::int64_t &cursor,
const std::byte *bytes) {
nodes[cursor].type = std::bit_cast<tag>(*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<const char *>(bytes + 3), name_length);
}
}
export [[nodiscard]] std::vector<node> parse(std::span<const std::byte> nbt) {
const auto node_count = nbt.size() / 4;
auto nodes = std::make_unique<node[]>(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<const char *>(&nbt[cursor]);
cursor += name_length;
return std::string_view(string_start, name_length);
};
auto stack = std::array<std::uint32_t, 128>();
auto stack_ptr = std::int16_t();
do {
const auto node_type = std::bit_cast<tag>(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

13
test/CMakeLists.txt Normal file
View File

@@ -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)

BIN
test/data/bigtest.nbt Normal file

Binary file not shown.

BIN
test/data/hello_world.nbt Normal file

Binary file not shown.

BIN
test/data/random_nbt.nbt Normal file

Binary file not shown.

34
test/test.cpp Normal file
View File

@@ -0,0 +1,34 @@
#include <catch2/catch_all.hpp>
#include <filesystem>
#include <fstream>
import nbtpp;
[[nodiscard]] std::vector<std::byte> 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<std::byte>(file_size);
stream.read(reinterpret_cast<char *>(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<std::string>() == "Bananrama");
}
/*
TAG_Compound('hello world'): 1 entry
{
TAG_String('name'): 'Bananrama'
}
*/