Files
SweepStore/cpp/binary_table.h
ImBenji 6ed74b6efb stuff
2025-09-18 03:02:14 +01:00

1119 lines
37 KiB
C++

#include <algorithm>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <memory>
#include <optional>
#include <sstream>
#include <string>
#include <unordered_map>
#include <utility>
#include <variant>
#include <vector>
//
// SweepStore (formerly Binary Table) - C++ port
// © 2025-26 by Benjamin Watt of IMBENJI.NET LIMITED - All rights reserved.
// MIT License
//
// Utility endian enum
enum class Endian { Little, Big };
// Forward declarations
struct BT_Pointer;
class BinaryTable;
class BT_UniformArray;
// Type identifiers matching Dart enum ordinals
enum class BT_TypeId : uint8_t {
POINTER = 0,
ADDRESS_TABLE = 1,
INTEGER = 2,
FLOAT = 3,
STRING = 4,
INTEGER_ARRAY = 5,
FLOAT_ARRAY = 6
};
// Type metadata
struct BT_TypeInfo {
int size; // Size in bytes, -1 = variable
bool arrayType; // Is this an array type
};
inline const BT_TypeInfo& typeInfo(BT_TypeId t) {
static const BT_TypeInfo kInfo[] = {
/* POINTER */ {8, false},
/* ADDRESS_TABLE */ {-1, false},
/* INTEGER */ {4, false},
/* FLOAT */ {4, false},
/* STRING */ {-1, false},
/* INTEGER_ARRAY */ {-1, true},
/* FLOAT_ARRAY */ {-1, true},
};
return kInfo[static_cast<size_t>(t)];
}
inline BT_TypeId typeFromId(uint8_t id) {
if (id > static_cast<uint8_t>(BT_TypeId::FLOAT_ARRAY)) {
throw std::invalid_argument("Invalid BT_Type id");
}
return static_cast<BT_TypeId>(id);
}
// Pointer wrapper
struct BT_Pointer {
int64_t address{-1};
BT_Pointer() = default;
explicit BT_Pointer(int64_t addr) : address(addr) {}
bool isNull() const { return address == -1; }
bool operator==(const BT_Pointer& other) const { return address == other.address; }
bool operator!=(const BT_Pointer& other) const { return !(*this == other); }
std::string toString() const {
std::ostringstream oss;
oss << "0x" << std::hex << std::uppercase << address << " (" << std::dec << address << ")";
return oss.str();
}
};
static const BT_Pointer BT_Null{-1};
// Random access file wrapper
class RandomAccessFile {
public:
explicit RandomAccessFile(const std::string& path)
: path_(path) {
open();
}
void setPosition(int64_t pos) {
fs_.flush();
fs_.clear();
fs_.seekg(pos, std::ios::beg);
fs_.seekp(pos, std::ios::beg);
}
int64_t length() const {
return static_cast<int64_t>(std::filesystem::file_size(path_));
}
void truncate(int64_t new_length) {
fs_.flush();
fs_.close();
std::filesystem::resize_file(path_, static_cast<uintmax_t>(new_length));
open();
}
uint8_t readByte() {
char c = 0;
fs_.read(&c, 1);
if (!fs_) throw std::runtime_error("readByte failed");
return static_cast<uint8_t>(c);
}
std::vector<uint8_t> read(size_t n) {
std::vector<uint8_t> buf(n);
fs_.read(reinterpret_cast<char*>(buf.data()), static_cast<std::streamsize>(n));
if (!fs_) throw std::runtime_error("read failed");
return buf;
}
void write(const std::vector<uint8_t>& data) {
fs_.write(reinterpret_cast<const char*>(data.data()), static_cast<std::streamsize>(data.size()));
if (!fs_) throw std::runtime_error("write failed");
fs_.flush();
}
// Read/Write Int dynamic
int64_t readInt(size_t size = 4, Endian endianness = Endian::Little) {
if (size < 1 || size > 8) throw std::invalid_argument("Size must be between 1 and 8 bytes");
auto bytes = read(size);
uint64_t u = 0;
if (endianness == Endian::Little) {
for (size_t i = 0; i < size; ++i) {
u |= static_cast<uint64_t>(bytes[i]) << (8 * i);
}
} else {
for (size_t i = 0; i < size; ++i) {
u = (u << 8) | bytes[i];
}
}
// Sign-extend based on MSB of the given size
uint64_t sign_bit = uint64_t{1} << (size * 8 - 1);
if (u & sign_bit) {
uint64_t mask = (~uint64_t{0}) << (size * 8);
u |= mask;
}
return static_cast<int64_t>(u);
}
void writeInt(int64_t value, size_t size = 4, Endian endianness = Endian::Little) {
if (size < 1 || size > 8) throw std::invalid_argument("Size must be between 1 and 8 bytes");
std::vector<uint8_t> bytes(size, 0);
if (endianness == Endian::Little) {
for (size_t i = 0; i < size; ++i) {
bytes[i] = static_cast<uint8_t>((static_cast<uint64_t>(value) >> (i * 8)) & 0xFF);
}
} else {
for (size_t i = 0; i < size; ++i) {
bytes[size - 1 - i] = static_cast<uint8_t>((static_cast<uint64_t>(value) >> (i * 8)) & 0xFF);
}
}
write(bytes);
}
// Read/Write Pointers
BT_Pointer readPointer() {
int64_t offset = readInt(typeInfo(BT_TypeId::POINTER).size);
return BT_Pointer(offset);
}
void writePointer(const BT_Pointer& pointer) {
writeInt(pointer.address, typeInfo(BT_TypeId::POINTER).size);
}
// Read/Write Float32
double readFloat32(Endian endianness = Endian::Little) {
auto bytes = read(4);
uint32_t u = 0;
if (endianness == Endian::Little) {
u = static_cast<uint32_t>(bytes[0]) |
(static_cast<uint32_t>(bytes[1]) << 8) |
(static_cast<uint32_t>(bytes[2]) << 16) |
(static_cast<uint32_t>(bytes[3]) << 24);
} else {
u = static_cast<uint32_t>(bytes[3]) |
(static_cast<uint32_t>(bytes[2]) << 8) |
(static_cast<uint32_t>(bytes[1]) << 16) |
(static_cast<uint32_t>(bytes[0]) << 24);
}
float f;
std::memcpy(&f, &u, sizeof(f));
return static_cast<double>(f);
}
void writeFloat32(double value, Endian endianness = Endian::Little) {
float f = static_cast<float>(value);
uint32_t u;
std::memcpy(&u, &f, sizeof(u));
std::vector<uint8_t> bytes(4);
if (endianness == Endian::Little) {
bytes[0] = static_cast<uint8_t>(u & 0xFF);
bytes[1] = static_cast<uint8_t>((u >> 8) & 0xFF);
bytes[2] = static_cast<uint8_t>((u >> 16) & 0xFF);
bytes[3] = static_cast<uint8_t>((u >> 24) & 0xFF);
} else {
bytes[3] = static_cast<uint8_t>(u & 0xFF);
bytes[2] = static_cast<uint8_t>((u >> 8) & 0xFF);
bytes[1] = static_cast<uint8_t>((u >> 16) & 0xFF);
bytes[0] = static_cast<uint8_t>((u >> 24) & 0xFF);
}
write(bytes);
}
// Read/Write Float64
double readFloat64(Endian endianness = Endian::Little) {
auto bytes = read(8);
uint64_t u = 0;
if (endianness == Endian::Little) {
for (int i = 0; i < 8; ++i) u |= static_cast<uint64_t>(bytes[i]) << (8 * i);
} else {
for (int i = 0; i < 8; ++i) u = (u << 8) | bytes[i];
}
double d;
std::memcpy(&d, &u, sizeof(d));
return d;
}
void writeFloat64(double value, Endian endianness = Endian::Little) {
uint64_t u;
std::memcpy(&u, &value, sizeof(u));
std::vector<uint8_t> bytes(8);
if (endianness == Endian::Little) {
for (int i = 0; i < 8; ++i) bytes[i] = static_cast<uint8_t>((u >> (8 * i)) & 0xFF);
} else {
for (int i = 0; i < 8; ++i) bytes[7 - i] = static_cast<uint8_t>((u >> (8 * i)) & 0xFF);
}
write(bytes);
}
private:
std::string path_;
std::fstream fs_;
void open() {
// Ensure file exists
if (!std::filesystem::exists(path_)) {
std::ofstream create(path_, std::ios::binary);
create.close();
}
fs_.open(path_, std::ios::binary | std::ios::in | std::ios::out);
if (!fs_) throw std::runtime_error("Failed to open file: " + path_);
}
};
// Helpers to append ints to buffers
inline void appendIntLE(std::vector<uint8_t>& buf, int64_t value, size_t size) {
for (size_t i = 0; i < size; ++i) {
buf.push_back(static_cast<uint8_t>((static_cast<uint64_t>(value) >> (8 * i)) & 0xFF));
}
}
// Value input types acceptable to encodeValue
using ValueInput = std::variant<int32_t, double, std::string, std::vector<int32_t>, std::vector<double>>;
// Forward: encodeValue
std::vector<uint8_t> encodeValue(const ValueInput& value);
// fromDynamic equivalent for ValueInput
inline BT_TypeId typeFromDynamic(const ValueInput& v) {
return std::visit([](auto&& arg) -> BT_TypeId {
using T = std::decay_t<decltype(arg)>;
if constexpr (std::is_same_v<T, int32_t>) {
return BT_TypeId::INTEGER;
} else if constexpr (std::is_same_v<T, double>) {
return BT_TypeId::FLOAT;
} else if constexpr (std::is_same_v<T, std::string>) {
return BT_TypeId::STRING;
} else if constexpr (std::is_same_v<T, std::vector<int32_t>>) {
return BT_TypeId::INTEGER_ARRAY;
} else if constexpr (std::is_same_v<T, std::vector<double>>) {
return BT_TypeId::FLOAT_ARRAY;
} else {
throw std::invalid_argument("Unsupported type");
}
}, v);
}
// Encode a value into bytes [type byte][payload ...]
std::vector<uint8_t> encodeValue(const ValueInput& value) {
std::vector<uint8_t> buf;
BT_TypeId valueType = typeFromDynamic(value);
buf.push_back(static_cast<uint8_t>(valueType));
switch (valueType) {
case BT_TypeId::INTEGER: {
int32_t v = std::get<int32_t>(value);
appendIntLE(buf, v, 4);
break;
}
case BT_TypeId::FLOAT: {
// Store as Float32
float f = static_cast<float>(std::get<double>(value));
uint32_t u;
std::memcpy(&u, &f, sizeof(u));
appendIntLE(buf, static_cast<int64_t>(u), 4);
break;
}
case BT_TypeId::STRING: {
const std::string& s = std::get<std::string>(value);
// Note: Dart used codeUnits; here we write raw bytes of the string (assumed UTF-8) and length = bytes length
appendIntLE(buf, static_cast<int32_t>(s.size()), 4);
buf.insert(buf.end(), s.begin(), s.end());
break;
}
case BT_TypeId::INTEGER_ARRAY: {
const auto& list = std::get<std::vector<int32_t>>(value);
appendIntLE(buf, static_cast<int32_t>(list.size()), 4);
for (auto& item : list) {
ValueInput vi = static_cast<int32_t>(item);
auto enc = encodeValue(vi);
buf.insert(buf.end(), enc.begin(), enc.end());
}
break;
}
case BT_TypeId::FLOAT_ARRAY: {
const auto& list = std::get<std::vector<double>>(value);
appendIntLE(buf, static_cast<int32_t>(list.size()), 4);
for (auto& item : list) {
ValueInput vi = static_cast<double>(item);
auto enc = encodeValue(vi);
buf.insert(buf.end(), enc.begin(), enc.end());
}
break;
}
default:
throw std::invalid_argument("Unsupported type for encodeValue");
}
return buf;
}
// Free list entry
struct BT_FreeListEntry {
BT_Pointer pointer;
int32_t size;
};
// Utility on vector<BT_FreeListEntry>
inline void removePointer(std::vector<BT_FreeListEntry>& list, const BT_Pointer& p) {
list.erase(std::remove_if(list.begin(), list.end(),
[&](const BT_FreeListEntry& e) { return e.pointer == p; }),
list.end());
}
// Encode free list: [entries...] [count (4 bytes)]
inline std::vector<uint8_t> bt_encode(const std::vector<BT_FreeListEntry>& list) {
std::vector<uint8_t> buf;
for (const auto& e : list) {
// Pointer (8 bytes)
appendIntLE(buf, e.pointer.address, typeInfo(BT_TypeId::POINTER).size);
// Size (4 bytes)
appendIntLE(buf, e.size, 4);
}
// Entry count (4 bytes)
appendIntLE(buf, static_cast<int32_t>(list.size()), 4);
return buf;
}
// 64-bit FNV-1a hash for strings
inline int64_t bt_hash(const std::string& s) {
uint64_t hash = 0xcbf29ce484222325ULL;
for (unsigned char c : s) {
hash ^= c;
hash *= 0x100000001b3ULL;
}
return static_cast<int64_t>(hash);
}
// A variant for decoding results (including arrays by handle)
using DecodedValue = std::variant<std::monostate, int32_t, double, std::string, BT_Pointer, std::shared_ptr<BT_UniformArray>>;
// Reference to a value at a file pointer
class BT_Reference {
public:
BT_Reference(BinaryTable* table, BT_Pointer pointer)
: table_(table), pointer_(pointer) {}
DecodedValue decodeValue();
// Size in bytes of the stored value (minimal reads)
virtual int32_t size();
const BT_Pointer& pointer() const { return pointer_; }
std::string toString() const { return pointer_.toString(); }
protected:
BinaryTable* table_;
BT_Pointer pointer_;
friend class BT_UniformArray;
friend class BinaryTable;
};
// Uniform array wrapper (random access)
class BT_UniformArray : public BT_Reference {
public:
BT_UniformArray(BinaryTable* table, BT_Pointer pointer)
: BT_Reference(table, pointer) {}
int32_t length();
DecodedValue get(int index);
void set(int index, const ValueInput& value);
void add(const ValueInput& value) { addAll(std::vector<ValueInput>{value}); }
void addAll(const std::vector<ValueInput>& values);
BT_TypeId elementType() const;
int32_t size() override; // total bytes used
std::string toString(bool readValues = false);
private:
BT_TypeId elementTypeOrThrow() const;
};
// Binary Table
class BinaryTable {
public:
explicit BinaryTable(const std::string& path)
: file_(path) {}
void initialise() {
file_.setPosition(0);
file_.writePointer(BT_Null); // Address table pointer
file_.writeInt(0, 4); // Free list entry count
}
// Set key = value
void set(const std::string& key, const ValueInput& value) {
antiFreeListScope([&]() {
auto addressTable = getAddressTable();
int64_t keyHash = bt_hash(key);
if (addressTable.find(keyHash) != addressTable.end()) {
throw std::runtime_error("Key already exists");
}
auto valueBuffer = encodeValue(value);
// Allocate and write value
BT_Pointer valueAddress = alloc(static_cast<int32_t>(valueBuffer.size()));
file_.setPosition(valueAddress.address);
file_.write(valueBuffer);
// Update address table
addressTable[keyHash] = valueAddress;
setAddressTable(addressTable);
});
}
// Get decoded value
DecodedValue get(const std::string& key) {
auto addressTable = getAddressTable();
int64_t keyHashV = bt_hash(key);
auto it = addressTable.find(keyHashV);
if (it == addressTable.end()) throw std::runtime_error("Key does not exist");
BT_Pointer valuePtr = it->second;
BT_Reference ref(this, valuePtr);
return ref.decodeValue();
}
void erase(const std::string& key) {
antiFreeListScope([&]() {
auto addressTable = getAddressTable();
int64_t keyHashV = bt_hash(key);
auto it = addressTable.find(keyHashV);
if (it == addressTable.end()) throw std::runtime_error("Key does not exist");
BT_Pointer valuePointer = it->second;
BT_Reference valueRef(this, valuePointer);
// Free the value
free(valuePointer, valueRef.size());
// Remove from address table
addressTable.erase(keyHashV);
setAddressTable(addressTable);
});
}
// Try to truncate file by reclaiming trailing free block
void truncate() {
antiFreeListScope([&]() {
// Relocate the address table if possible
setAddressTable(getAddressTable());
auto freeList = getFreeList();
std::sort(freeList.begin(), freeList.end(),
[](const BT_FreeListEntry& a, const BT_FreeListEntry& b) { return a.pointer.address < b.pointer.address; });
if (freeList.empty()) return;
auto lastEntry = freeList.back();
int64_t fileEnd = file_.length();
int64_t expectedEnd = lastEntry.pointer.address + lastEntry.size;
if (expectedEnd != fileEnd) return;
// Remove the last entry and update free list
freeList.pop_back();
setFreeList(freeList);
// Truncate file
int64_t newLength = lastEntry.pointer.address;
file_.truncate(newLength);
});
}
// Internals used by BT_Reference / BT_UniformArray
RandomAccessFile& file() { return file_; }
void antiFreeListScope(const std::function<void()>& fn) {
liftFreeList();
try {
fn();
dropFreeList();
} catch (...) {
try {
dropFreeList();
} catch (...) {
// swallow to not throw during unwinding
}
throw;
}
}
// Free memory region: adds to free list and merges contiguous
void free(BT_Pointer pointer, int32_t size) {
if (!freeListLifted_) throw std::runtime_error("Free list must be lifted before freeing memory");
if (pointer.isNull() || size <= 0) throw std::invalid_argument("Cannot free null pointer or zero size");
auto freeList = getFreeList();
freeList.push_back(BT_FreeListEntry{pointer, size});
// Merge contiguous blocks
if (!freeList.empty()) {
std::sort(freeList.begin(), freeList.end(),
[](const BT_FreeListEntry& a, const BT_FreeListEntry& b) { return a.pointer.address < b.pointer.address; });
std::vector<BT_FreeListEntry> merged;
for (const auto& e : freeList) {
if (merged.empty()) {
merged.push_back(e);
} else {
auto& last = merged.back();
if (last.pointer.address + last.size == e.pointer.address) {
last.size += e.size;
} else {
merged.push_back(e);
}
}
}
setFreeList(merged);
} else {
setFreeList(freeList);
}
}
// Allocate memory region
BT_Pointer alloc(int32_t size) {
if (!freeListLifted_) throw std::runtime_error("Free list must be lifted before allocation");
auto freeList = getFreeList();
// No free blocks: allocate at end
if (freeList.empty()) {
return BT_Pointer(file_.length());
}
// First-fit block
std::optional<BT_FreeListEntry> bestFit;
for (const auto& entry : freeList) {
if (entry.size >= size) {
bestFit = entry;
break;
}
}
if (!bestFit.has_value()) {
return BT_Pointer(file_.length());
}
bool exactFit = bestFit->size == size;
if (exactFit) {
BT_Pointer allocated = bestFit->pointer;
removePointer(freeList, allocated);
setFreeList(freeList);
return allocated;
} else {
BT_Pointer allocated = bestFit->pointer;
BT_FreeListEntry remainder{BT_Pointer(bestFit->pointer.address + size), bestFit->size - size};
removePointer(freeList, allocated);
freeList.push_back(remainder);
setFreeList(freeList);
return allocated;
}
}
// Address table getters/setters
std::unordered_map<int64_t, BT_Pointer> getAddressTable() {
file_.setPosition(0);
BT_Reference tableRef(this, file_.readPointer());
if (tableRef.pointer_.isNull()) {
return {};
}
// Skip type byte (ADDRESS_TABLE)
file_.setPosition(tableRef.pointer_.address + 1);
int32_t tableCount = static_cast<int32_t>(file_.readInt(4));
std::unordered_map<int64_t, BT_Pointer> map;
for (int32_t i = 0; i < tableCount; ++i) {
int64_t keyHash = file_.readInt(8);
int64_t valueAddr = file_.readInt(typeInfo(BT_TypeId::POINTER).size);
map[keyHash] = BT_Pointer(valueAddr);
}
return map;
}
void setAddressTable(const std::unordered_map<int64_t, BT_Pointer>& table) {
// Build buffer
std::vector<uint8_t> buf;
buf.push_back(static_cast<uint8_t>(BT_TypeId::ADDRESS_TABLE));
appendIntLE(buf, static_cast<int32_t>(table.size()), 4);
for (const auto& kv : table) {
appendIntLE(buf, kv.first, 8);
appendIntLE(buf, kv.second.address, typeInfo(BT_TypeId::POINTER).size);
}
// Write new address table at end
BT_Pointer tableAddress = alloc(static_cast<int32_t>(buf.size()));
file_.setPosition(tableAddress.address);
file_.write(buf);
// Read old table pointer before updating
file_.setPosition(0);
BT_Reference oldTableRef(this, file_.readPointer());
// Update header to point to new table
file_.setPosition(0);
file_.writePointer(tableAddress);
// Free old table if exists and isn't same
if (!oldTableRef.pointer_.isNull() && oldTableRef.pointer_ != tableAddress) {
free(oldTableRef.pointer_, oldTableRef.size());
}
}
// Free list load/store
std::vector<BT_FreeListEntry> getFreeList() {
if (freeListLifted_) {
return freeListCache_.value_or(std::vector<BT_FreeListEntry>{});
}
if (file_.length() < 4) return {};
file_.setPosition(file_.length() - 4);
int32_t entryCount = static_cast<int32_t>(file_.readInt(4));
if (entryCount == 0) return {};
int32_t entrySize = typeInfo(BT_TypeId::POINTER).size + 4;
int64_t freeListSize = static_cast<int64_t>(entryCount) * entrySize;
file_.setPosition(file_.length() - 4 - freeListSize);
std::vector<BT_FreeListEntry> list;
list.reserve(entryCount);
for (int32_t i = 0; i < entryCount; ++i) {
int64_t ptrAddr = file_.readInt(typeInfo(BT_TypeId::POINTER).size);
int32_t sz = static_cast<int32_t>(file_.readInt(4));
list.push_back(BT_FreeListEntry{BT_Pointer(ptrAddr), sz});
}
return list;
}
void setFreeList(const std::vector<BT_FreeListEntry>& list) {
if (freeListLifted_) {
freeListCache_ = list;
return;
}
// Remove old free list
file_.setPosition(file_.length() - 4);
int32_t oldEntryCount = static_cast<int32_t>(file_.readInt(4));
int64_t oldListSize = static_cast<int64_t>(oldEntryCount) * (typeInfo(BT_TypeId::POINTER).size + 4) + 4;
file_.truncate(file_.length() - oldListSize);
// Append new free list
auto buf = bt_encode(list);
file_.setPosition(file_.length());
file_.write(buf);
}
void liftFreeList() {
if (freeListLifted_) throw std::runtime_error("Free list is already lifted");
freeListCache_ = getFreeList();
// Remove it from the file
file_.setPosition(file_.length() - 4);
int32_t oldEntryCount = static_cast<int32_t>(file_.readInt(4));
int32_t oldEntrySize = typeInfo(BT_TypeId::POINTER).size + 4;
int64_t oldFreeListSize = static_cast<int64_t>(oldEntryCount) * oldEntrySize + 4;
file_.truncate(file_.length() - oldFreeListSize);
freeListLifted_ = true;
}
void dropFreeList() {
if (!freeListLifted_) throw std::runtime_error("Free list is not lifted");
// Write placeholder count
file_.setPosition(file_.length());
file_.writeInt(0, 4);
freeListLifted_ = false;
setFreeList(freeListCache_.value_or(std::vector<BT_FreeListEntry>{}));
freeListCache_.reset();
}
private:
RandomAccessFile file_;
bool freeListLifted_{false};
std::optional<std::vector<BT_FreeListEntry>> freeListCache_;
};
// BT_Reference implementations
DecodedValue BT_Reference::decodeValue() {
if (pointer_.isNull()) return std::monostate{};
table_->file().setPosition(pointer_.address);
uint8_t typeId = table_->file().readByte();
BT_TypeId type = typeFromId(typeId);
switch (type) {
case BT_TypeId::INTEGER: {
int32_t v = static_cast<int32_t>(table_->file().readInt(4));
return v;
}
case BT_TypeId::FLOAT: {
double v = table_->file().readFloat32();
return v;
}
case BT_TypeId::STRING: {
int32_t len = static_cast<int32_t>(table_->file().readInt(4));
auto bytes = table_->file().read(len);
return std::string(bytes.begin(), bytes.end());
}
case BT_TypeId::ADDRESS_TABLE: {
throw std::runtime_error("Address table decoding not implemented");
}
case BT_TypeId::POINTER: {
BT_Pointer p = table_->file().readPointer();
return p;
}
case BT_TypeId::INTEGER_ARRAY:
case BT_TypeId::FLOAT_ARRAY: {
return std::make_shared<BT_UniformArray>(table_, pointer_);
}
default:
throw std::runtime_error("Unsupported type");
}
}
int32_t BT_Reference::size() {
if (pointer_.isNull()) return 0;
table_->file().setPosition(pointer_.address);
BT_TypeId type = typeFromId(table_->file().readByte());
if (type == BT_TypeId::INTEGER) {
return 1 + 4;
} else if (type == BT_TypeId::FLOAT) {
return 1 + 4;
} else if (type == BT_TypeId::STRING) {
int32_t length = static_cast<int32_t>(table_->file().readInt(4));
return 1 + 4 + length;
} else if (type == BT_TypeId::ADDRESS_TABLE) {
int32_t count = static_cast<int32_t>(table_->file().readInt(4));
return 1 + 4 + count * (8 + typeInfo(BT_TypeId::POINTER).size);
} else {
throw std::runtime_error("Unsupported type for size()");
}
}
// BT_UniformArray implementations
int32_t BT_UniformArray::length() {
if (pointer_.isNull()) return 0;
table_->file().setPosition(pointer_.address);
BT_TypeId containerType = typeFromId(table_->file().readByte());
if (!typeInfo(containerType).arrayType) throw std::runtime_error("Not an array");
return static_cast<int32_t>(table_->file().readInt(4));
}
BT_TypeId BT_UniformArray::elementTypeOrThrow() const {
if (pointer_.isNull()) throw std::runtime_error("Null pointer");
// Read first element type
const_cast<BinaryTable*>(table_)->file().setPosition(pointer_.address + 1 + 4);
uint8_t typeId = const_cast<BinaryTable*>(table_)->file().readByte();
return typeFromId(typeId);
}
BT_TypeId BT_UniformArray::elementType() const {
if (length() == 0) {
// No elements; undefined
return BT_TypeId::INTEGER; // dummy, not used
}
return elementTypeOrThrow();
}
DecodedValue BT_UniformArray::get(int index) {
if (pointer_.isNull()) throw std::runtime_error("Null pointer");
int32_t len = length();
if (index < 0 || index >= len) throw std::out_of_range("Index out of range");
// Determine element type
table_->file().setPosition(pointer_.address + 1 + 4);
BT_TypeId type = typeFromId(table_->file().readByte());
int itemStride = 1 + typeInfo(type).size; // type byte + fixed data
BT_Reference itemRef(table_, BT_Pointer((pointer_.address + 1 + 4) + static_cast<int64_t>(index) * itemStride));
return itemRef.decodeValue();
}
void BT_UniformArray::set(int index, const ValueInput& value) {
if (pointer_.isNull()) throw std::runtime_error("Null pointer");
int32_t len = length();
if (index < 0 || index >= len) throw std::out_of_range("Index out of range");
// Determine element type
table_->file().setPosition(pointer_.address + 1 + 4);
BT_TypeId type = typeFromId(table_->file().readByte());
if (typeInfo(type).size == -1) {
throw std::runtime_error("Variable-size types not supported in uniform arrays.");
}
// Ensure new value type matches
BT_TypeId newValueType = typeFromDynamic(value);
if (newValueType != type) {
throw std::runtime_error("Type mismatch in BT_UniformArray::set()");
}
int itemStride = 1 + typeInfo(type).size; // header + payload
BT_Pointer itemPointer((pointer_.address + 1 + 4) + static_cast<int64_t>(index) * itemStride);
auto valueBuffer = encodeValue(value);
table_->file().setPosition(itemPointer.address);
table_->file().write(valueBuffer);
}
void BT_UniformArray::addAll(const std::vector<ValueInput>& values) {
if (values.empty()) return;
table_->antiFreeListScope([&]() {
// Determine type by existing element or new value's first element
BT_TypeId type;
if (length() > 0) {
table_->file().setPosition(pointer_.address + 1 + 4);
type = typeFromId(table_->file().readByte());
} else {
type = typeFromDynamic(values.front());
}
// Validate new values
for (size_t i = 0; i < values.size(); ++i) {
BT_TypeId t = typeFromDynamic(values[i]);
if (t != type) {
std::ostringstream oss;
oss << "Type mismatch at index " << i;
throw std::runtime_error(oss.str());
}
if (typeInfo(t).size == -1) {
throw std::runtime_error("Variable-size types not supported in uniform arrays.");
}
}
int32_t oldLen = length();
int itemStride = 1 + typeInfo(type).size;
int32_t oldBufferSize = 1 + 4 + oldLen * itemStride;
// Read full existing buffer
table_->file().setPosition(pointer_.address);
auto fullBuffer = table_->file().read(oldBufferSize);
// Append new values
for (const auto& v : values) {
auto enc = encodeValue(v);
fullBuffer.insert(fullBuffer.end(), enc.begin(), enc.end());
}
// Update length in buffer (little-endian at offset 1)
int32_t newLength = oldLen + static_cast<int32_t>(values.size());
fullBuffer[1] = static_cast<uint8_t>(newLength & 0xFF);
fullBuffer[2] = static_cast<uint8_t>((newLength >> 8) & 0xFF);
fullBuffer[3] = static_cast<uint8_t>((newLength >> 16) & 0xFF);
fullBuffer[4] = static_cast<uint8_t>((newLength >> 24) & 0xFF);
// Free old array
int32_t oldSizeBytes = this->size();
table_->free(pointer_, oldSizeBytes);
// Allocate new
BT_Pointer newPtr = table_->alloc(static_cast<int32_t>(fullBuffer.size()));
// Replace references in address table
auto addressTable = table_->getAddressTable();
for (auto& kv : addressTable) {
if (kv.second == pointer_) {
std::cout << "Updating address table entry for key " << kv.first
<< " from " << pointer_.toString() << " to " << newPtr.toString() << "\n";
kv.second = newPtr;
}
}
table_->setAddressTable(addressTable);
// Update this pointer
pointer_ = newPtr;
// Write new buffer
table_->file().setPosition(newPtr.address);
table_->file().write(fullBuffer);
std::cout << "Array resized to new length " << newLength << " at " << newPtr.toString() << "\n";
});
}
int32_t BT_UniformArray::size() {
int32_t len = length();
if (len == 0) return 1 + 4;
// Read element type
table_->file().setPosition(pointer_.address + 1 + 4);
BT_TypeId t = typeFromId(table_->file().readByte());
return 1 + 4 + len * (1 + typeInfo(t).size);
}
std::string BT_UniformArray::toString(bool readValues) {
std::ostringstream oss;
if (readValues) {
oss << "Uniform Array of length " << length();
return oss.str();
}
oss << "Uniform Array: [";
int32_t len = length();
for (int i = 0; i < len; ++i) {
auto v = get(i);
if (i > 0) oss << ", ";
if (std::holds_alternative<int32_t>(v)) {
oss << std::get<int32_t>(v);
} else if (std::holds_alternative<double>(v)) {
oss << std::get<double>(v);
} else if (std::holds_alternative<std::string>(v)) {
oss << "\"" << std::get<std::string>(v) << "\"";
} else if (std::holds_alternative<BT_Pointer>(v)) {
oss << std::get<BT_Pointer>(v).toString();
} else {
oss << "?";
}
}
oss << "]";
return oss.str();
}
// Binary dump for display
std::string binaryDump(const std::vector<uint8_t>& data) {
std::ostringstream buffer;
for (size_t i = 0; i < data.size(); i += 16) {
buffer << "0x" << std::uppercase << std::hex << std::setw(4) << std::setfill('0') << i
<< std::dec << " (" << std::setw(4) << std::setfill(' ') << i << ") | ";
// Hex bytes
for (int j = 0; j < 16; ++j) {
if (i + j < data.size()) {
buffer << std::uppercase << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(data[i + j]) << " ";
} else {
buffer << " ";
}
}
buffer << " | ";
// Integer representation
for (int j = 0; j < 16; ++j) {
if (i + j < data.size()) {
buffer << std::dec << std::setw(3) << std::setfill(' ') << static_cast<int>(data[i + j]) << " ";
} else {
buffer << " ";
}
}
buffer << " | ";
// ASCII representation
for (int j = 0; j < 16; ++j) {
if (i + j < data.size()) {
int byte = data[i + j];
if (byte >= 32 && byte <= 126) {
buffer << static_cast<char>(byte);
} else {
buffer << '.';
}
}
}
buffer << " | ";
if (i + 16 < data.size()) buffer << "\n";
}
return buffer.str();
}
// Convenience: read full file into bytes
std::vector<uint8_t> readAllBytes(const std::string& path) {
std::ifstream ifs(path, std::ios::binary);
ifs.seekg(0, std::ios::end);
std::streamsize size = ifs.tellg();
ifs.seekg(0, std::ios::beg);
std::vector<uint8_t> buf(size);
if (size > 0) ifs.read(reinterpret_cast<char*>(buf.data()), size);
return buf;
}
// MAIN demonstrating usage equivalent to the Dart example
int main() {
const std::string filename = "example.bin";
if (std::filesystem::exists(filename)) {
std::filesystem::remove(filename);
}
{
// Create and initialise
BinaryTable table(filename);
table.initialise();
std::cout << "File dump:\n";
std::cout << binaryDump(readAllBytes(filename)) << "\n";
std::cout << "File size: " << std::filesystem::file_size(filename) << " bytes\n\n";
// Set arrays
table.set("int_array", std::vector<int32_t>{6, 3, 9, 2, 5});
table.set("float_array", std::vector<double>{1.5, 2.5, 3.5});
table.set("empty", std::vector<int32_t>{});
// Modify elements
{
auto v = table.get("int_array");
auto arr = std::get<std::shared_ptr<BT_UniformArray>>(v);
arr->set(0, static_cast<int32_t>(1));
}
{
auto v = table.get("float_array");
auto arr = std::get<std::shared_ptr<BT_UniformArray>>(v);
arr->set(1, static_cast<double>(4.5));
}
{
auto v = table.get("int_array");
auto arr = std::get<std::shared_ptr<BT_UniformArray>>(v);
std::cout << "int_array pointer: " << arr->pointer_.toString() << "\n";
}
{
auto v = table.get("float_array");
auto arr = std::get<std::shared_ptr<BT_UniformArray>>(v);
std::cout << "float_array pointer: " << arr->pointer_.toString() << "\n";
}
{
auto v = table.get("int_array");
auto arr = std::get<std::shared_ptr<BT_UniformArray>>(v);
arr->add(static_cast<int32_t>(10));
arr->addAll({static_cast<int32_t>(420), static_cast<int32_t>(69), static_cast<int32_t>(1337), static_cast<int32_t>(1738)});
}
{
auto v = table.get("float_array");
auto arr = std::get<std::shared_ptr<BT_UniformArray>>(v);
arr->add(static_cast<double>(5.5));
arr->addAll({6.5, 7.5, 8.5});
}
auto readback1 = table.get("int_array");
auto readback2 = table.get("float_array");
auto readback3 = table.get("empty");
std::cout << "Readback1: " << std::get<std::shared_ptr<BT_UniformArray>>(readback1)->toString() << "\n";
std::cout << "Readback2: " << std::get<std::shared_ptr<BT_UniformArray>>(readback2)->toString() << "\n";
std::cout << "Readback3: " << std::get<std::shared_ptr<BT_UniformArray>>(readback3)->toString() << "\n\n";
std::cout << "File dump:\n";
std::cout << binaryDump(readAllBytes(filename)) << "\n";
std::cout << "File size: " << std::filesystem::file_size(filename) << " bytes\n";
}
return 0;
}