1119 lines
37 KiB
C++
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;
|
|
} |