1005 lines
29 KiB
Dart
1005 lines
29 KiB
Dart
/*
|
|
|
|
/$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$$$ /$$ /$$ /$$$$$ /$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$$
|
|
|_ $$_/| $$$ /$$$| $$__ $$| $$_____/| $$$ | $$ |__ $$|_ $$_/ | $$$ | $$| $$_____/|__ $$__/
|
|
| $$ | $$$$ /$$$$| $$ \ $$| $$ | $$$$| $$ | $$ | $$ | $$$$| $$| $$ | $$
|
|
| $$ | $$ $$/$$ $$| $$$$$$$ | $$$$$ | $$ $$ $$ | $$ | $$ | $$ $$ $$| $$$$$ | $$
|
|
| $$ | $$ $$$| $$| $$__ $$| $$__/ | $$ $$$$ /$$ | $$ | $$ | $$ $$$$| $$__/ | $$
|
|
| $$ | $$\ $ | $$| $$ \ $$| $$ | $$\ $$$| $$ | $$ | $$ | $$\ $$$| $$ | $$
|
|
/$$$$$$| $$ \/ | $$| $$$$$$$/| $$$$$$$$| $$ \ $$| $$$$$$/ /$$$$$$ /$$| $$ \ $$| $$$$$$$$ | $$
|
|
|______/|__/ |__/|_______/ |________/|__/ \__/ \______/ |______/|__/|__/ \__/|________/ |__/
|
|
|
|
© 2025-26 by Benjamin Watt of IMBENJI.NET LIMITED - All rights reserved.
|
|
|
|
Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
|
|
|
|
|
|
*/
|
|
|
|
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
|
|
enum BT_Type {
|
|
|
|
POINTER (8),
|
|
ADDRESS_TABLE (-1),
|
|
|
|
INTEGER (4),
|
|
FLOAT (4),
|
|
STRING (-1),
|
|
|
|
INTEGER_ARRAY (-1, arrayType: true),
|
|
FLOAT_ARRAY (-1, arrayType: true),;
|
|
|
|
|
|
final int size;
|
|
final bool arrayType;
|
|
const BT_Type(this.size, {
|
|
this.arrayType = false,
|
|
});
|
|
|
|
static BT_Type fromId(int id) {
|
|
if (id < 0 || id >= BT_Type.values.length) {
|
|
throw ArgumentError('Invalid BT_Type id: $id');
|
|
}
|
|
return BT_Type.values[id];
|
|
}
|
|
|
|
static BT_Type fromDynamic(dynamic value) {
|
|
if (value is int) {
|
|
return BT_Type.INTEGER;
|
|
} else if (value is double) {
|
|
return BT_Type.FLOAT;
|
|
} else if (value is String) {
|
|
return BT_Type.STRING;
|
|
} else if (value is List<int>) {
|
|
return BT_Type.INTEGER_ARRAY;
|
|
} else if (value is List<double>) {
|
|
return BT_Type.FLOAT_ARRAY;
|
|
} else {
|
|
throw ArgumentError('Unsupported type: ${value.runtimeType}');
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
List<int> encodeValue(dynamic value) {
|
|
|
|
BT_Type valueType = BT_Type.fromDynamic(value);
|
|
List<int> valueBuffer = [valueType.index];
|
|
|
|
|
|
if (valueType == BT_Type.INTEGER) {
|
|
|
|
valueBuffer.addAll((ByteData(4)..setInt32(0, value as int, Endian.little)).buffer.asUint8List());
|
|
|
|
} else if (valueType == BT_Type.FLOAT) {
|
|
|
|
valueBuffer.addAll((ByteData(4)..setFloat32(0, value as double, Endian.little)).buffer.asUint8List());
|
|
|
|
} else if (valueType == BT_Type.STRING) {
|
|
// String length
|
|
valueBuffer.addAll((ByteData(4)
|
|
..setInt32(0, (value as String).length, Endian.little)).buffer
|
|
.asUint8List());
|
|
// String bytes
|
|
valueBuffer.addAll(value.codeUnits);
|
|
} else if (valueType == BT_Type.INTEGER_ARRAY) {
|
|
List<int> list = value as List<int>;
|
|
// Length
|
|
valueBuffer.addAll((ByteData(4)
|
|
..setInt32(0, list.length, Endian.little)).buffer.asUint8List());
|
|
// Entries
|
|
for (var item in list) {
|
|
valueBuffer.addAll(encodeValue(item));
|
|
}
|
|
} else if (valueType == BT_Type.FLOAT_ARRAY) {
|
|
List<double> list = value as List<double>;
|
|
// Length
|
|
valueBuffer.addAll((ByteData(4)
|
|
..setInt32(0, list.length, Endian.little)).buffer.asUint8List());
|
|
// Entries
|
|
for (var item in list) {
|
|
valueBuffer.addAll(encodeValue(item));
|
|
}
|
|
} else {
|
|
|
|
throw ArgumentError('Unsupported type: ${value.runtimeType}');
|
|
|
|
}
|
|
|
|
return valueBuffer;
|
|
}
|
|
|
|
class BT_Pointer {
|
|
final int address;
|
|
|
|
const BT_Pointer(this.address);
|
|
|
|
bool get isNull => address == -1;
|
|
|
|
operator ==(Object other) {
|
|
if (other is! BT_Pointer) return false;
|
|
return address == other.address;
|
|
}
|
|
|
|
@override
|
|
String toString() => '0x${address.toRadixString(16)} ($address)';
|
|
}
|
|
const BT_Pointer BT_Null = BT_Pointer(-1);
|
|
|
|
class BT_Reference {
|
|
|
|
BinaryTable _table;
|
|
BT_Pointer _pointer;
|
|
|
|
BT_Reference(this._table, this._pointer);
|
|
|
|
dynamic decodeValue() {
|
|
|
|
if (_pointer.isNull) {
|
|
return null;
|
|
}
|
|
|
|
_table._file.setPositionSync(_pointer.address);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
|
|
if (type == BT_Type.INTEGER) {
|
|
|
|
return _table._file.readIntSync(4);
|
|
|
|
} else if (type == BT_Type.FLOAT) {
|
|
|
|
return _table._file.readFloat32Sync();
|
|
|
|
} else if (type == BT_Type.STRING) {
|
|
int length = _table._file.readIntSync(4);
|
|
List<int> bytes = _table._file.readSync(length);
|
|
return String.fromCharCodes(bytes);
|
|
|
|
} else if (type == BT_Type.ADDRESS_TABLE) {
|
|
// Address table decoding not implemented
|
|
throw UnimplementedError('Address table decoding not implemented');
|
|
} else if (type == BT_Type.POINTER) {
|
|
return _table._file.readPointerSync();
|
|
} else if (type == BT_Type.INTEGER_ARRAY) {
|
|
return BT_UniformArray(_table, _pointer);
|
|
} else if (type == BT_Type.FLOAT_ARRAY) {
|
|
return BT_UniformArray(_table, _pointer);
|
|
} else {
|
|
throw Exception('Unsupported type: $type');
|
|
}
|
|
|
|
}
|
|
|
|
/// Determine the size in storage in bytes with the least amount of reads possible
|
|
int get size {
|
|
if (_pointer.isNull) {
|
|
return 0;
|
|
}
|
|
|
|
_table._file.setPositionSync(_pointer.address);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
|
|
if (type == BT_Type.INTEGER) {
|
|
|
|
return 1 + 4; // Type byte + Int32
|
|
|
|
} else if (type == BT_Type.FLOAT) {
|
|
|
|
return 1 + 4; // Type byte + Float32
|
|
|
|
} else if (type == BT_Type.STRING) {
|
|
|
|
int length = _table._file.readIntSync(4);
|
|
return 1 + 4 + length; // Type byte + Length Int32 + String bytes
|
|
|
|
} else if (type == BT_Type.ADDRESS_TABLE) {
|
|
|
|
// Address table size is variable, need to read the count
|
|
int count = _table._file.readIntSync(4);
|
|
return 1 + 4 + count *
|
|
(8 + BT_Type.POINTER.size); // Type byte + Count Int32 + Entries
|
|
|
|
} else {
|
|
throw Exception('Unsupported type: $type');
|
|
}
|
|
}
|
|
|
|
@override
|
|
String toString() => _pointer.toString();
|
|
}
|
|
|
|
class BT_FreeListEntry {
|
|
BT_Pointer pointer;
|
|
int size;
|
|
|
|
BT_FreeListEntry(this.pointer, this.size);
|
|
}
|
|
|
|
/// Wrapper for interacting with uniform arrays in the binary table. A uniform array is an array where all elements are of the same type, allowing for more efficient storage and retrieval using random access.
|
|
class BT_UniformArray extends BT_Reference {
|
|
|
|
BT_UniformArray(super._table, super._pointer);
|
|
|
|
/// Get the length of the array - Don't mix up with size.
|
|
int get length {
|
|
if (_pointer.isNull) {
|
|
return 0;
|
|
}
|
|
|
|
_table._file.setPositionSync(_pointer.address);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
|
|
if (!type.arrayType) {
|
|
throw Exception('Not an array');
|
|
}
|
|
|
|
return _table._file.readIntSync(4);
|
|
}
|
|
|
|
dynamic operator [](int index) {
|
|
if (_pointer.isNull) {
|
|
throw Exception('Null pointer');
|
|
}
|
|
|
|
int len = length;
|
|
if (index < 0 || index >= len) {
|
|
throw RangeError.index(index, this, 'index', null, len);
|
|
}
|
|
|
|
// Determine the type of the array by reading the first items type
|
|
_table._file.setPositionSync(_pointer.address + 1 + 4);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
|
|
int itemOffset = index * (1 + type.size); // Type byte + data size
|
|
BT_Reference itemRef = BT_Reference(_table, BT_Pointer((_pointer.address + 1 + 4) + itemOffset));
|
|
return itemRef.decodeValue();
|
|
}
|
|
|
|
void operator []=(int index, dynamic value) {
|
|
if (_pointer.isNull) {
|
|
throw Exception('Null pointer');
|
|
}
|
|
|
|
int len = length;
|
|
if (index < 0 || index >= len) {
|
|
throw RangeError.index(index, this, 'index', null, len);
|
|
}
|
|
|
|
// Determine the type of the array by reading the first items type
|
|
_table._file.setPositionSync(_pointer.address + 1 + 4);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
if (type.size == -1) {
|
|
throw Exception("Types with variable size are not supported in uniform arrays. Use a non-uniform array instead.");
|
|
}
|
|
|
|
// Ensure the new value is of the correct type
|
|
BT_Type newValueType = BT_Type.fromDynamic(value);
|
|
if (newValueType != type) {
|
|
throw Exception('Type mismatch. Expected $type but got $newValueType');
|
|
}
|
|
|
|
// Calculate the offset of the item to update
|
|
int itemOffset = index * (1 + type.size); // Type byte + data size
|
|
BT_Pointer itemPointer = BT_Pointer((_pointer.address + 1 + 4) + itemOffset);
|
|
|
|
// Encode the new value
|
|
List<int> valueBuffer = encodeValue(value);
|
|
|
|
// Place the new value in the file
|
|
_table._file.setPositionSync(itemPointer.address);
|
|
_table._file.writeFromSync(valueBuffer);
|
|
|
|
}
|
|
|
|
void addAll(Iterable<dynamic> values) {
|
|
|
|
_table.antiFreeListScope(() {
|
|
// Determine the type of the array by reading the first items type
|
|
BT_Type type = elementType ?? BT_Type.fromDynamic(values.first);
|
|
|
|
// Validate all new values are of the correct type
|
|
for (int i = 0; i < values.length; i++) {
|
|
BT_Type newValueType = BT_Type.fromDynamic(values.elementAt(i));
|
|
if (newValueType != type) {
|
|
throw Exception('Type mismatch at index $i. Expected $type but got $newValueType');
|
|
} else if (newValueType.size == -1) {
|
|
throw Exception("Types with variable size are not supported in uniform arrays. Use a non-uniform array instead.");
|
|
}
|
|
}
|
|
|
|
// Read the full array by loading the full buffer
|
|
List<int> fullBuffer = [];
|
|
int bufferSize = 1 + 4 + length * (1 + type.size);
|
|
_table._file.setPositionSync(_pointer.address);
|
|
fullBuffer = _table._file.readSync(bufferSize).toList();
|
|
|
|
// Encode the new values and add them to the buffer
|
|
for (var value in values) {
|
|
fullBuffer.addAll(encodeValue(value));
|
|
}
|
|
|
|
// Update the length in the buffer
|
|
int newLength = length + values.length;
|
|
List<int> lengthBytes = (ByteData(4)..setInt32(0, newLength, Endian.little)).buffer.asUint8List();
|
|
fullBuffer.replaceRange(1, 5, lengthBytes);
|
|
|
|
// Free the old array
|
|
_table.free(_pointer, size);
|
|
|
|
// Allocate new space for the updated array
|
|
BT_Pointer newPointer = _table.alloc(fullBuffer.length);
|
|
|
|
// Replace any references to the old pointer with the new one
|
|
Map<int, BT_Pointer> addressTable = _table._addressTable;
|
|
addressTable.updateAll((key, value) {
|
|
if (value == _pointer) {
|
|
print('Updating address table entry for key $key from $value to $newPointer');
|
|
return newPointer;
|
|
}
|
|
return value;
|
|
});
|
|
_table._addressTable = addressTable;
|
|
_pointer = newPointer;
|
|
|
|
// Write the updated buffer to the new location
|
|
_table._file.setPositionSync(newPointer.address);
|
|
_table._file.writeFromSync(fullBuffer);
|
|
|
|
print('Array resized to new length $newLength at $newPointer');
|
|
});
|
|
|
|
}
|
|
|
|
void add(dynamic value) {
|
|
addAll([value]);
|
|
}
|
|
|
|
@override
|
|
int get size {
|
|
|
|
if (length == 0) {
|
|
return 1 + 4; // Type byte + Length Int32
|
|
}
|
|
|
|
_table._file.setPositionSync(_pointer.address);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
|
|
if (type.arrayType) {
|
|
|
|
_table._file.setPositionSync(_pointer.address);
|
|
int bufferSize = 1 + 4 + length * (1 + elementType!.size);
|
|
|
|
return bufferSize;
|
|
}
|
|
|
|
return super.size;
|
|
}
|
|
|
|
BT_Type? get elementType {
|
|
if (length == 0) {
|
|
return null;
|
|
} else {
|
|
_table._file.setPositionSync(_pointer.address + 1 + 4);
|
|
int typeId = _table._file.readByteSync();
|
|
BT_Type type = BT_Type.fromId(typeId);
|
|
return type;
|
|
}
|
|
}
|
|
|
|
@override
|
|
String toString([bool readValues = false]) {
|
|
|
|
if (readValues) {
|
|
return 'Uniform Array of length $length';
|
|
}
|
|
|
|
List<dynamic> preview = [];
|
|
int len = length;
|
|
for (int i = 0; i < length; i++) {
|
|
preview.add(this[i]);
|
|
}
|
|
|
|
return 'Uniform Array: $preview';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
extension FreeList on List<BT_FreeListEntry> {
|
|
|
|
void removePointer(BT_Pointer pointer) {
|
|
removeWhere((entry) => entry.pointer == pointer);
|
|
}
|
|
|
|
List<int> bt_encode() {
|
|
List<int> buffer = [];
|
|
|
|
/*
|
|
|
|
New encoding should reflect a "read from the end" approach.
|
|
So it should look like:
|
|
- Entries (variable)
|
|
- Entry count (4 bytes)
|
|
|
|
Entry:
|
|
- Pointer (8 bytes)
|
|
- Size (4 bytes)
|
|
|
|
This means that when reading, we can always assume that the last 4 bytes in the file are the entry count for the free list.
|
|
The entries however, can be read from front to back. Once we know the count, we know where to start reading.
|
|
*/
|
|
|
|
for (var entry in this) {
|
|
// Pointer
|
|
buffer.addAll((ByteData(BT_Type.POINTER.size)..setInt64(0, entry.pointer.address, Endian.little)).buffer.asUint8List());
|
|
// Size
|
|
buffer.addAll((ByteData(4)..setInt32(0, entry.size, Endian.little)).buffer.asUint8List());
|
|
}
|
|
|
|
// Entry count
|
|
buffer.addAll((ByteData(4)..setInt32(0, length, Endian.little)).buffer.asUint8List());
|
|
|
|
return buffer;
|
|
}
|
|
|
|
}
|
|
|
|
class BinaryTable {
|
|
|
|
RandomAccessFile _file;
|
|
|
|
BinaryTable(String path) : _file = File(path).openSync(mode: FileMode.append);
|
|
|
|
void initialise() {
|
|
_file.setPositionSync(0);
|
|
_file.writePointerSync(BT_Null); // Address table pointer
|
|
_file.writeIntSync(0, 4); // Free list entry count
|
|
}
|
|
|
|
Map<int, BT_Pointer> get _addressTable {
|
|
|
|
_file.setPositionSync(0);
|
|
BT_Reference tableRef = BT_Reference(this, _file.readPointerSync());
|
|
|
|
if (tableRef._pointer.isNull) {
|
|
return {};
|
|
}
|
|
|
|
_file.setPositionSync(tableRef._pointer.address + 1);
|
|
int fileSize = _file.lengthSync();
|
|
int tableCount = _file.readIntSync(4);
|
|
int tableSize = tableCount * (8 + BT_Type.POINTER.size);
|
|
List<int> buffer = _file.readSync(tableSize).toList();
|
|
|
|
Map<int, BT_Pointer> addressTable = {};
|
|
|
|
for (int i = 0; i < tableCount; i++) {
|
|
|
|
int offset = i * (8 + BT_Type.POINTER.size);
|
|
|
|
// Key Hash
|
|
List<int> keyHashBytes = buffer.sublist(offset, offset + 8);
|
|
int keyHash = ByteData.sublistView(Uint8List.fromList(keyHashBytes)).getInt64(0, Endian.little);
|
|
|
|
// Value Pointer
|
|
List<int> valuePointerBytes = buffer.sublist(offset + 8, offset + 8 + BT_Type.POINTER.size);
|
|
int valuePointerAddress = ByteData.sublistView(Uint8List.fromList(valuePointerBytes)).getInt64(0, Endian.little);
|
|
BT_Pointer valuePointer = BT_Pointer(valuePointerAddress);
|
|
|
|
// Add to address table
|
|
addressTable[keyHash] = valuePointer;
|
|
}
|
|
|
|
return addressTable;
|
|
|
|
}
|
|
set _addressTable(Map<int, BT_Pointer> table) {
|
|
List<int> buffer = [
|
|
BT_Type.ADDRESS_TABLE.index
|
|
];
|
|
|
|
buffer.addAll((ByteData(4)..setInt32(0, table.length, Endian.little)).buffer.asUint8List());
|
|
table.forEach((key, value) {
|
|
buffer.addAll((ByteData(8)..setInt64(0, key, Endian.little)).buffer.asUint8List());
|
|
buffer.addAll((ByteData(BT_Type.POINTER.size)..setInt64(0, value.address, Endian.little)).buffer.asUint8List());
|
|
});
|
|
|
|
// Write new address table at end of file
|
|
BT_Pointer tableAddress = alloc(buffer.length);
|
|
_file.setPositionSync(tableAddress.address);
|
|
_file.writeFromSync(buffer);
|
|
|
|
// Read old table pointer before updating
|
|
_file.setPositionSync(0);
|
|
BT_Reference oldTableRef = BT_Reference(this, _file.readPointerSync());
|
|
|
|
// Update header to point to new table
|
|
_file.setPositionSync(0);
|
|
_file.writePointerSync(tableAddress);
|
|
|
|
// Now free the old table if it exists and is not the same as the new one
|
|
if (!oldTableRef._pointer.isNull && oldTableRef._pointer != tableAddress) {
|
|
free(oldTableRef._pointer, oldTableRef.size);
|
|
}
|
|
}
|
|
|
|
bool freeListLifted = false;
|
|
List<BT_FreeListEntry>? _freeListCache;
|
|
|
|
List<BT_FreeListEntry> get _freeList {
|
|
|
|
if (freeListLifted) {
|
|
return _freeListCache ?? [];
|
|
}
|
|
|
|
_file.setPositionSync(_file.lengthSync() - 4);
|
|
int entryCount = _file.readIntSync(4);
|
|
if (entryCount == 0) {
|
|
return [];
|
|
}
|
|
|
|
int entrySize = BT_Type.POINTER.size + 4; // Pointer + Size
|
|
int freeListSize = entryCount * entrySize;
|
|
_file.setPositionSync(_file.lengthSync() - 4 - freeListSize);
|
|
List<int> buffer = _file.readSync(freeListSize);
|
|
|
|
List<BT_FreeListEntry> freeList = [];
|
|
for (int i = 0; i < entryCount; i++) {
|
|
int offset = i * entrySize;
|
|
|
|
// Pointer
|
|
List<int> pointerBytes = buffer.sublist(offset, offset + BT_Type.POINTER.size);
|
|
int pointerAddress = ByteData.sublistView(Uint8List.fromList(pointerBytes)).getInt64(0, Endian.little);
|
|
BT_Pointer pointer = BT_Pointer(pointerAddress);
|
|
|
|
// Size
|
|
List<int> sizeBytes = buffer.sublist(offset + BT_Type.POINTER.size, offset + entrySize);
|
|
int size = ByteData.sublistView(Uint8List.fromList(sizeBytes)).getInt32(0, Endian.little);
|
|
|
|
freeList.add(BT_FreeListEntry(pointer, size));
|
|
}
|
|
|
|
return freeList;
|
|
}
|
|
set _freeList(List<BT_FreeListEntry> list) {
|
|
|
|
if (freeListLifted) {
|
|
_freeListCache = list;
|
|
return;
|
|
}
|
|
|
|
_file.setPositionSync(_file.lengthSync() - 4);
|
|
int oldEntryCount = _file.readIntSync(4);
|
|
int oldListSize = (oldEntryCount * (BT_Type.POINTER.size + 4)) + 4; // Entries + Count
|
|
_file.truncateSync(_file.lengthSync() - oldListSize);
|
|
|
|
List<int> buffer = list.bt_encode();
|
|
_file.setPositionSync(_file.lengthSync());
|
|
_file.writeFromSync(buffer);
|
|
}
|
|
|
|
/// Caches the free list in memory, and removed it from the file.
|
|
void liftFreeList() {
|
|
if (freeListLifted) {
|
|
throw StateError('Free list is already lifted');
|
|
}
|
|
|
|
_freeListCache = _freeList;
|
|
|
|
_file.setPositionSync(_file.lengthSync() - 4);
|
|
int oldEntryCount = _file.readIntSync(4);
|
|
int oldEntrySize = BT_Type.POINTER.size + 4; // Pointer + Size
|
|
int oldFreeListSize = oldEntryCount * oldEntrySize + 4; // +4 for entry count
|
|
_file.truncateSync(_file.lengthSync() - oldFreeListSize);
|
|
|
|
freeListLifted = true;
|
|
}
|
|
/// Appends the cached free list back to the file, and clears the cache.
|
|
void dropFreeList() {
|
|
if (!freeListLifted) {
|
|
throw StateError('Free list is not lifted');
|
|
}
|
|
|
|
_file.setPositionSync(_file.lengthSync());
|
|
_file.writeIntSync(0, 4); // Placeholder for entry count
|
|
|
|
freeListLifted = false;
|
|
_freeList = _freeListCache!;
|
|
_freeListCache = null;
|
|
}
|
|
|
|
void antiFreeListScope(void Function() fn) {
|
|
liftFreeList();
|
|
try {
|
|
fn();
|
|
} finally {
|
|
dropFreeList();
|
|
}
|
|
}
|
|
|
|
void free(BT_Pointer pointer, int size) {
|
|
|
|
if (!freeListLifted) {
|
|
throw StateError('Free list must be lifted before freeing memory');
|
|
}
|
|
|
|
if (pointer.isNull || size <= 0) {
|
|
throw ArgumentError('Cannot free null pointer or zero size');
|
|
}
|
|
|
|
// Fetch current free list
|
|
List<BT_FreeListEntry> freeList = _freeList;
|
|
|
|
// Add new free entry
|
|
freeList.add(BT_FreeListEntry(pointer, size));
|
|
|
|
// Merge contiguous free entries
|
|
List<BT_FreeListEntry> mergeContiguousFreeBlocks(List<BT_FreeListEntry> freeList) {
|
|
if (freeList.isEmpty) return [];
|
|
|
|
// Create a copy and sort by address to check for contiguous blocks
|
|
List<BT_FreeListEntry> sorted = List.from(freeList);
|
|
sorted.sort((a, b) => a.pointer.address.compareTo(b.pointer.address));
|
|
|
|
List<BT_FreeListEntry> merged = [];
|
|
|
|
for (var entry in sorted) {
|
|
if (merged.isEmpty) {
|
|
// First entry, just add it
|
|
merged.add(BT_FreeListEntry(entry.pointer, entry.size));
|
|
} else {
|
|
var last = merged.last;
|
|
|
|
// Check if current entry is contiguous with the last merged entry
|
|
if (last.pointer.address + last.size == entry.pointer.address) {
|
|
// Merge: extend the size of the last entry
|
|
last.size += entry.size;
|
|
} else {
|
|
// Not contiguous, add as separate entry
|
|
merged.add(BT_FreeListEntry(entry.pointer, entry.size));
|
|
}
|
|
}
|
|
}
|
|
|
|
return merged;
|
|
}
|
|
|
|
freeList = mergeContiguousFreeBlocks(freeList);
|
|
|
|
// Update free list
|
|
_freeList = freeList;
|
|
}
|
|
BT_Pointer alloc(int size) {
|
|
|
|
if (!freeListLifted) {
|
|
throw StateError('Free list must be lifted before allocation');
|
|
}
|
|
|
|
// Fetch current free list
|
|
List<BT_FreeListEntry> freeList = _freeList;
|
|
|
|
// If its empty, allocate at end of file
|
|
if (freeList.isEmpty) {
|
|
// "[ALLOC] CODE 1: No free blocks available, allocating at end of file".yellow.log();
|
|
return BT_Pointer(_file.lengthSync());
|
|
}
|
|
|
|
// Find a free block that fits the size (exact fit or larger)
|
|
BT_FreeListEntry? bestFit;
|
|
for (var entry in freeList) {
|
|
if (entry.size >= size) {
|
|
bestFit = entry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (bestFit == null) {
|
|
// "[ALLOC] CODE 2: No suitable free block found, allocating at end of file".yellow.log();
|
|
return BT_Pointer(_file.lengthSync());
|
|
}
|
|
|
|
bool exactFit = bestFit.size == size;
|
|
if (exactFit) {
|
|
// "[ALLOC] CODE 3: Found exact fit in free block at ${bestFit.pointer} of size ${bestFit.size} bytes".green.log();
|
|
// Remove from free list
|
|
freeList.removePointer(bestFit.pointer);
|
|
_freeList = freeList;
|
|
return bestFit.pointer;
|
|
} else {
|
|
// "[ALLOC] CODE 4: Found larger free block at ${bestFit.pointer} of size ${bestFit.size} bytes, allocating ${size} bytes. Splitting block, remaining size ${bestFit.size - size} bytes".green.log();
|
|
// Allocate from start of block, reduce size and move pointer
|
|
BT_Pointer allocatedPointer = bestFit.pointer;
|
|
bestFit = BT_FreeListEntry(BT_Pointer(bestFit.pointer.address + size), bestFit.size - size);
|
|
// Update free list
|
|
freeList.removePointer(allocatedPointer);
|
|
freeList.add(bestFit);
|
|
_freeList = freeList;
|
|
return allocatedPointer;
|
|
}
|
|
}
|
|
|
|
operator []=(String key, dynamic value) {
|
|
|
|
antiFreeListScope(() {
|
|
Map<int, BT_Pointer> addressTable = _addressTable;
|
|
|
|
int keyHash = key.hashCode;
|
|
|
|
if (addressTable.containsKey(keyHash)) {
|
|
throw Exception('Key already exists');
|
|
}
|
|
|
|
|
|
|
|
List<int> valueBuffer = encodeValue(value);
|
|
|
|
// Write value to file
|
|
BT_Pointer valueAddress = alloc(valueBuffer.length);
|
|
|
|
_file.setPositionSync(valueAddress.address);
|
|
_file.writeFromSync(valueBuffer);
|
|
|
|
// Update address table
|
|
addressTable[keyHash] = valueAddress;
|
|
_addressTable = addressTable;
|
|
});
|
|
|
|
}
|
|
operator [](String key) {
|
|
Map<int, BT_Pointer> addressTable = _addressTable;
|
|
|
|
int keyHash = key.hashCode;
|
|
|
|
if (!addressTable.containsKey(keyHash)) {
|
|
throw Exception('Key does not exist');
|
|
}
|
|
|
|
BT_Pointer valuePointer = addressTable[keyHash]!;
|
|
BT_Reference valueRef = BT_Reference(this, valuePointer);
|
|
|
|
return valueRef.decodeValue();
|
|
}
|
|
|
|
void delete(String key) {
|
|
|
|
antiFreeListScope(() {
|
|
Map<int, BT_Pointer> addressTable = _addressTable;
|
|
|
|
int keyHash = key.hashCode;
|
|
|
|
if (!addressTable.containsKey(keyHash)) {
|
|
throw Exception('Key does not exist');
|
|
}
|
|
|
|
BT_Pointer valuePointer = addressTable[keyHash]!;
|
|
BT_Reference valueRef = BT_Reference(this, valuePointer);
|
|
|
|
// Free the value
|
|
free(valuePointer, valueRef.size);
|
|
|
|
// Remove from address table
|
|
addressTable.remove(keyHash);
|
|
_addressTable = addressTable;
|
|
});
|
|
|
|
}
|
|
|
|
void truncate() {
|
|
|
|
antiFreeListScope(() {
|
|
// Relocate the address table if possible
|
|
_addressTable = _addressTable;
|
|
|
|
// Check if the last free block is at the end of the file
|
|
List<BT_FreeListEntry> freeList = _freeList;
|
|
freeList.sort((a, b) => a.pointer.address.compareTo(b.pointer.address));
|
|
|
|
if (freeList.isEmpty) {
|
|
return;
|
|
}
|
|
|
|
BT_FreeListEntry lastEntry = freeList.last;
|
|
int fileEnd = _file.lengthSync();
|
|
int expectedEnd = lastEntry.pointer.address + lastEntry.size;
|
|
|
|
if (expectedEnd != fileEnd) {
|
|
return;
|
|
}
|
|
|
|
// Remove the last entry from the free list
|
|
freeList.removeLast();
|
|
_freeList = freeList;
|
|
|
|
// Truncate the file
|
|
int newLength = lastEntry.pointer.address;
|
|
_file.truncateSync(newLength);
|
|
});
|
|
|
|
}
|
|
|
|
}
|
|
|
|
void main() {
|
|
File file = File('example.bin');
|
|
if (file.existsSync()) {
|
|
file.deleteSync();
|
|
}
|
|
file.createSync();
|
|
BinaryTable table = BinaryTable(file.path);
|
|
table.initialise();
|
|
|
|
print("File dump:");
|
|
print(binaryDump(file.readAsBytesSync()));
|
|
print("File size: ${file.lengthSync()} bytes");
|
|
print(" ");
|
|
|
|
table["int_array"] = [6, 3, 9, 2, 5];
|
|
table["float_array"] = [1.5, 2.5, 3.5];
|
|
table["empty"] = <int>[];
|
|
|
|
table["int_array"][0] = 1;
|
|
table["float_array"][1] = 4.5;
|
|
|
|
print("int_array pointer: ${table["int_array"]._pointer}");
|
|
print("float_array pointer: ${table["float_array"]._pointer}");
|
|
|
|
table["int_array"].add(10);
|
|
table["float_array"].add(5.5);
|
|
|
|
table["int_array"].addAll([420, 69, 1337, 1738]);
|
|
table["float_array"].addAll([6.5, 7.5, 8.5]);
|
|
|
|
var readback1 = table["int_array"];
|
|
var readback2 = table["float_array"];
|
|
var readback3 = table["empty"];
|
|
print("Readback1: $readback1");
|
|
print("Readback2: $readback2");
|
|
print("Readback3: $readback3");
|
|
|
|
print(" ");
|
|
print("File dump:");
|
|
print(binaryDump(file.readAsBytesSync()));
|
|
print("File size: ${file.lengthSync()} bytes");
|
|
}
|
|
|
|
extension on RandomAccessFile {
|
|
|
|
// Read/Write Int Dynamic - Can specify size in bytes, does not have to align to 1, 2, 4, or 8 bytes. Default is 4 bytes (Int32)
|
|
int readIntSync([int size = 4, Endian endianness = Endian.little]) {
|
|
if (size < 1 || size > 8) {
|
|
throw ArgumentError('Size must be between 1 and 8 bytes');
|
|
}
|
|
|
|
List<int> bytes = readSync(size);
|
|
|
|
// Build integer from bytes with proper endianness
|
|
int result = 0;
|
|
if (endianness == Endian.little) {
|
|
for (int i = size - 1; i >= 0; i--) {
|
|
result = (result << 8) | bytes[i];
|
|
}
|
|
} else {
|
|
for (int i = 0; i < size; i++) {
|
|
result = (result << 8) | bytes[i];
|
|
}
|
|
}
|
|
|
|
// Sign extend if MSB is set
|
|
int signBit = 1 << (size * 8 - 1);
|
|
if (result & signBit != 0) {
|
|
result -= 1 << (size * 8);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void writeIntSync(int value, [int size = 4, Endian endianness = Endian.little]) {
|
|
if (size < 1 || size > 8) {
|
|
throw ArgumentError('Size must be between 1 and 8 bytes');
|
|
}
|
|
|
|
List<int> bytes = List.filled(size, 0);
|
|
|
|
// Extract bytes with proper endianness
|
|
if (endianness == Endian.little) {
|
|
for (int i = 0; i < size; i++) {
|
|
bytes[i] = (value >> (i * 8)) & 0xFF;
|
|
}
|
|
} else {
|
|
for (int i = 0; i < size; i++) {
|
|
bytes[size - 1 - i] = (value >> (i * 8)) & 0xFF;
|
|
}
|
|
}
|
|
|
|
writeFromSync(bytes);
|
|
}
|
|
|
|
// Read/Write Pointers
|
|
BT_Pointer readPointerSync() {
|
|
int offset = readIntSync(BT_Type.POINTER.size);
|
|
return BT_Pointer(offset);
|
|
}
|
|
void writePointerSync(BT_Pointer pointer) {
|
|
writeIntSync(pointer.address, BT_Type.POINTER.size);
|
|
}
|
|
|
|
// Read/Write Float32
|
|
double readFloat32Sync([Endian endianness = Endian.little]) {
|
|
List<int> bytes = readSync(4);
|
|
return ByteData.sublistView(Uint8List.fromList(bytes)).getFloat32(0, endianness);
|
|
}
|
|
void writeFloat32Sync(double value, [Endian endianness = Endian.little]) {
|
|
ByteData byteData = ByteData(4);
|
|
byteData.setFloat32(0, value, endianness);
|
|
writeFromSync(byteData.buffer.asUint8List());
|
|
}
|
|
|
|
// Read/Write Float64 (Double)
|
|
double readFloat64Sync([Endian endianness = Endian.little]) {
|
|
List<int> bytes = readSync(8);
|
|
return ByteData.sublistView(Uint8List.fromList(bytes)).getFloat64(0, endianness);
|
|
}
|
|
void writeFloat64Sync(double value, [Endian endianness = Endian.little]) {
|
|
ByteData byteData = ByteData(8);
|
|
byteData.setFloat64(0, value, endianness);
|
|
writeFromSync(byteData.buffer.asUint8List());
|
|
}
|
|
|
|
}
|
|
|
|
String binaryDump(Uint8List data) {
|
|
StringBuffer buffer = StringBuffer();
|
|
|
|
for (int i = 0; i < data.length; i += 16) {
|
|
// Address
|
|
buffer.write('0x${i.toRadixString(16).padLeft(4, '0').toUpperCase()} (${i.toString().padLeft(4)}) | ');
|
|
|
|
// Hex bytes
|
|
for (int j = 0; j < 16; j++) {
|
|
if (i + j < data.length) {
|
|
buffer.write('${data[i + j].toRadixString(16).padLeft(2, '0').toUpperCase()} ');
|
|
} else {
|
|
buffer.write(' ');
|
|
}
|
|
}
|
|
|
|
buffer.write(' | ');
|
|
|
|
// Integer representation
|
|
for (int j = 0; j < 16; j++) {
|
|
if (i + j < data.length) {
|
|
buffer.write('${data[i + j].toString().padLeft(3)} ');
|
|
} else {
|
|
buffer.write(' ');
|
|
}
|
|
}
|
|
|
|
buffer.write(' | ');
|
|
|
|
// ASCII representation
|
|
for (int j = 0; j < 16; j++) {
|
|
if (i + j < data.length) {
|
|
int byte = data[i + j];
|
|
if (byte >= 32 && byte <= 126) {
|
|
buffer.write(String.fromCharCode(byte));
|
|
} else {
|
|
buffer.write('.');
|
|
}
|
|
}
|
|
}
|
|
|
|
buffer.write(' | ');
|
|
if (i + 16 < data.length) buffer.writeln();
|
|
}
|
|
|
|
return buffer.toString();
|
|
}
|