diff --git a/sqlite3/lib/common.dart b/sqlite3/lib/common.dart index e04c296a..48773140 100644 --- a/sqlite3/lib/common.dart +++ b/sqlite3/lib/common.dart @@ -10,4 +10,3 @@ export 'src/result_set.dart'; export 'src/sqlite3.dart'; export 'src/statement.dart' show CommonPreparedStatement, StatementParameters, CustomStatementParameter; -export 'src/vfs.dart'; diff --git a/sqlite3/lib/src/wasm/bindings.dart b/sqlite3/lib/src/wasm/bindings.dart index ca9026b3..4e2aa133 100644 --- a/sqlite3/lib/src/wasm/bindings.dart +++ b/sqlite3/lib/src/wasm/bindings.dart @@ -1,12 +1,14 @@ import 'dart:collection'; import 'dart:convert'; +import 'dart:js_interop'; import 'dart:typed_data'; -import 'package:sqlite3/src/vfs.dart'; +import 'package:sqlite3/src/wasm/vfs.dart'; import '../constants.dart'; import '../functions.dart'; import '../implementation/bindings.dart'; +import 'js_interop/typed_data.dart'; import 'wasm_interop.dart' as wasm; import 'wasm_interop.dart'; @@ -134,7 +136,7 @@ final class WasmDatabase extends RawSqliteDatabase { @override RawStatementCompiler newCompiler(List utf8EncodedSql) { - final ptr = bindings.allocateBytes(utf8EncodedSql); + final ptr = bindings.allocateBytes(_viewOrCopyByteList(utf8EncodedSql)); return WasmStatementCompiler(this, ptr); } @@ -148,7 +150,8 @@ final class WasmDatabase extends RawSqliteDatabase { required int eTextRep, required RawCollation collation, }) { - final ptr = bindings.allocateBytes(collationName, additionalLength: 1); + final ptr = bindings.allocateBytes(SafeU8Array(collationName.toJS), + additionalLength: 1); final result = bindings.create_collation( db, ptr, @@ -170,7 +173,8 @@ final class WasmDatabase extends RawSqliteDatabase { RawXStep? xStep, RawXFinal? xFinal, }) { - final ptr = bindings.allocateBytes(functionName, additionalLength: 1); + final ptr = bindings.allocateBytes(SafeU8Array(functionName.toJS), + additionalLength: 1); final int result; if (xFunc != null) { @@ -209,7 +213,8 @@ final class WasmDatabase extends RawSqliteDatabase { required RawXFinal xValue, required RawXStep xInverse, }) { - final ptr = bindings.allocateBytes(functionName, additionalLength: 1); + final ptr = bindings.allocateBytes(SafeU8Array(functionName.toJS), + additionalLength: 1); final result = bindings.create_window_function( db, ptr, @@ -319,7 +324,7 @@ final class WasmStatement extends RawSqliteStatement { @override void sqlite3_bind_blob64(int index, List value) { - final ptr = bindings.allocateBytes(value); + final ptr = bindings.allocateBytes(_viewOrCopyByteList(value)); _allocatedArguments.add(ptr); bindings.sqlite3_bind_blob64(stmt, index, ptr, value.length, 0); @@ -373,7 +378,7 @@ final class WasmStatement extends RawSqliteStatement { @override void sqlite3_bind_text(int index, String value) { final encoded = utf8.encode(value); - final ptr = bindings.allocateBytes(encoded); + final ptr = bindings.allocateBytes(SafeU8Array(encoded.toJS)); _allocatedArguments.add(ptr); bindings.sqlite3_bind_text(stmt, index, ptr, encoded.length, 0); @@ -494,7 +499,7 @@ final class WasmContext extends RawSqliteContext { @override void sqlite3_result_blob64(List blob) { - final ptr = bindings.allocateBytes(blob); + final ptr = bindings.allocateBytes(_viewOrCopyByteList(blob)); bindings.sqlite3_result_blob64( context, ptr, blob.length, SqlSpecialDestructor.SQLITE_TRANSIENT); @@ -509,7 +514,7 @@ final class WasmContext extends RawSqliteContext { @override void sqlite3_result_error(String message) { final encoded = utf8.encode(message); - final ptr = bindings.allocateBytes(encoded); + final ptr = bindings.allocateBytes(SafeU8Array(encoded.toJS)); bindings.sqlite3_result_error(context, ptr, encoded.length); bindings.free(ptr); @@ -533,7 +538,7 @@ final class WasmContext extends RawSqliteContext { @override void sqlite3_result_text(String text) { final encoded = utf8.encode(text); - final ptr = bindings.allocateBytes(encoded); + final ptr = bindings.allocateBytes(SafeU8Array(encoded.toJS)); bindings.sqlite3_result_text( context, ptr, encoded.length, SqlSpecialDestructor.SQLITE_TRANSIENT); @@ -602,3 +607,11 @@ class WasmValueList extends ListBase { throw UnsupportedError('Setting element in WasmValueList'); } } + +SafeU8Array _viewOrCopyByteList(List bytes) { + if (bytes is Uint8List) { + return SafeU8Array(bytes.toJS); + } else { + return SafeU8Array(Uint8List.fromList(bytes).toJS); + } +} diff --git a/sqlite3/lib/src/wasm/js_interop.dart b/sqlite3/lib/src/wasm/js_interop.dart index b539dbc4..345936df 100644 --- a/sqlite3/lib/src/wasm/js_interop.dart +++ b/sqlite3/lib/src/wasm/js_interop.dart @@ -1,7 +1,7 @@ import 'dart:js_interop'; -import 'dart:typed_data'; import 'package:web/web.dart' show Blob; +import 'js_interop/typed_data.dart'; // This internal library exports wrappers around newer Web APIs for which no // up-to-date bindings exist in the Dart SDK. @@ -15,8 +15,8 @@ export 'js_interop/typed_data.dart'; export 'js_interop/wasm.dart'; extension ReadBlob on Blob { - Future byteBuffer() async { + Future byteBuffer() async { final buffer = await arrayBuffer().toDart; - return buffer.toDart; + return SafeBuffer(buffer); } } diff --git a/sqlite3/lib/src/wasm/js_interop/atomics.dart b/sqlite3/lib/src/wasm/js_interop/atomics.dart index 902e2499..4ee55dcd 100644 --- a/sqlite3/lib/src/wasm/js_interop/atomics.dart +++ b/sqlite3/lib/src/wasm/js_interop/atomics.dart @@ -1,7 +1,8 @@ -import 'dart:typed_data'; import 'dart:js_interop'; import 'dart:js_interop_unsafe'; +import 'typed_data.dart'; + @JS('Int32Array') external JSFunction _int32Array; @@ -17,24 +18,22 @@ extension type SharedArrayBuffer._(JSObject _) implements JSObject { external int get byteLength; - Int32List asInt32List() { - return _int32Array.callAsConstructor(this).toDart; + SafeI32Array asInt32List() { + return SafeI32Array(_int32Array.callAsConstructor(this)); } - ByteData asByteData(int offset, int length) { - return _dataView - .callAsConstructor(this, offset.toJS, length.toJS) - .toDart; + SafeDataView asByteData(int offset, int length) { + return SafeDataView(_dataView.callAsConstructor( + this, offset.toJS, length.toJS)); } - Uint8List asUint8List() { - return _uint8Array.callAsConstructor(this).toDart; + SafeU8Array asUint8List() { + return SafeU8Array(_uint8Array.callAsConstructor(this)); } - Uint8List asUint8ListSlice(int offset, int length) { - return _uint8Array - .callAsConstructor(this, offset.toJS, length.toJS) - .toDart; + SafeU8Array asUint8ListSlice(int offset, int length) { + return SafeU8Array(_uint8Array.callAsConstructor( + this, offset.toJS, length.toJS)); } } @@ -66,27 +65,27 @@ class Atomics { return globalContext.has('Atomics'); } - static String wait(Int32List typedArray, int index, int value) { - return _Atomics.wait(typedArray.toJS, index, value).toDart; + static String wait(SafeI32Array typedArray, int index, int value) { + return _Atomics.wait(typedArray.inner, index, value).toDart; } static String waitWithTimeout( - Int32List typedArray, int index, int value, int timeOutInMillis) { + SafeI32Array typedArray, int index, int value, int timeOutInMillis) { return _Atomics.waitWithTimeout( - typedArray.toJS, index, value, timeOutInMillis) + typedArray.inner, index, value, timeOutInMillis) .toDart; } - static void notify(Int32List typedArray, int index, + static void notify(SafeI32Array typedArray, int index, [num count = double.infinity]) { - _Atomics.notify(typedArray.toJS, index, count); + _Atomics.notify(typedArray.inner, index, count); } - static int store(Int32List typedArray, int index, int value) { - return _Atomics.store(typedArray.toJS, index, value); + static int store(SafeI32Array typedArray, int index, int value) { + return _Atomics.store(typedArray.inner, index, value); } - static int load(Int32List typedArray, int index) { - return _Atomics.load(typedArray.toJS, index); + static int load(SafeI32Array typedArray, int index) { + return _Atomics.load(typedArray.inner, index); } } diff --git a/sqlite3/lib/src/wasm/js_interop/new_file_system_access.dart b/sqlite3/lib/src/wasm/js_interop/new_file_system_access.dart index ddd13560..8180779f 100644 --- a/sqlite3/lib/src/wasm/js_interop/new_file_system_access.dart +++ b/sqlite3/lib/src/wasm/js_interop/new_file_system_access.dart @@ -1,10 +1,10 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'dart:typed_data'; import 'package:web/web.dart'; import 'core.dart'; +import 'typed_data.dart'; @JS('navigator') external Navigator get _navigator; @@ -24,19 +24,19 @@ extension StorageManagerApi on StorageManager { } extension FileSystemSyncAccessHandleApi on FileSystemSyncAccessHandle { - int readDart(Uint8List buffer, [FileSystemReadWriteOptions? options]) { + int readDart(SafeU8Array buffer, [FileSystemReadWriteOptions? options]) { if (options == null) { - return read(buffer.toJS); + return read(buffer); } else { - return read(buffer.toJS, options); + return read(buffer, options); } } - int writeDart(Uint8List buffer, [FileSystemReadWriteOptions? options]) { + int writeDart(SafeU8Array buffer, [FileSystemReadWriteOptions? options]) { if (options == null) { - return write(buffer.toJS); + return write(buffer); } else { - return write(buffer.toJS, options); + return write(buffer, options); } } } diff --git a/sqlite3/lib/src/wasm/js_interop/typed_data.dart b/sqlite3/lib/src/wasm/js_interop/typed_data.dart index 99bea285..6d96282a 100644 --- a/sqlite3/lib/src/wasm/js_interop/typed_data.dart +++ b/sqlite3/lib/src/wasm/js_interop/typed_data.dart @@ -1,21 +1,87 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; -import 'dart:typed_data'; import 'core.dart'; -extension NativeUint8List on Uint8List { - /// A native version of [setRange] that takes another typed array directly. - /// This avoids the type checks part of [setRange] in compiled JavaScript - /// code. - void set(Uint8List from, int offset) { - toJS.callMethod('set'.toJS, from.toJS, offset.toJS); +// These types provide wrappers around JS typeddata objects that don't have +// the toDart extension on them. +// Using toDart is unsafe as it creates a reference in dart2js while copying in +// dart2wasm. This causes issues that are very hard to find, so we should be +// very explicit about where we copy and explain why that's safe. + +extension type SafeBuffer(JSArrayBuffer inner) implements JSObject { + SafeU8Array asUint8Array([int offsetInBytes = 0, int? length]) { + return switch (length) { + null => SafeU8Array._bufferView(this, offsetInBytes), + var length => + SafeU8Array._bufferViewWithLength(this, offsetInBytes, length), + }; } } -extension NativeDataView on ByteData { - void setBigInt64(int offset, JsBigInt value, bool littleEndian) { - toJS.callMethod( - 'setBigInt64'.toJS, offset.toJS, value.jsObject, littleEndian.toJS); +@JS('DataView') +extension type SafeDataView(JSDataView inner) implements JSObject { + @JS('') + external factory SafeDataView.entireBufferView(SafeBuffer buffer); + + external void setInt32(int byteOffset, int value); + external int getInt32(int byteOffset); + + external void setBigInt64(int offset, JsBigInt value, bool littleEndian); +} + +@JS('Uint8Array') +extension type SafeU8Array(JSUint8Array inner) implements JSObject { + @JS('') + external factory SafeU8Array.allocate(int length); + + @JS('') + external factory SafeU8Array._bufferView(SafeBuffer buffer, int byteOffset); + + @JS('') + external factory SafeU8Array._bufferViewWithLength( + SafeBuffer buffer, int byteOffset, int length); + + external void set(JSTypedArray array, int targetOffset); + external void fill(int value, int start, int end); + + external SafeU8Array subarray(int begin, int end); + + external int get length; + + int operator [](int index) { + return getProperty(index.toJS).toDartInt; + } + + void operator []=(int index, int value) { + setProperty(index.toJS, value.toJS); + } + + /// Implementation of [List.setRange] for wrapped u8 arrays. + void setRange(int start, int end, SafeU8Array other, [int skipCount = 0]) { + if (skipCount == 0 && end - start == other.length) { + set(other.inner, start); + } else { + set(other.subarray(skipCount, skipCount + (end - start)).inner, start); + } + } + + /// Implementation of [List.setAll] for wrapped u8 arrays. + void setAll(int index, SafeU8Array other) { + set(other.inner, index); + } +} + +@JS('Int32Array') +extension type SafeI32Array(JSInt32Array inner) implements JSObject { + @JS('') + external factory SafeI32Array.entireBufferView(SafeBuffer buffer); + + int operator [](int index) { + return getProperty(index.toJS).toDartInt; + } + + void operator []=(int index, int value) { + setProperty(index.toJS, value.toJS); } } diff --git a/sqlite3/lib/src/wasm/js_interop/wasm.dart b/sqlite3/lib/src/wasm/js_interop/wasm.dart index 8f8e2561..40b0574c 100644 --- a/sqlite3/lib/src/wasm/js_interop/wasm.dart +++ b/sqlite3/lib/src/wasm/js_interop/wasm.dart @@ -5,6 +5,7 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'core.dart'; +import 'typed_data.dart'; import 'package:web/web.dart' as web; @@ -71,7 +72,7 @@ extension type MemoryDescriptor._(JSObject _) implements JSObject { extension type Memory._(JSObject _) implements JSObject { external factory Memory(MemoryDescriptor descriptor); - external JSArrayBuffer get buffer; + external SafeBuffer get buffer; } @JS('WebAssembly.Global') diff --git a/sqlite3/lib/src/wasm/sqlite3.dart b/sqlite3/lib/src/wasm/sqlite3.dart index 498daa11..94051395 100644 --- a/sqlite3/lib/src/wasm/sqlite3.dart +++ b/sqlite3/lib/src/wasm/sqlite3.dart @@ -5,7 +5,7 @@ import 'dart:typed_data'; import 'package:web/web.dart' as web; import '../implementation/sqlite3.dart'; -import '../vfs.dart'; +import 'vfs.dart'; import 'bindings.dart'; import 'js_interop.dart'; import 'wasm_interop.dart'; diff --git a/sqlite3/lib/src/vfs.dart b/sqlite3/lib/src/wasm/vfs.dart similarity index 93% rename from sqlite3/lib/src/vfs.dart rename to sqlite3/lib/src/wasm/vfs.dart index 7b843bca..622cde11 100644 --- a/sqlite3/lib/src/vfs.dart +++ b/sqlite3/lib/src/wasm/vfs.dart @@ -1,7 +1,7 @@ import 'dart:math'; -import 'dart:typed_data'; -import 'constants.dart'; +import '../constants.dart'; +import 'js_interop/typed_data.dart'; /// An exception thrown by [VirtualFileSystem] implementations written in Dart /// to signal that an operation could not be completed. @@ -68,7 +68,7 @@ abstract base class VirtualFileSystem { /// /// __Safety warning__: Target may be a direct view over native memory that /// must not be used after this function returns. - void xRandomness(Uint8List target); + void xRandomness(SafeU8Array target); /// Sleeps for the passed [duration]. void xSleep(Duration duration); @@ -95,7 +95,7 @@ abstract interface class VirtualFileSystemFile { /// /// __Safety warning__: Target may be a direct view over native memory that /// must not be used after this function returns. - void xRead(Uint8List target, int fileOffset); + void xRead(SafeU8Array target, int fileOffset); /// Writes the [buffer] into this file at [fileOffset], overwriting existing /// content or appending to it. @@ -105,7 +105,7 @@ abstract interface class VirtualFileSystemFile { /// /// __Safety warning__: Target may be a direct view over native memory that /// must not be used after this function returns. - void xWrite(Uint8List buffer, int fileOffset); + void xWrite(SafeU8Array buffer, int fileOffset); /// Truncates this file to a size of [size]. void xTruncate(int size); @@ -139,7 +139,7 @@ abstract base class BaseVirtualFileSystem extends VirtualFileSystem { super(name); @override - void xRandomness(Uint8List target) { + void xRandomness(SafeU8Array target) { for (var i = 0; i < target.length; i++) { target[i] = random.nextInt(1 << 8); } @@ -157,18 +157,18 @@ abstract class BaseVfsFile implements VirtualFileSystemFile { /// /// __Safety warning__: [bufer] may be a direct view over native memory that /// must not be used after this function returns. - int readInto(Uint8List buffer, int offset); + int readInto(SafeU8Array buffer, int offset); @override int get xDeviceCharacteristics => 0; @override - void xRead(Uint8List target, int fileOffset) { + void xRead(SafeU8Array target, int fileOffset) { final bytesRead = readInto(target, fileOffset); if (bytesRead < target.length) { // Remaining buffer must be filled with zeroes. - target.fillRange(bytesRead, target.length, 0); + target.fill(0, bytesRead, target.length); // And we need to return a short read error throw const VfsException(SqlExtendedError.SQLITE_IOERR_SHORT_READ); diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart index a3aeba70..1b3925af 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/client.dart @@ -1,12 +1,11 @@ import 'dart:js_interop'; import 'dart:js_interop_unsafe'; import 'dart:math'; -import 'dart:typed_data'; import 'package:path/path.dart' as p; import '../../../constants.dart'; -import '../../../vfs.dart'; +import '../../vfs.dart'; import '../../js_interop.dart'; import '../utils.dart'; import 'sync_channel.dart'; @@ -113,7 +112,7 @@ class WasmFile extends BaseVfsFile { } @override - int readInto(Uint8List buffer, int offset) { + int readInto(SafeU8Array buffer, int offset) { var remainingBytes = buffer.length; var totalBytesRead = 0; @@ -129,7 +128,8 @@ class WasmFile extends BaseVfsFile { final bytesRead = result.flag0; // Copy read bytes into result buffer. - buffer.set(vfs.serializer.viewByteRange(0, bytesRead), totalBytesRead); + buffer.set( + vfs.serializer.byteView.subarray(0, bytesRead).inner, totalBytesRead); totalBytesRead += bytesRead; if (bytesRead < bytesToRead) { @@ -194,7 +194,7 @@ class WasmFile extends BaseVfsFile { } @override - void xWrite(Uint8List buffer, int fileOffset) { + void xWrite(SafeU8Array buffer, int fileOffset) { var remainingBytes = buffer.length; var totalBytesWritten = 0; @@ -205,8 +205,8 @@ class WasmFile extends BaseVfsFile { final subBuffer = bytesToWrite == remainingBytes ? buffer - : buffer.buffer.asUint8List(buffer.offsetInBytes, bytesToWrite); - vfs.serializer.byteView.set(subBuffer, 0); + : buffer.subarray(0, bytesToWrite); + vfs.serializer.byteView.set(subBuffer.inner, 0); vfs._runInWorker(WorkerOperation.xWrite, Flags(fd, fileOffset + totalBytesWritten, bytesToWrite)); diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart index 938e3d25..81a4c394 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/sync_channel.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:js_interop'; import 'dart:typed_data'; import '../../js_interop.dart'; @@ -21,7 +22,7 @@ class RequestResponseSynchronizer { final SharedArrayBuffer buffer; /// A int32 view over [buffer], required for atomics to work. - final Int32List int32View; + final SafeI32Array int32View; RequestResponseSynchronizer._(this.buffer) : int32View = buffer.asInt32List(); @@ -71,8 +72,8 @@ class MessageSerializer { static const totalSize = metaOffset + metaSize; final SharedArrayBuffer buffer; - final ByteData dataView; - final Uint8List byteView; + final SafeDataView dataView; + final SafeU8Array byteView; MessageSerializer(this.buffer) : dataView = buffer.asByteData(metaOffset, metaSize), @@ -94,19 +95,21 @@ class MessageSerializer { } } - Uint8List viewByteRange(int offset, int length) { - return buffer.asUint8ListSlice(offset, length); + /// Returns a view (dart2js) or copy (dart2wasm) of the the underlying byte + /// buffer from [offset] with length [length]. + Uint8List viewOrCopyByteRange(int offset, int length) { + return buffer.asUint8ListSlice(offset, length).inner.toDart; } String _readString(int offset) { final length = dataView.getInt32(offset); - return utf8.decode(buffer.asUint8ListSlice(offset + 4, length)); + return utf8.decode(viewOrCopyByteRange(offset + 4, length)); } void _writeString(int offset, String data) { final encoded = utf8.encode(data); dataView.setInt32(offset, encoded.length); - byteView.setAll(offset + 4, encoded); + byteView.set(encoded.toJS, offset + 4); } static EmptyMessage readEmpty(MessageSerializer unused) { diff --git a/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart b/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart index 24f371f1..9aac2bd1 100644 --- a/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart +++ b/sqlite3/lib/src/wasm/vfs/async_opfs/worker.dart @@ -13,7 +13,7 @@ import 'package:web/web.dart' FileSystemReadWriteOptions; import '../../../constants.dart'; -import '../../../vfs.dart'; +import '../../vfs.dart'; import '../../js_interop.dart'; import 'sync_channel.dart'; @@ -175,7 +175,7 @@ class VfsWorker { final syncHandle = await _openForSynchronousAccess(file); final bytesRead = syncHandle.readDart( - messages.viewByteRange(0, bufferLength), + messages.byteView.subarray(0, bufferLength), FileSystemReadWriteOptions(at: offset)); return Flags(bytesRead, 0, 0); @@ -189,7 +189,7 @@ class VfsWorker { final syncHandle = await _openForSynchronousAccess(file); final bytesWritten = syncHandle.writeDart( - messages.viewByteRange(0, bufferLength), + messages.byteView.subarray(0, bufferLength), FileSystemReadWriteOptions(at: offset)); if (bytesWritten != bufferLength) { diff --git a/sqlite3/lib/src/wasm/vfs/indexed_db.dart b/sqlite3/lib/src/wasm/vfs/indexed_db.dart index 34bd641a..fbe359d4 100644 --- a/sqlite3/lib/src/wasm/vfs/indexed_db.dart +++ b/sqlite3/lib/src/wasm/vfs/indexed_db.dart @@ -5,13 +5,12 @@ import 'dart:async'; import 'dart:collection'; import 'dart:js_interop'; import 'dart:math'; -import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:web/web.dart' as web; import '../../constants.dart'; -import '../../vfs.dart'; +import '../vfs.dart'; import '../js_interop.dart'; import 'memory.dart'; import 'utils.dart'; @@ -141,12 +140,12 @@ class AsynchronousIndexedDbFileSystem { }); } - Future readFully(int fileId) async { + Future readFully(int fileId) async { final transaction = _database!.transaction(_storesJs, 'readonly'); final blocks = transaction.objectStore(_blocksStore); final file = await _readFile(transaction, fileId); - final result = Uint8List(file.length); + final result = SafeU8Array.allocate(file.length); final readOperations = >[]; @@ -163,7 +162,7 @@ class AsynchronousIndexedDbFileSystem { // transaction. Launch the reader now and wait for all reads later. readOperations.add(Future.sync(() async { final data = await (row.value as web.Blob).byteBuffer(); - result.setAll(rowOffset, data.asUint8List(0, length)); + result.setAll(rowOffset, data.asUint8Array(0, length)); })); } await Future.wait(readOperations); @@ -171,7 +170,7 @@ class AsynchronousIndexedDbFileSystem { return result; } - Future read(int fileId, int offset, Uint8List target) async { + Future read(int fileId, int offset, SafeU8Array target) async { final transaction = _database!.transaction(_storesJs, 'readonly'); final blocks = transaction.objectStore(_blocksStore); @@ -205,10 +204,7 @@ class AsynchronousIndexedDbFileSystem { final data = await blob.byteBuffer(); target.setRange( - 0, - lengthToCopy, - data.asUint8List(startInRow, lengthToCopy), - ); + 0, lengthToCopy, data.asUint8Array(startInRow, lengthToCopy)); })); if (lengthToCopy >= target.length) { @@ -226,7 +222,7 @@ class AsynchronousIndexedDbFileSystem { readOperations.add(Future.sync(() async { final data = await blob.byteBuffer(); - target.setAll(startInTarget, data.asUint8List(0, lengthToCopy)); + target.setAll(startInTarget, data.asUint8Array(0, lengthToCopy)); })); if (lengthToCopy >= target.length - startInTarget) { @@ -244,14 +240,14 @@ class AsynchronousIndexedDbFileSystem { final blocks = transaction.objectStore(_blocksStore); final file = await _readFile(transaction, fileId); - Future writeBlock(int blockStart, Uint8List block) async { + Future writeBlock(int blockStart, SafeU8Array block) async { assert(block.length == _blockSize, 'Invalid block size'); // Check if we're overriding (parts of) an existing block final cursor = await blocks .openCursor(web.IDBKeyRange.only([fileId.toJS, blockStart.toJS].toJS)) .complete(); - final blob = web.Blob([block.toJS].toJS); + final blob = web.Blob([block].toJS); if (cursor == null) { // There isn't, let's write a new block @@ -333,23 +329,25 @@ extension type _FileEntry._(JSObject _) implements JSObject { class _FileWriteRequest { static const _blockLength = AsynchronousIndexedDbFileSystem._blockSize; - final Uint8List originalContent; - final Map replacedBlocks = {}; + final SafeU8Array originalContent; + final Map replacedBlocks = {}; int newFileLength; _FileWriteRequest(this.originalContent) : newFileLength = originalContent.length; - void _updateBlock(int blockOffset, int offsetInBlock, Uint8List data) { + void _updateBlock(int blockOffset, int offsetInBlock, SafeU8Array data) { final block = replacedBlocks.putIfAbsent(blockOffset, () { - final block = Uint8List(_blockLength); + final block = SafeU8Array.allocate(_blockLength); if (originalContent.length > blockOffset) { + // We might only be changing parts of a block, so copy the original data + // that would have been part of this block. block.setAll( 0, - originalContent.buffer.asUint8List( - originalContent.offsetInBytes + blockOffset, - min(_blockLength, originalContent.length - blockOffset), + originalContent.subarray( + blockOffset, + min(originalContent.length, blockOffset + _blockLength), ), ); } @@ -360,7 +358,7 @@ class _FileWriteRequest { block.setAll(offsetInBlock, data); } - void addWrite(int offset, Uint8List data) { + void addWrite(int offset, SafeU8Array data) { var offsetInData = 0; while (offsetInData < data.length) { final offsetInFile = offset + offsetInData; @@ -379,8 +377,7 @@ class _FileWriteRequest { offsetInBlock = 0; } - final chunk = data.buffer - .asUint8List(data.offsetInBytes + offsetInData, bytesToWrite); + final chunk = data.subarray(offsetInData, offsetInData + bytesToWrite); offsetInData += bytesToWrite; _updateBlock(blockStart, offsetInBlock, chunk); @@ -392,7 +389,7 @@ class _FileWriteRequest { class _OffsetAndBuffer { final int offset; - final Uint8List buffer; + final SafeU8Array buffer; _OffsetAndBuffer(this.offset, this.buffer); } @@ -599,7 +596,7 @@ class _IndexedDbFile implements VirtualFileSystemFile { _IndexedDbFile(this.vfs, this.memoryFile, this.path); @override - void xRead(Uint8List target, int fileOffset) { + void xRead(SafeU8Array target, int fileOffset) { memoryFile.xRead(target, fileOffset); } @@ -639,17 +636,18 @@ class _IndexedDbFile implements VirtualFileSystemFile { void xUnlock(int mode) => memoryFile.xUnlock(mode); @override - void xWrite(Uint8List buffer, int fileOffset) { + void xWrite(SafeU8Array buffer, int fileOffset) { vfs._checkClosed(); - final previousContent = vfs._memory.fileData[path] ?? Uint8List(0); + final previousContent = + vfs._memory.fileData[path] ?? SafeU8Array.allocate(0); memoryFile.xWrite(buffer, fileOffset); if (!vfs._inMemoryOnlyFiles.contains(path)) { // We need to copy the buffer for the write because it will become invalid // after this synchronous method returns. - final copy = Uint8List(buffer.length); - copy.setAll(0, buffer); + final copy = SafeU8Array.allocate(buffer.length); + copy.set(buffer.inner, 0); vfs._submitWork(_WriteFileWorkItem(vfs, path, previousContent) ..writes.add(_OffsetAndBuffer(fileOffset, copy))); @@ -768,7 +766,7 @@ final class _WriteFileWorkItem extends _IndexedDbWorkItem { final IndexedDbFileSystem fileSystem; final String path; - final Uint8List originalContent; + final SafeU8Array originalContent; final List<_OffsetAndBuffer> writes = []; _WriteFileWorkItem(this.fileSystem, this.path, this.originalContent); diff --git a/sqlite3/lib/src/wasm/vfs/memory.dart b/sqlite3/lib/src/wasm/vfs/memory.dart index 236b9cf5..cb703b86 100644 --- a/sqlite3/lib/src/wasm/vfs/memory.dart +++ b/sqlite3/lib/src/wasm/vfs/memory.dart @@ -1,14 +1,14 @@ import 'dart:math'; -import 'dart:typed_data'; import 'package:path/path.dart' as p; import '../../constants.dart'; -import '../../vfs.dart'; +import '../js_interop/typed_data.dart'; +import '../vfs.dart'; import 'utils.dart'; final class InMemoryFileSystem extends BaseVirtualFileSystem { - final Map fileData = {}; + final Map fileData = {}; InMemoryFileSystem({super.name = 'dart-memory', super.random}); @@ -34,7 +34,7 @@ final class InMemoryFileSystem extends BaseVirtualFileSystem { final create = flags & SqlFlag.SQLITE_OPEN_CREATE; if (create != 0) { - fileData[pathStr] = Uint8List(0); + fileData[pathStr] = SafeU8Array.allocate(0); } else { throw VfsException(SqlError.SQLITE_CANTOPEN); } @@ -64,7 +64,7 @@ class _InMemoryFile extends BaseVfsFile { _InMemoryFile(this.vfs, this.path, this.deleteOnClose); @override - int readInto(Uint8List buffer, int offset) { + int readInto(SafeU8Array buffer, int offset) { final file = vfs.fileData[path]; if (file == null || file.length <= offset) return 0; @@ -102,7 +102,7 @@ class _InMemoryFile extends BaseVfsFile { void xTruncate(int size) { final file = vfs.fileData[path]; - final result = Uint8List(size); + final result = SafeU8Array.allocate(size); if (file != null) { result.setRange(0, min(size, file.length), file); } @@ -116,8 +116,8 @@ class _InMemoryFile extends BaseVfsFile { } @override - void xWrite(Uint8List buffer, int fileOffset) { - final file = vfs.fileData[path] ?? Uint8List(0); + void xWrite(SafeU8Array buffer, int fileOffset) { + final file = vfs.fileData[path] ?? SafeU8Array.allocate(0); final increasedSize = fileOffset + buffer.length - file.length; if (increasedSize <= 0) { @@ -125,10 +125,9 @@ class _InMemoryFile extends BaseVfsFile { file.setRange(fileOffset, fileOffset + buffer.length, buffer); } else { // We need to grow the file first - final newFile = Uint8List(file.length + increasedSize) + final newFile = SafeU8Array.allocate(file.length + increasedSize) ..setAll(0, file) ..setAll(fileOffset, buffer); - vfs.fileData[path] = newFile; } } diff --git a/sqlite3/lib/src/wasm/vfs/simple_opfs.dart b/sqlite3/lib/src/wasm/vfs/simple_opfs.dart index d36d1a78..0368154a 100644 --- a/sqlite3/lib/src/wasm/vfs/simple_opfs.dart +++ b/sqlite3/lib/src/wasm/vfs/simple_opfs.dart @@ -1,5 +1,4 @@ import 'dart:js_interop'; -import 'dart:typed_data'; import 'package:meta/meta.dart'; import 'package:path/path.dart' as p; @@ -10,7 +9,7 @@ import 'package:web/web.dart' FileSystemReadWriteOptions; import '../../constants.dart'; -import '../../vfs.dart'; +import '../vfs.dart'; import '../js_interop.dart'; import 'memory.dart'; @@ -59,7 +58,7 @@ final class SimpleOpfsFileSystem extends BaseVirtualFileSystem { // done asynchronously. final FileSystemSyncAccessHandle _metaHandle; - final Uint8List _existsList = Uint8List(FileType.values.length); + final SafeU8Array _existsList = SafeU8Array.allocate(FileType.values.length); final Map _files; final InMemoryFileSystem _memory = InMemoryFileSystem(); @@ -202,7 +201,7 @@ class _SimpleOpfsFile extends BaseVfsFile { _SimpleOpfsFile(this.vfs, this.type, this.syncHandle, this.deleteOnClose); @override - int readInto(Uint8List buffer, int offset) { + int readInto(SafeU8Array buffer, int offset) { return syncHandle.readDart(buffer, FileSystemReadWriteOptions(at: offset)); } @@ -246,7 +245,7 @@ class _SimpleOpfsFile extends BaseVfsFile { } @override - void xWrite(Uint8List buffer, int fileOffset) { + void xWrite(SafeU8Array buffer, int fileOffset) { final bytesWritten = syncHandle.writeDart( buffer, FileSystemReadWriteOptions(at: fileOffset)); diff --git a/sqlite3/lib/src/wasm/wasm_interop.dart b/sqlite3/lib/src/wasm/wasm_interop.dart index 75a1ca94..9f01d7ae 100644 --- a/sqlite3/lib/src/wasm/wasm_interop.dart +++ b/sqlite3/lib/src/wasm/wasm_interop.dart @@ -187,17 +187,18 @@ class WasmBindings { return function; } - Pointer allocateBytes(List bytes, {int additionalLength = 0}) { + Pointer allocateBytes(SafeU8Array bytes, {int additionalLength = 0}) { final ptr = malloc(bytes.length + additionalLength); memory.asBytes - ..setRange(ptr, ptr + bytes.length, bytes) - ..fillRange(ptr + bytes.length, ptr + bytes.length + additionalLength, 0); + ..set(bytes.inner, ptr) + ..fill(0, ptr + bytes.length, ptr + bytes.length + additionalLength); return ptr; } Pointer allocateZeroTerminated(String string) { - return allocateBytes(utf8.encode(string), additionalLength: 1); + return allocateBytes(SafeU8Array(utf8.encode(string).toJS), + additionalLength: 1); } Pointer malloc(int size) { @@ -483,17 +484,15 @@ int _runVfs(void Function() body) { } extension WrappedMemory on Memory { - ByteBuffer get dartBuffer => buffer.toDart; - - Uint8List get asBytes => buffer.toDart.asUint8List(); + SafeI32Array get asInt32 => SafeI32Array.entireBufferView(buffer); + SafeU8Array get asBytes => buffer.asUint8Array(); int strlen(int address) { assert(address != 0, 'Null pointer dereference'); - - final bytes = dartBuffer.asUint8List(address); + final bytes = asBytes; var length = 0; - while (bytes[length] != 0) { + while (bytes[address + length] != 0) { length++; } @@ -502,36 +501,39 @@ extension WrappedMemory on Memory { int int32ValueOfPointer(Pointer pointer) { assert(pointer != 0, 'Null pointer dereference'); - return dartBuffer.asInt32List()[pointer >> 2]; + return asInt32[pointer >> 2]; } void setInt32Value(Pointer pointer, int value) { assert(pointer != 0, 'Null pointer dereference'); - dartBuffer.asInt32List()[pointer >> 2] = value; + asInt32[pointer >> 2] = value; } void setInt64Value(Pointer pointer, JsBigInt value) { assert(pointer != 0, 'Null pointer dereference'); - dartBuffer.asByteData().setBigInt64(pointer, value, true); + SafeDataView.entireBufferView(buffer).setBigInt64(pointer, value, true); } String readString(int address, [int? length]) { assert(address != 0, 'Null pointer dereference'); - return utf8 - .decode(dartBuffer.asUint8List(address, length ?? strlen(address))); + return utf8.decode(copyOrViewRange(address, length ?? strlen(address))); } String? readNullableString(int address, [int? length]) { if (address == 0) return null; - - return utf8 - .decode(dartBuffer.asUint8List(address, length ?? strlen(address))); + return readString(address, length); } Uint8List copyRange(Pointer pointer, int length) { - final list = Uint8List(length); - list.setAll(0, dartBuffer.asUint8List(pointer, length)); - return list; + return Uint8List.fromList(copyOrViewRange(pointer, length)); + } + + /// Views a memory region starting at [pointer] with length [length]. + /// + /// The returned [Uint8List] is a view over the native memory when compiled + /// with dart2js/DDC and a copy when compiled with dart2wasm. + Uint8List copyOrViewRange(Pointer pointer, int length) { + return asBytes.subarray(pointer, pointer + length).inner.toDart; } } @@ -596,7 +598,7 @@ class _InjectedValues { } memory.asBytes - ..setAll(zOut, encoded) + ..set(encoded.toJS, zOut) ..[zOut + encoded.length] = 0; }); }).toJS, @@ -604,7 +606,7 @@ class _InjectedValues { final vfs = callbacks.registeredVfs[vfsId]!; return _runVfs(() { - vfs.xRandomness(memory.buffer.toDart.asUint8List(zOut, nByte)); + vfs.xRandomness(memory.buffer.asUint8Array(zOut, nByte)); }); }).toJS, 'xSleep': ((int vfsId, int micros) { @@ -637,14 +639,14 @@ class _InjectedValues { 'xRead': ((int fd, Pointer target, int amount, JSBigInt offset) { final file = callbacks.openedFiles[fd]!; return _runVfs(() { - file.xRead(memory.buffer.toDart.asUint8List(target, amount), + file.xRead(memory.buffer.asUint8Array(target, amount), JsBigInt(offset).asDartInt); }); }).toJS, 'xWrite': ((int fd, Pointer source, int amount, JSBigInt offset) { final file = callbacks.openedFiles[fd]!; return _runVfs(() { - file.xWrite(memory.buffer.toDart.asUint8List(source, amount), + file.xWrite(memory.buffer.asUint8Array(source, amount), JsBigInt(offset).asDartInt); }); }).toJS, diff --git a/sqlite3/lib/wasm.dart b/sqlite3/lib/wasm.dart index 11a71c3d..17a4c00e 100644 --- a/sqlite3/lib/wasm.dart +++ b/sqlite3/lib/wasm.dart @@ -24,3 +24,4 @@ export 'src/wasm/vfs/indexed_db.dart' show IndexedDbFileSystem; export 'src/wasm/vfs/async_opfs/client.dart' show WasmVfs; export 'src/wasm/vfs/async_opfs/worker.dart' show WorkerOptions, VfsWorker; export 'src/wasm/sqlite3.dart'; +export 'src/wasm/vfs.dart'; diff --git a/sqlite3/test/wasm/file_system_test.dart b/sqlite3/test/wasm/file_system_test.dart index ace51435..1989b317 100644 --- a/sqlite3/test/wasm/file_system_test.dart +++ b/sqlite3/test/wasm/file_system_test.dart @@ -2,11 +2,12 @@ library; import 'dart:async'; -import 'dart:developer'; +import 'dart:js_interop'; import 'dart:math'; import 'dart:typed_data'; import 'package:sqlite3/wasm.dart'; +import 'package:sqlite3/src/wasm/js_interop/typed_data.dart'; import 'package:test/test.dart'; import 'utils.dart'; @@ -107,7 +108,6 @@ Future _testWith(FutureOr Function() open) async { final path = '$_fsRoot/foo$i.txt'; paths.add(path); - debugger(); fs.createFile(path); expect(fs.exists(path), isTrue); } @@ -142,12 +142,12 @@ Future _testWith(FutureOr Function() open) async { file.xTruncate(0); expect(file.xFileSize(), 0); - file.xWrite(Uint8List.fromList([1, 2, 3]), 0); + file.xWrite(SafeU8Array(Uint8List.fromList([1, 2, 3]).toJS), 0); expect(file.xFileSize(), 3); - final target = Uint8List(3); + final target = SafeU8Array.allocate(3); file.xRead(target, 0); - expect(target, [1, 2, 3]); + expect(target.inner.toDart, [1, 2, 3]); }); }