1541 lines
43 KiB
Dart
1541 lines
43 KiB
Dart
/*
|
|
|
|
/$$$$$$ /$$ /$$ /$$$$$$$ /$$$$$$$$ /$$ /$$ /$$$$$ /$$$$$$ /$$ /$$ /$$$$$$$$ /$$$$$$$$
|
|
|_ $$_/| $$$ /$$$| $$__ $$| $$_____/| $$$ | $$ |__ $$|_ $$_/ | $$$ | $$| $$_____/|__ $$__/
|
|
| $$ | $$$$ /$$$$| $$ \ $$| $$ | $$$$| $$ | $$ | $$ | $$$$| $$| $$ | $$
|
|
| $$ | $$ $$/$$ $$| $$$$$$$ | $$$$$ | $$ $$ $$ | $$ | $$ | $$ $$ $$| $$$$$ | $$
|
|
| $$ | $$ $$$| $$| $$__ $$| $$__/ | $$ $$$$ /$$ | $$ | $$ | $$ $$$$| $$__/ | $$
|
|
| $$ | $$\ $ | $$| $$ \ $$| $$ | $$\ $$$| $$ | $$ | $$ | $$\ $$$| $$ | $$
|
|
/$$$$$$| $$ \/ | $$| $$$$$$$/| $$$$$$$$| $$ \ $$| $$$$$$/ /$$$$$$ /$$| $$ \ $$| $$$$$$$$ | $$
|
|
|______/|__/ |__/|_______/ |________/|__/ \__/ \______/ |______/|__/|__/ \__/|________/ |__/
|
|
|
|
© 2025-26 by Benjamin Watt of IMBENJI.NET LIMITED - All rights reserved.
|
|
|
|
Use of this source code is governed by the Business Source License 1.1 that can be found in the LICENSE file.
|
|
|
|
This file is part of the SweepStore (formerly Binary Table) package for Dart.
|
|
|
|
*/
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
import 'dart:isolate';
|
|
import 'dart:math';
|
|
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');
|
|
}
|
|
}
|
|
|
|
BT_Type? get type {
|
|
if (_pointer.isNull) {
|
|
return null;
|
|
}
|
|
|
|
_table._file.setPositionSync(_pointer.address);
|
|
int typeId = _table._file.readByteSync();
|
|
return BT_Type.fromId(typeId);
|
|
}
|
|
|
|
|
|
@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) {
|
|
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);
|
|
});
|
|
|
|
}
|
|
|
|
void add(dynamic value) {
|
|
addAll([value]);
|
|
}
|
|
|
|
List<dynamic> fetchSublist([int start = 0, int end = -1]) {
|
|
|
|
// Read the full array by loading the full buffer of only whats needed
|
|
BT_Type? type = elementType;
|
|
if (type == null) {
|
|
return [];
|
|
}
|
|
if (type.size == -1) {
|
|
throw Exception("Types with variable size are not supported in uniform arrays. Use a non-uniform array instead.");
|
|
}
|
|
|
|
end = end == -1 ? length : end;
|
|
|
|
int bufferStart = 1 + 4 + start * (1 + type.size);
|
|
int bufferEnd = 1 + 4 + end * (1 + type.size);
|
|
int bufferSize = bufferEnd - bufferStart;
|
|
_table._file.setPositionSync(_pointer.address + bufferStart);
|
|
List<int> buffer = _table._file.readSync(bufferSize).toList();
|
|
|
|
List<dynamic> values = [];
|
|
for (int i = 0; i < (end - start); i++) {
|
|
int offset = i * (1 + type.size);
|
|
BT_Reference itemRef = BT_Reference(_table, BT_Pointer((_pointer.address + bufferStart) + offset));
|
|
values.add(itemRef.decodeValue());
|
|
}
|
|
return values;
|
|
}
|
|
|
|
@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:
|
|
- 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());
|
|
}
|
|
|
|
return buffer;
|
|
}
|
|
|
|
}
|
|
|
|
extension fnv1a on String {
|
|
|
|
int get bt_hash {
|
|
List<int> bytes = utf8.encode(this);
|
|
int hash = 0xcbf29ce484222325; // FNV offset basis
|
|
|
|
for (int byte in bytes) {
|
|
hash ^= byte;
|
|
hash *= 0x100000001b3; // FNV prime
|
|
}
|
|
|
|
return hash;
|
|
}
|
|
|
|
}
|
|
|
|
// Convert double to float16
|
|
extension on double {
|
|
|
|
List<int> _toFloat16Bytes() {
|
|
ByteData float32Data = ByteData(4);
|
|
float32Data.setFloat32(0, this, Endian.little);
|
|
int f = float32Data.getUint32(0);
|
|
|
|
int sign = (f >> 31) & 0x1;
|
|
int exponent = (f >> 23) & 0xff;
|
|
int mantissa = f & 0x7fffff;
|
|
|
|
if (exponent == 0xff) { // Inf or NaN
|
|
if (mantissa != 0) {
|
|
return [(sign << 7) | 0x7e, 0x01]; // NaN
|
|
} else {
|
|
return [(sign << 7) | 0x7c, 0x00]; // Inf
|
|
}
|
|
}
|
|
|
|
if (exponent == 0) { // Zero or denormalized
|
|
return [sign << 7, 0x00]; // Zero
|
|
}
|
|
|
|
// Adjust exponent for float16 (bias: 127 for float32, 15 for float16)
|
|
exponent = exponent - 127 + 15;
|
|
|
|
if (exponent >= 31) { // Overflow to infinity
|
|
return [(sign << 7) | 0x7c, 0x00]; // Inf
|
|
}
|
|
|
|
if (exponent <= 0) { // Underflow to zero
|
|
return [sign << 7, 0x00]; // Zero
|
|
}
|
|
|
|
// Extract the top 10 bits of mantissa for float16
|
|
mantissa >>= 13;
|
|
|
|
int float16 = (sign << 15) | (exponent << 10) | mantissa;
|
|
return [float16 & 0xff, (float16 >> 8) & 0xff]; // Little endian
|
|
}
|
|
|
|
}
|
|
|
|
enum BT_TicketState {
|
|
IDLE,
|
|
WAITING,
|
|
APPROVED,
|
|
EXECUTING,
|
|
COMPLETED,
|
|
}
|
|
|
|
enum BT_TicketOpcode {
|
|
NONE,
|
|
READ,
|
|
MODIFY,
|
|
WRITE,
|
|
}
|
|
|
|
class BT_Ticket {
|
|
|
|
final int heartbeat;
|
|
final BT_TicketState state;
|
|
final BT_TicketOpcode opcode;
|
|
final int keyHash;
|
|
|
|
// Defined by the slave
|
|
final int writeSize;
|
|
|
|
// Defined by the master
|
|
final BT_Pointer writePointer;
|
|
|
|
BT_Ticket({
|
|
required this.heartbeat,
|
|
required this.state,
|
|
required this.opcode,
|
|
required this.keyHash,
|
|
|
|
this.writeSize = 0,
|
|
this.writePointer = BT_Null,
|
|
});
|
|
|
|
BT_Ticket.fromIntList(List<int> data)
|
|
: heartbeat = ByteData.sublistView(Uint8List.fromList(data.sublist(0, 4))).getInt32(0, Endian.little),
|
|
state = BT_TicketState.values[data[4]],
|
|
opcode = BT_TicketOpcode.values[data[5]],
|
|
keyHash = ByteData.sublistView(Uint8List.fromList(data.sublist(6, 14))).getInt64(0, Endian.little),
|
|
|
|
writePointer = BT_Pointer(ByteData.sublistView(Uint8List.fromList(data.sublist(14, 22))).getInt64(0, Endian.little)),
|
|
writeSize = ByteData.sublistView(Uint8List.fromList(data.sublist(22, 26))).getInt32(0, Endian.little);
|
|
|
|
List<int> toIntList() {
|
|
List<int> data = [];
|
|
|
|
// Heartbeat (4 bytes)
|
|
data.addAll((ByteData(4)..setInt32(0, heartbeat, Endian.little)).buffer.asUint8List());
|
|
|
|
// State (1 byte)
|
|
data.add(state.index);
|
|
|
|
// Opcode (1 byte)
|
|
data.add(opcode.index);
|
|
|
|
// Key Hash (8 bytes)
|
|
data.addAll((ByteData(8)..setInt64(0, keyHash, Endian.little)).buffer.asUint8List());
|
|
|
|
// Write Pointer (8 bytes)
|
|
data.addAll((ByteData(8)..setInt64(0, writePointer.address, Endian.little)).buffer.asUint8List());
|
|
|
|
// Write Size (4 bytes)
|
|
data.addAll((ByteData(4)..setInt32(0, writeSize, Endian.little)).buffer.asUint8List());
|
|
|
|
return data;
|
|
}
|
|
|
|
String toString() {
|
|
return 'BT_Ticket(heartbeat: $heartbeat, state: $state, opcode: $opcode, keyHash: $keyHash, writePointer: $writePointer, writeSize: $writeSize)';
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class BinaryTable {
|
|
|
|
late final int sessionId;
|
|
|
|
RandomAccessFile _file;
|
|
|
|
BinaryTable(String path) : _file = File(path).openSync(mode: FileMode.append) {
|
|
|
|
var nextSessionId = Random.secure();
|
|
int high = nextSessionId.nextInt(1 << 32);
|
|
int low = nextSessionId.nextInt(1 << 32);
|
|
sessionId = (high << 32) | low;
|
|
|
|
// Check if the file is initialised
|
|
_file.setPositionSync(0);
|
|
List<int> magicNumber = _file.readSync(4);
|
|
bool isInitialised = magicNumber.length == 4 &&
|
|
magicNumber[0] == 'S'.codeUnitAt(0) &&
|
|
magicNumber[1] == 'W'.codeUnitAt(0) &&
|
|
magicNumber[2] == 'P'.codeUnitAt(0) &&
|
|
magicNumber[3] == 'S'.codeUnitAt(0);
|
|
|
|
if (isInitialised && !isMasterAlive) {
|
|
|
|
_initialiseMaster();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
void initialise({
|
|
int concurrentReaders = 4,
|
|
})
|
|
{
|
|
_file.setPositionSync(0);
|
|
|
|
// Ensure the file hasnt already been initialised
|
|
List<int> magicNumber = _file.readSync(4);
|
|
bool isInitialised = magicNumber.length == 4 &&
|
|
magicNumber[0] == 'S'.codeUnitAt(0) &&
|
|
magicNumber[1] == 'W'.codeUnitAt(0) &&
|
|
magicNumber[2] == 'P'.codeUnitAt(0) &&
|
|
magicNumber[3] == 'S'.codeUnitAt(0);
|
|
if (isInitialised) {
|
|
throw Exception('Binary Table file is already initialised.');
|
|
}
|
|
|
|
// Magic number "SWPS" (0/4 bytes)
|
|
_file.writeFromSync("SWPS".codeUnits);
|
|
|
|
// Version (1.0 float16) (4/2 bytes)
|
|
_file.writeFromSync(1.0._toFloat16Bytes());
|
|
|
|
// Address table pointer (null) (6/8 bytes)
|
|
_file.writePointerSync(BT_Null);
|
|
|
|
// Free list count (0) (14/4 bytes)
|
|
_file.writeIntSync(0, 4); // Free list entry count
|
|
|
|
/*
|
|
The values below are for concurrency.
|
|
*/
|
|
|
|
// Master Identifier (18/8 bytes)
|
|
_file.writeIntSync(sessionId & 0xFFFFFFFF, 8);
|
|
|
|
// Master Heartbeat (26/4 bytes)
|
|
int now = DateTime.now().millisecondsSinceEpoch;
|
|
_file.writeIntSync(now, 4);
|
|
|
|
// Number of concurrent readers (30/4 bytes) // Cannot be changed after initialisation
|
|
_file.writeIntSync(concurrentReaders, 4);
|
|
|
|
// Allow reads (34/1 bytes)
|
|
_file.writeByteSync(1);
|
|
|
|
// Everything else is operator slots starting at 35 bytes.
|
|
for (int i = 0; i < concurrentReaders; i++) {
|
|
|
|
// Slave Heartbeat (4 bytes)
|
|
_file.writeIntSync(0, 4);
|
|
|
|
// Ticket state (1 byte)
|
|
_file.writeByteSync(0);
|
|
|
|
// Ticket Opcode (1 byte)
|
|
_file.writeByteSync(0);
|
|
|
|
// Key Hash (8 bytes)
|
|
_file.writeIntSync(0, 8);
|
|
|
|
// Write Pointer (8 bytes)
|
|
_file.writeIntSync(-1, 8);
|
|
|
|
// Write Size (4 bytes)
|
|
_file.writeIntSync(0, 4);
|
|
|
|
}
|
|
|
|
// Run the master initialisation
|
|
_initialiseMaster();
|
|
}
|
|
|
|
/*
|
|
Address Table
|
|
*/
|
|
|
|
Map<int, BT_Pointer> get _addressTable {
|
|
|
|
_file.setPositionSync(6);
|
|
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(6);
|
|
BT_Reference oldTableRef = BT_Reference(this, _file.readPointerSync());
|
|
|
|
// Update header to point to new table
|
|
_file.setPositionSync(6);
|
|
_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);
|
|
}
|
|
}
|
|
|
|
/*
|
|
Free List
|
|
*/
|
|
|
|
bool freeListLifted = false;
|
|
List<BT_FreeListEntry>? _freeListCache;
|
|
List<BT_FreeListEntry> get _freeList {
|
|
|
|
if (freeListLifted) {
|
|
return _freeListCache ?? [];
|
|
}
|
|
|
|
_file.setPositionSync(14);
|
|
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() - 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;
|
|
}
|
|
|
|
// Read OLD count from header
|
|
_file.setPositionSync(14);
|
|
int oldEntryCount = _file.readIntSync(4);
|
|
|
|
// Calculate old free list size (entries only, not count)
|
|
int oldListSize = oldEntryCount * (BT_Type.POINTER.size + 4);
|
|
|
|
// Remove old free list entries from EOF
|
|
if (oldEntryCount > 0) {
|
|
int currentLength = _file.lengthSync();
|
|
_file.truncateSync(currentLength - oldListSize);
|
|
}
|
|
|
|
// Write NEW count to header
|
|
_file.setPositionSync(14);
|
|
_file.writeIntSync(list.length, 4);
|
|
|
|
// Write NEW entries to EOF (if any)
|
|
if (list.isNotEmpty) {
|
|
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');
|
|
}
|
|
|
|
// Cache the free list
|
|
_freeListCache = _freeList;
|
|
|
|
// Read count from header
|
|
_file.setPositionSync(14);
|
|
int oldEntryCount = _file.readIntSync(4);
|
|
|
|
if (oldEntryCount > 0) {
|
|
int oldEntrySize = BT_Type.POINTER.size + 4;
|
|
int oldFreeListSize = oldEntryCount * oldEntrySize; // Just entries, no count
|
|
|
|
// Remove free list entries from EOF
|
|
_file.truncateSync(_file.lengthSync() - oldFreeListSize);
|
|
}
|
|
|
|
// Clear count in header
|
|
_file.setPositionSync(14);
|
|
_file.writeIntSync(0, 4);
|
|
|
|
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');
|
|
}
|
|
|
|
freeListLifted = false;
|
|
_freeList = _freeListCache!; // This now writes count to header and entries to EOF
|
|
_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;
|
|
}
|
|
}
|
|
|
|
/*
|
|
Concurrency
|
|
*/
|
|
|
|
void _initialiseMaster() {
|
|
Isolate.spawn((String filePath) {
|
|
RandomAccessFile file = File(filePath).openSync(mode: FileMode.append);
|
|
|
|
// Print with yellow [MASTER] prefix - use ansi
|
|
void _mstPrint(String message) {
|
|
print('\x1B[33m[MASTER]\x1B[0m $message');
|
|
}
|
|
|
|
late final int concurrentReaders;
|
|
file.setPositionSync(30);
|
|
concurrentReaders = file.readIntSync(4);
|
|
|
|
while (true) {
|
|
|
|
int now = DateTime.now().millisecondsSinceEpoch32();
|
|
file.setPositionSync(26);
|
|
file.writeIntSync(now, 4);
|
|
|
|
_mstPrint('Master heartbeat updated at $now');
|
|
|
|
file.setPositionSync(35);
|
|
for (int i = 0; i < concurrentReaders; i++) {
|
|
|
|
// Read ticket
|
|
List<int> ticketBuffer = file.readSync(26);
|
|
BT_Ticket ticket = BT_Ticket.fromIntList(ticketBuffer);
|
|
|
|
// Check if ticket is alive and waiting
|
|
int ticketHeartbeat = ticket.heartbeat;
|
|
bool isTicketAlive = (now - ticket.heartbeat) <= 5000;
|
|
BT_TicketState state = ticket.state;
|
|
if (isTicketAlive && state == BT_TicketState.WAITING) {
|
|
_mstPrint("Ticket ${i} is alive and waiting...");
|
|
|
|
// If reading is still allowed, we need to disallow it
|
|
file.setPositionSync(34);
|
|
if (file.readByteSync() == 1) {
|
|
_mstPrint("Disallowing reads for ticket processing...");
|
|
file.setPositionSync(34);
|
|
file.writeByteSync(0);
|
|
// _liftFreeList(); // Broken/Incorrect Logic. the isolate cant access the main instance's free list
|
|
}
|
|
|
|
// We need to give it a write pointer and approve it
|
|
// BT_Pointer allocation = _alloc(ticket.writeSize); // Broken/Incorrect Logic. the isolate cant access the main instance's free list
|
|
|
|
BT_Ticket approvedTicket = BT_Ticket(
|
|
heartbeat: ticket.heartbeat,
|
|
state: BT_TicketState.APPROVED,
|
|
opcode: ticket.opcode,
|
|
keyHash: ticket.keyHash,
|
|
writeSize: ticket.writeSize,
|
|
// writePointer: allocation, // Broken/Incorrect Logic. the isolate cant access the main instance's free list
|
|
);
|
|
|
|
// Write approved ticket back to file
|
|
file.setPositionSync(35 + i * 26);
|
|
file.writeFromSync(approvedTicket.toIntList());
|
|
|
|
}
|
|
|
|
}
|
|
|
|
// If reads are disallowed, we need to allow them again, and drop the free list
|
|
file.setPositionSync(34);
|
|
if (file.readByteSync() == 0) {
|
|
_mstPrint("Allowing reads after ticket processing...");
|
|
file.setPositionSync(34);
|
|
file.writeByteSync(1);
|
|
dropFreeList();
|
|
}
|
|
|
|
sleep(Duration(seconds: 1));
|
|
}
|
|
}, _file.path);
|
|
}
|
|
|
|
bool get isMasterAlive {
|
|
_file.setPositionSync(18);
|
|
int masterId = _file.readIntSync(8);
|
|
|
|
_file.setPositionSync(26);
|
|
int masterHeartbeat = _file.readIntSync(4);
|
|
|
|
int now = DateTime.now().millisecondsSinceEpoch32();
|
|
return (now - masterHeartbeat) <= 5000;
|
|
}
|
|
|
|
int get concurrentReaders {
|
|
_file.setPositionSync(30);
|
|
return _file.readIntSync(4);
|
|
}
|
|
|
|
bool get allowReads {
|
|
_file.setPositionSync(34);
|
|
int flag = _file.readByteSync();
|
|
return flag == 1;
|
|
}
|
|
|
|
// Add ticket
|
|
void _ticket({
|
|
required BT_TicketOpcode operation,
|
|
required int keyHash,
|
|
required int writeSize,
|
|
required void Function(BT_Ticket ticket)? onApproved,
|
|
}) {
|
|
|
|
// Reduce the chance of a race condition/deadlock by adding a small random delay
|
|
sleep(Duration(milliseconds: Random().nextInt(10)));
|
|
|
|
int? ticketIndex;
|
|
|
|
// Were gonna iterate through all the tickets and find an empty slot
|
|
while (ticketIndex == null) {
|
|
|
|
_file.setPositionSync(35);
|
|
for (int i = 0; i < concurrentReaders; i++) {
|
|
|
|
List<int> ticketBuffer = _file.readSync(26); // Size of a ticket entry
|
|
|
|
BT_Ticket ticket = BT_Ticket.fromIntList(ticketBuffer);
|
|
|
|
// Check if the heartbeat is stale (older than 5 seconds)
|
|
int now = DateTime.now().millisecondsSinceEpoch64();
|
|
if (now - ticket.heartbeat > 5000) {
|
|
|
|
// We found a stale ticket, we can take this slot
|
|
ticketIndex = i;
|
|
break;
|
|
|
|
}
|
|
}
|
|
|
|
sleep(Duration(milliseconds: 500));
|
|
}
|
|
|
|
if (ticketIndex == null) {
|
|
throw Exception('Failed to acquire ticket');
|
|
}
|
|
|
|
BT_Ticket newTicket = BT_Ticket(
|
|
heartbeat: DateTime.now().millisecondsSinceEpoch32(),
|
|
state: BT_TicketState.WAITING,
|
|
opcode: operation,
|
|
keyHash: keyHash,
|
|
writeSize: writeSize,
|
|
);
|
|
|
|
// Write the new ticket to the file
|
|
_file.setPositionSync(35 + ticketIndex * 38);
|
|
_file.writeFromSync(newTicket.toIntList());
|
|
|
|
// Wait for approval
|
|
while (true) {
|
|
|
|
print('Waiting for ticket approval...');
|
|
|
|
_file.setPositionSync(35 + ticketIndex * 38);
|
|
List<int> ticketBuffer = _file.readSync(38);
|
|
BT_Ticket ticket = BT_Ticket.fromIntList(ticketBuffer);
|
|
|
|
if (ticket.state == BT_TicketState.APPROVED) {
|
|
// Ticket approved
|
|
onApproved?.call(ticket);
|
|
break;
|
|
}
|
|
|
|
if (!isMasterAlive) {
|
|
print('Master is not alive, cannot proceed with ticket');
|
|
}
|
|
|
|
sleep(Duration(milliseconds: 1000));
|
|
|
|
// Update heartbeat
|
|
_file.setPositionSync(35 + ticketIndex * 38);
|
|
int now = DateTime.now().millisecondsSinceEpoch32();
|
|
_file.writeIntSync(now, 4);
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
/*
|
|
Key-Value Operations
|
|
*/
|
|
|
|
operator []=(String key, dynamic value) {
|
|
|
|
Map<int, BT_Pointer> addressTable = _addressTable;
|
|
|
|
// Check if key already exists
|
|
int keyHash = key.bt_hash;
|
|
if (addressTable.containsKey(keyHash)) {
|
|
throw Exception('Key already exists'); // The pair should be deleted first
|
|
}
|
|
|
|
List<int> valueBuffer = encodeValue(value);
|
|
|
|
_ticket(
|
|
operation: BT_TicketOpcode.WRITE, // Modification operations will come later,
|
|
keyHash: keyHash,
|
|
writeSize: valueBuffer.length,
|
|
onApproved: (BT_Ticket ticket) {
|
|
// Write value to file
|
|
_file.setPositionSync(ticket.writePointer.address);
|
|
_file.writeFromSync(valueBuffer);
|
|
}
|
|
);
|
|
|
|
// v1.0.0 implementation
|
|
// _antiFreeListScope(() {
|
|
// Map<int, BT_Pointer> addressTable = _addressTable;
|
|
//
|
|
// int keyHash = key.bt_hash;
|
|
//
|
|
// // 1.1 Note: I cant remember why i did this check here.
|
|
// 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) {
|
|
|
|
while (!allowReads) {
|
|
// Wait until reads are allowed
|
|
sleep(Duration(milliseconds: 1));
|
|
}
|
|
|
|
Map<int, BT_Pointer> addressTable = _addressTable;
|
|
|
|
int keyHash = key.bt_hash;
|
|
|
|
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.bt_hash;
|
|
|
|
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]);
|
|
|
|
// Test the full read method
|
|
var intArrayFull = table["int_array"].fetchSublist(0, 3);
|
|
var floatArrayFull = table["float_array"].fetchSublist(0, 2);
|
|
print("Full read int_array (0-3): $intArrayFull");
|
|
print("Full read float_array (0-2): $floatArrayFull");
|
|
|
|
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();
|
|
}
|
|
|
|
// DateTime to 32-bit Unix timestamp (seconds since epoch)
|
|
extension on DateTime {
|
|
int millisecondsSinceEpoch32() {
|
|
return (millisecondsSinceEpoch ~/ 1000) & 0xFFFFFFFF;
|
|
}
|
|
int millisecondsSinceEpoch64() {
|
|
return millisecondsSinceEpoch;
|
|
}
|
|
} |