Files
SweepStore/dart/binary_table.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();
}