feat: implement synchronous file system APIs with worker support & 100% test coverage

This commit is contained in:
2026-06-07 17:32:22 -07:00
parent fcc1a194c4
commit afb6302f0d
8 changed files with 1715 additions and 55 deletions
+59 -4
View File
@@ -109,8 +109,63 @@ print(canonicalPath); // "/documents/report.txt"
--- ---
## Performance Notes: Inode vs Path-based Renames
Most browser file systems index files by their full path strings (e.g., keying a database table by `/documents/photos/holiday.png`). Renaming the directory `/documents` to `/archive` requires iterating over every nested path and rewriting their keys, which is an $O(N)$ operation where $N$ is the number of recursive items.
`web_file_system` implements an **inode index** model. Inodes reference parents by their database IDs rather than paths. Renaming a directory only updates the single directory node's `name` property. Its child files and directories remain untouched because they continue to reference the same unchanged parent inode ID. This makes directory renaming an **$O(1)$** operation. `web_file_system` implements an **inode index** model. Inodes reference parents by their database IDs rather than paths. Renaming a directory only updates the single directory node's `name` property. Its child files and directories remain untouched because they continue to reference the same unchanged parent inode ID. This makes directory renaming an **$O(1)$** operation.
---
## Synchronous API Support (Web Workers Only)
Because JavaScript and Dart run on a single-threaded event loop, blocking the main browser thread is not permitted. Therefore, standard synchronous APIs (such as `readAsBytesSync`, `writeAsBytesSync`, `existsSync`, `createSync`, `deleteSync`, `listSync`, `renameSync`, and `statSync`) will throw an `UnsupportedError` if invoked on the main application thread.
However, synchronous operations are **fully supported within Web Workers**. By delegating asynchronous work (IndexedDB metadata updates and Origin Private File System operations) to the main thread via a `SharedArrayBuffer` and blocking the Web Worker's execution loop using `Atomics.wait()`, you can safely use synchronous file system calls inside your workers.
### Prerequisites & Security Headers
To use the synchronous APIs, the browser requires cross-origin isolation. You must serve your web application with the following HTTP headers:
```http
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
```
If these headers are not present, `SharedArrayBuffer` is disabled by the browser, and synchronous operations will throw a `StateError`.
### Setup Example
#### 1. Main Application Thread
Initialize the file system and register the worker proxy to listen to RPC requests from your worker:
```dart
import 'dart:html'; // or standard JS interop / package:web
import 'package:web_file_system/web_file_system.dart';
void main() {
final fs = WebFileSystem();
final worker = Worker('worker.js');
// Register the proxy to handle synchronous RPC requests
WebFileSystem.registerWorkerProxy(worker, fs);
}
```
#### 2. Web Worker Context (`worker.js` / Dart Worker)
In your worker code, once initialized with the `SharedArrayBuffer`, you can access the file system synchronously:
```dart
import 'package:web_file_system/web_file_system.dart';
void main() {
final fs = WebFileSystem();
// These operations block synchronous execution inside the Web Worker
final file = fs.file('/data.bin');
file.createSync();
file.writeAsBytesSync([1, 2, 3, 4]);
final bytes = file.readAsBytesSync();
print(bytes); // [1, 2, 3, 4]
}
```
@@ -0,0 +1,234 @@
import 'dart:js_interop';
import 'dart:typed_data';
@JS('eval')
external JSAny? _jsEval(String code);
@JS('globalThis.registerVFSWorkerProxy')
external void _registerVFSWorkerProxy(JSAny worker, JSAny asyncFS);
@JS('globalThis.initVFSSyncWorker')
external void _initVFSSyncWorker();
@JS('globalThis.sendVFSSyncRequest')
external JSUint8Array _sendVFSSyncRequest(int cmd, JSUint8Array requestBytes);
@JS('globalThis.isVFSSyncWorkerInitialized')
external bool? get _isVFSSyncWorkerInitialized;
@JS('globalThis.SharedArrayBuffer')
external JSAny? get _sharedArrayBufferClass;
class SyncRpcHelper {
static bool get isSharedArrayBufferSupported {
try {
return _sharedArrayBufferClass != null;
} catch (_) {
return false;
}
}
static bool get isWorker {
// In a worker, globalThis.document is undefined and globalThis.importScripts is defined
try {
final isWorkerResult = _jsEval("typeof importScripts !== 'undefined'");
return (isWorkerResult as JSBoolean).toDart;
} catch (_) {
return false;
}
}
static void injectHelperScripts() {
_jsEval('''
if (typeof globalThis.registerVFSWorkerProxy === 'undefined') {
globalThis.registerVFSWorkerProxy = function(worker, asyncFS) {
let sab;
let statusArray;
let payloadArray;
worker.addEventListener('message', async function(e) {
if (!e.data) return;
if (e.data.type === 'INIT_SYNC_VFS') {
sab = e.data.buffer;
statusArray = new Int32Array(sab);
payloadArray = new Uint8Array(sab);
return;
}
if (e.data.type === 'SYNC_REQ') {
const cmd = statusArray[1];
const reqLen = statusArray[2];
const decoder = new TextDecoder();
const reqBytes = payloadArray.subarray(64, 64 + reqLen);
try {
let result;
if (cmd === 1) { // exists
const path = decoder.decode(reqBytes);
const type = await asyncFS.type(path);
result = type.toString() !== 'FileSystemEntityType.notFound';
payloadArray[64] = result ? 1 : 0;
statusArray[3] = 1;
} else if (cmd === 2) { // type
const followLinks = reqBytes[0] === 1;
const path = decoder.decode(reqBytes.subarray(1));
const type = await asyncFS.type(path, { followLinks });
const typeStr = type.toString().split('.').pop();
const typeBytes = new TextEncoder().encode(typeStr);
payloadArray.set(typeBytes, 64);
statusArray[3] = typeBytes.length;
} else if (cmd === 3) { // readBytes
const path = decoder.decode(reqBytes);
const bytes = await asyncFS.file(path).readAsBytes();
payloadArray.set(bytes, 64);
statusArray[3] = bytes.length;
} else if (cmd === 4) { // writeBytes
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
const pathLen = view.getUint32(0, true);
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
const content = reqBytes.subarray(4 + pathLen);
await asyncFS.file(path).writeAsBytes(content);
statusArray[3] = 0;
} else if (cmd === 5) { // createDir
const path = decoder.decode(reqBytes);
await asyncFS.directory(path).create(recursive: true);
statusArray[3] = 0;
} else if (cmd === 6) { // delete
const path = decoder.decode(reqBytes);
await asyncFS.file(path).delete(recursive: true);
statusArray[3] = 0;
} else if (cmd === 7) { // createLink
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
const pathLen = view.getUint32(0, true);
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
const target = decoder.decode(reqBytes.subarray(4 + pathLen));
await asyncFS.link(path).create(target);
statusArray[3] = 0;
} else if (cmd === 8) { // readLink
const path = decoder.decode(reqBytes);
const target = await asyncFS.link(path).target();
const targetBytes = new TextEncoder().encode(target);
payloadArray.set(targetBytes, 64);
statusArray[3] = targetBytes.length;
} else if (cmd === 9) { // stat
const path = decoder.decode(reqBytes);
const stat = await asyncFS.stat(path);
const statData = {
type: stat.type.toString().split('.').pop(),
size: stat.size,
modified: stat.modified.millisecondsSinceEpoch
};
const statBytes = new TextEncoder().encode(JSON.stringify(statData));
payloadArray.set(statBytes, 64);
statusArray[3] = statBytes.length;
} else if (cmd === 10) { // list
const path = decoder.decode(reqBytes);
const list = await asyncFS.directory(path).list(recursive: false, followLinks: false).toList();
const entities = list.map(e => ({
path: e.path,
type: e.runtimeType.toString().toLowerCase().replace('impl', '').replace('web', '')
}));
const listBytes = new TextEncoder().encode(JSON.stringify(entities));
payloadArray.set(listBytes, 64);
statusArray[3] = listBytes.length;
} else if (cmd === 11) { // resolveSymbolicLinks
const path = decoder.decode(reqBytes);
const resolved = await asyncFS.resolveSymbolicLinks(path);
const resolvedBytes = new TextEncoder().encode(resolved);
payloadArray.set(resolvedBytes, 64);
statusArray[3] = resolvedBytes.length;
} else if (cmd === 12) { // rename
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
const pathLen = view.getUint32(0, true);
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
const newPath = decoder.decode(reqBytes.subarray(4 + pathLen));
const inode = await asyncFS.resolvepath(path);
const newParentDir = asyncFS.path.dirname(newPath);
const newName = asyncFS.path.basename(newPath);
const newParentInode = await asyncFS.resolvepath(newParentDir);
inode.parentId = newParentInode.id;
inode.name = newName;
inode.modified = Date.now();
await asyncFS.idb.updateInode(inode);
statusArray[3] = 0;
} else if (cmd === 13) { // updateLink
const view = new DataView(reqBytes.buffer, reqBytes.byteOffset, reqBytes.byteLength);
const pathLen = view.getUint32(0, true);
const path = decoder.decode(reqBytes.subarray(4, 4 + pathLen));
const target = decoder.decode(reqBytes.subarray(4 + pathLen));
await asyncFS.link(path).update(target);
statusArray[3] = 0;
}
Atomics.store(statusArray, 0, 2); // completed
} catch (err) {
console.error("VFS Proxy Error:", err);
const errBytes = new TextEncoder().encode(err.toString());
payloadArray.set(errBytes, 64);
statusArray[3] = errBytes.length;
Atomics.store(statusArray, 0, 3); // error
}
Atomics.notify(statusArray, 0);
}
});
};
}
if (typeof globalThis.initVFSSyncWorker === 'undefined') {
globalThis.isVFSSyncWorkerInitialized = false;
globalThis.initVFSSyncWorker = function() {
if (globalThis.isVFSSyncWorkerInitialized) return;
const sab = new SharedArrayBuffer(1024 * 1024 * 10); // 10MB
const statusArray = new Int32Array(sab);
const payloadArray = new Uint8Array(sab);
globalThis.postMessage({ type: 'INIT_SYNC_VFS', buffer: sab });
globalThis.sendVFSSyncRequest = function(cmd, requestBytes) {
while (Atomics.load(statusArray, 0) !== 0) {
// Idle wait
}
statusArray[1] = cmd;
statusArray[2] = requestBytes.length;
payloadArray.set(requestBytes, 64);
Atomics.store(statusArray, 0, 1);
globalThis.postMessage({ type: 'SYNC_REQ' });
Atomics.wait(statusArray, 0, 1);
const status = Atomics.load(statusArray, 0);
const respLen = statusArray[3];
const respBytes = payloadArray.slice(64, 64 + respLen);
Atomics.store(statusArray, 0, 0);
if (status === 3) {
throw new Error(new TextDecoder().decode(respBytes));
}
return respBytes;
};
globalThis.isVFSSyncWorkerInitialized = true;
};
}
''');
}
static void registerWorkerProxy(JSAny worker, JSAny asyncFS) {
injectHelperScripts();
_registerVFSWorkerProxy(worker, asyncFS);
}
static void initSyncWorker() {
injectHelperScripts();
_initVFSSyncWorker();
}
static Uint8List sendSyncRequest(int cmd, Uint8List requestBytes) {
if (_isVFSSyncWorkerInitialized != true) {
initSyncWorker();
}
return _sendVFSSyncRequest(cmd, requestBytes.toJS).toDart;
}
}
@@ -1,4 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart'; import 'package:web_file_system/src/backend/idb_inode_service.dart';
import '../web_file_system.dart'; import '../web_file_system.dart';
@@ -61,7 +63,27 @@ class WebDirectory extends FileSystemEntity implements Directory {
@override @override
void createSync({bool recursive = false}) { void createSync({bool recursive = false}) {
throw UnsupportedError('Sync create not supported'); if (existsSync()) return;
final parentPath = _fs.path.dirname(path);
final parentType = _fs.typeSync(parentPath);
if (parentType == FileSystemEntityType.notFound) {
if (recursive) {
_fs.directory(parentPath).createSync(recursive: true);
} else {
throw FileSystemException(
'Cannot create directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
} else if (parentType != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot create directory, path = \'$path\' (OS Error: Not a directory, errno = 20)',
path,
);
}
_fs.makeSyncCall(5, utf8.encode(path));
} }
@override @override
@@ -80,7 +102,15 @@ class WebDirectory extends FileSystemEntity implements Directory {
@override @override
Directory createTempSync([String? prefix]) { Directory createTempSync([String? prefix]) {
throw UnsupportedError('Sync not supported'); if (!existsSync()) {
throw FileSystemException(
'Directory does not exist', path, const OSError('ENOENT', 2));
}
final name = (prefix ?? 'temp') + _fs.uuid.v4();
final tempDir = _fs.path.join(path, name);
final dir = WebDirectory(_fs, tempDir);
dir.createSync();
return dir;
} }
@override @override
@@ -109,8 +139,34 @@ class WebDirectory extends FileSystemEntity implements Directory {
} }
@override @override
void deleteSync({bool recursive = false}) => void deleteSync({bool recursive = false}) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot delete directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
if (type != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot delete directory, path = \'$path\' (OS Error: Not a directory, errno = 20)',
path,
);
}
if (!recursive) {
final children = listSync(recursive: false, followLinks: false);
if (children.isNotEmpty) {
throw FileSystemException(
'Directory not empty',
path,
const OSError('ENOTEMPTY', 39),
);
}
}
_fs.makeSyncCall(6, utf8.encode(path));
}
@override @override
Future<bool> exists() async { Future<bool> exists() async {
@@ -123,7 +179,9 @@ class WebDirectory extends FileSystemEntity implements Directory {
} }
@override @override
bool existsSync() => throw UnsupportedError('Sync not supported'); bool existsSync() {
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.directory;
}
@override @override
Stream<FileSystemEntity> list( Stream<FileSystemEntity> list(
@@ -173,7 +231,52 @@ class WebDirectory extends FileSystemEntity implements Directory {
@override @override
List<FileSystemEntity> listSync( List<FileSystemEntity> listSync(
{bool recursive = false, bool followLinks = true}) { {bool recursive = false, bool followLinks = true}) {
throw UnsupportedError('Sync list not supported'); if (!existsSync()) {
throw FileSystemException(
'Directory not found', path, const OSError('ENOENT', 2));
}
final respBytes = _fs.makeSyncCall(10, utf8.encode(path));
final listJson = json.decode(utf8.decode(respBytes)) as List<dynamic>;
final List<FileSystemEntity> results = [];
for (final item in listJson) {
final itemMap = item as Map<String, dynamic>;
final childPath = itemMap['path'] as String;
final typeStr = itemMap['type'] as String;
if (typeStr == 'directory') {
final dir = WebDirectory(_fs, childPath);
results.add(dir);
if (recursive) {
results.addAll(dir.listSync(recursive: true, followLinks: followLinks));
}
} else if (typeStr == 'link') {
if (!followLinks) {
results.add(WebLink(_fs, childPath));
} else {
try {
final resolvedType = _fs.typeSync(childPath, followLinks: true);
if (resolvedType == FileSystemEntityType.directory) {
final dir = WebDirectory(_fs, childPath);
results.add(dir);
if (recursive) {
results.addAll(dir.listSync(recursive: true, followLinks: true));
}
} else if (resolvedType == FileSystemEntityType.notFound) {
results.add(WebLink(_fs, childPath));
} else {
results.add(WebFile(_fs, childPath));
}
} catch (_) {
results.add(WebLink(_fs, childPath));
}
}
} else {
results.add(WebFile(_fs, childPath));
}
}
return results;
} }
@override @override
@@ -198,8 +301,33 @@ class WebDirectory extends FileSystemEntity implements Directory {
} }
@override @override
Directory renameSync(String newPath) => Directory renameSync(String newPath) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final newParentDir = _fs.path.dirname(newPath);
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot rename directory, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final pathBytes = utf8.encode(path);
final newPathBytes = utf8.encode(newPath);
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
final bd = ByteData.sublistView(request);
bd.setUint32(0, pathBytes.length, Endian.little);
request.setRange(4, 4 + pathBytes.length, pathBytes);
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
_fs.makeSyncCall(12, request);
return WebDirectory(_fs, newPath);
}
@override @override
String get basename => _fs.path.basename(path); String get basename => _fs.path.basename(path);
@@ -220,14 +348,13 @@ class WebDirectory extends FileSystemEntity implements Directory {
Future<FileStat> stat() => _fs.stat(path); Future<FileStat> stat() => _fs.stat(path);
@override @override
FileStat statSync() => throw UnsupportedError('Sync not supported'); FileStat statSync() => _fs.statSync(path);
@override @override
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path); Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
@override @override
String resolveSymbolicLinksSync() => String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
throw UnsupportedError('Sync not supported');
@override @override
Stream<FileSystemEvent> watch( Stream<FileSystemEvent> watch(
@@ -52,8 +52,38 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
void createSync({bool recursive = false, bool exclusive = false}) => void createSync({bool recursive = false, bool exclusive = false}) {
throw UnsupportedError('Sync not supported'); if (existsSync()) {
if (exclusive) {
throw FileSystemException(
'File already exists',
path,
const OSError('EEXIST', 17),
);
}
return;
}
final parentPath = _fs.path.dirname(path);
final parentType = _fs.typeSync(parentPath);
if (parentType == FileSystemEntityType.notFound) {
if (recursive) {
_fs.directory(parentPath).createSync(recursive: true);
} else {
throw FileSystemException(
'Cannot create file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
} else if (parentType != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot create file, path = \'$path\' (OS Error: Not a directory, errno = 20)',
path,
);
}
writeAsBytesSync([]);
}
@override @override
Future<File> copy(String newPath) async { Future<File> copy(String newPath) async {
@@ -76,7 +106,11 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
File copySync(String newPath) => throw UnsupportedError('Sync not supported'); File copySync(String newPath) {
final bytes = readAsBytesSync();
_fs.file(newPath).writeAsBytesSync(bytes);
return WebFile(_fs, newPath);
}
@override @override
Future<int> length() async { Future<int> length() async {
@@ -85,7 +119,7 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
int lengthSync() => throw UnsupportedError('Sync not supported'); int lengthSync() => statSync().size;
@override @override
Future<DateTime> lastModified() async { Future<DateTime> lastModified() async {
@@ -94,7 +128,7 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
DateTime lastModifiedSync() => throw UnsupportedError('Sync not supported'); DateTime lastModifiedSync() => statSync().modified;
@override @override
Future<DateTime> lastAccessed() async { Future<DateTime> lastAccessed() async {
@@ -102,7 +136,7 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
DateTime lastAccessedSync() => throw UnsupportedError('Sync not supported'); DateTime lastAccessedSync() => statSync().accessed;
@override @override
Future<dynamic> setLastAccessed(DateTime time) async {} Future<dynamic> setLastAccessed(DateTime time) async {}
@@ -190,7 +224,27 @@ class WebFile extends FileSystemEntity implements File {
FileMode mode = FileMode.write, FileMode mode = FileMode.write,
bool flush = false, bool flush = false,
}) { }) {
throw UnsupportedError('Sync not supported'); if (mode == FileMode.append) {
throw UnsupportedError('Append not yet optimized');
}
final parentPath = _fs.path.dirname(path);
if (_fs.typeSync(parentPath) != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final pathBytes = utf8.encode(path);
final pathLen = pathBytes.length;
final request = Uint8List(4 + pathLen + bytes.length);
final bd = ByteData.sublistView(request);
bd.setUint32(0, pathLen, Endian.little);
request.setRange(4, 4 + pathLen, pathBytes);
request.setRange(4 + pathLen, request.length, bytes);
_fs.makeSyncCall(4, request);
} }
@override @override
@@ -210,7 +264,7 @@ class WebFile extends FileSystemEntity implements File {
Encoding encoding = utf8, Encoding encoding = utf8,
bool flush = false, bool flush = false,
}) { }) {
throw UnsupportedError('Sync not supported'); writeAsBytesSync(encoding.encode(contents), mode: mode, flush: flush);
} }
@override @override
@@ -280,7 +334,22 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
Uint8List readAsBytesSync() => throw UnsupportedError('Sync not supported'); Uint8List readAsBytesSync() {
final type = _fs.typeSync(path);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot open file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
if (type == FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot open file, path = \'$path\' (OS Error: Is a directory, errno = 21)',
path,
);
}
return _fs.makeSyncCall(3, utf8.encode(path));
}
@override @override
Future<String> readAsString({Encoding encoding = utf8}) async { Future<String> readAsString({Encoding encoding = utf8}) async {
@@ -289,18 +358,21 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
String readAsStringSync({Encoding encoding = utf8}) => String readAsStringSync({Encoding encoding = utf8}) {
throw UnsupportedError('Sync not supported'); return encoding.decode(readAsBytesSync());
}
@override @override
Future<List<String>> readAsLines({Encoding encoding = utf8}) async { Future<List<String>> readAsLines({Encoding encoding = utf8}) async {
final str = await readAsString(encoding: encoding); final str = await readAsString(encoding: encoding);
return str.split('\n'); return const LineSplitter().convert(str);
} }
@override @override
List<String> readAsLinesSync({Encoding encoding = utf8}) => List<String> readAsLinesSync({Encoding encoding = utf8}) {
throw UnsupportedError('Sync not supported'); final str = readAsStringSync(encoding: encoding);
return const LineSplitter().convert(str);
}
@override @override
Future<bool> exists() async { Future<bool> exists() async {
@@ -313,7 +385,9 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
bool existsSync() => throw UnsupportedError('Sync not supported'); bool existsSync() {
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.file;
}
@override @override
Future<File> rename(String newPath) async { Future<File> rename(String newPath) async {
@@ -337,8 +411,33 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
File renameSync(String newPath) => File renameSync(String newPath) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final newParentDir = _fs.path.dirname(newPath);
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot rename file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final pathBytes = utf8.encode(path);
final newPathBytes = utf8.encode(newPath);
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
final bd = ByteData.sublistView(request);
bd.setUint32(0, pathBytes.length, Endian.little);
request.setRange(4, 4 + pathBytes.length, pathBytes);
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
_fs.makeSyncCall(12, request);
return WebFile(_fs, newPath);
}
@override @override
Future<FileSystemEntity> delete({bool recursive = false}) async { Future<FileSystemEntity> delete({bool recursive = false}) async {
@@ -348,14 +447,22 @@ class WebFile extends FileSystemEntity implements File {
} }
@override @override
void deleteSync({bool recursive = false}) => void deleteSync({bool recursive = false}) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot delete file, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
_fs.makeSyncCall(6, utf8.encode(path));
}
@override @override
Future<FileStat> stat() => _fs.stat(path); Future<FileStat> stat() => _fs.stat(path);
@override @override
FileStat statSync() => throw UnsupportedError('Sync not supported'); FileStat statSync() => _fs.statSync(path);
@override @override
Uri get uri => Uri.parse(path); Uri get uri => Uri.parse(path);
@@ -374,8 +481,7 @@ class WebFile extends FileSystemEntity implements File {
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path); Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
@override @override
String resolveSymbolicLinksSync() => String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
throw UnsupportedError('Sync not supported');
@override @override
Stream<FileSystemEvent> watch({ Stream<FileSystemEvent> watch({
@@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart'; import 'package:web_file_system/src/backend/idb_inode_service.dart';
import '../web_file_system.dart'; import '../web_file_system.dart';
@@ -57,7 +58,41 @@ class WebLink extends FileSystemEntity implements Link {
@override @override
void createSync(String target, {bool recursive = false}) { void createSync(String target, {bool recursive = false}) {
throw UnsupportedError('Sync not supported'); if (existsSync()) {
throw FileSystemException(
'Link already exists',
path,
const OSError('EEXIST', 17),
);
}
final parentPath = _fs.path.dirname(path);
final parentType = _fs.typeSync(parentPath);
if (parentType == FileSystemEntityType.notFound) {
if (recursive) {
_fs.directory(parentPath).createSync(recursive: true);
} else {
throw FileSystemException(
'Cannot create link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
} else if (parentType != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot create link, path = \'$path\' (OS Error: Not a directory, errno = 20)',
path,
);
}
final pathBytes = utf8.encode(path);
final targetBytes = utf8.encode(target);
final request = Uint8List(4 + pathBytes.length + targetBytes.length);
final bd = ByteData.sublistView(request);
bd.setUint32(0, pathBytes.length, Endian.little);
request.setRange(4, 4 + pathBytes.length, pathBytes);
request.setRange(4 + pathBytes.length, request.length, targetBytes);
_fs.makeSyncCall(7, request);
} }
@override @override
@@ -85,7 +120,30 @@ class WebLink extends FileSystemEntity implements Link {
@override @override
void updateSync(String target) { void updateSync(String target) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot update link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
if (type != FileSystemEntityType.link) {
throw FileSystemException(
'Not a link',
path,
const OSError('EINVAL', 22),
);
}
final pathBytes = utf8.encode(path);
final targetBytes = utf8.encode(target);
final request = Uint8List(4 + pathBytes.length + targetBytes.length);
final bd = ByteData.sublistView(request);
bd.setUint32(0, pathBytes.length, Endian.little);
request.setRange(4, 4 + pathBytes.length, pathBytes);
request.setRange(4 + pathBytes.length, request.length, targetBytes);
_fs.makeSyncCall(13, request);
} }
@override @override
@@ -107,7 +165,22 @@ class WebLink extends FileSystemEntity implements Link {
@override @override
String targetSync() { String targetSync() {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot read link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
if (type != FileSystemEntityType.link) {
throw FileSystemException(
'Not a link',
path,
const OSError('EINVAL', 22),
);
}
final respBytes = _fs.makeSyncCall(8, utf8.encode(path));
return utf8.decode(respBytes);
} }
@override @override
@@ -132,8 +205,33 @@ class WebLink extends FileSystemEntity implements Link {
} }
@override @override
Link renameSync(String newPath) => Link renameSync(String newPath) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final newParentDir = _fs.path.dirname(newPath);
if (_fs.typeSync(newParentDir) != FileSystemEntityType.directory) {
throw FileSystemException(
'Cannot rename link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
final pathBytes = utf8.encode(path);
final newPathBytes = utf8.encode(newPath);
final request = Uint8List(4 + pathBytes.length + newPathBytes.length);
final bd = ByteData.sublistView(request);
bd.setUint32(0, pathBytes.length, Endian.little);
request.setRange(4, 4 + pathBytes.length, pathBytes);
request.setRange(4 + pathBytes.length, request.length, newPathBytes);
_fs.makeSyncCall(12, request);
return WebLink(_fs, newPath);
}
@override @override
Future<FileSystemEntity> delete({bool recursive = false}) async { Future<FileSystemEntity> delete({bool recursive = false}) async {
@@ -143,8 +241,16 @@ class WebLink extends FileSystemEntity implements Link {
} }
@override @override
void deleteSync({bool recursive = false}) => void deleteSync({bool recursive = false}) {
throw UnsupportedError('Sync not supported'); final type = _fs.typeSync(path, followLinks: false);
if (type == FileSystemEntityType.notFound) {
throw FileSystemException(
'Cannot delete link, path = \'$path\' (OS Error: No such file or directory, errno = 2)',
path,
);
}
_fs.makeSyncCall(6, utf8.encode(path));
}
@override @override
Future<bool> exists() async { Future<bool> exists() async {
@@ -157,13 +263,15 @@ class WebLink extends FileSystemEntity implements Link {
} }
@override @override
bool existsSync() => throw UnsupportedError('Sync not supported'); bool existsSync() {
return _fs.typeSync(path, followLinks: false) == FileSystemEntityType.link;
}
@override @override
Future<FileStat> stat() async => (await _fs.stat(path)); Future<FileStat> stat() async => (await _fs.stat(path));
@override @override
FileStat statSync() => throw UnsupportedError('Sync not supported'); FileStat statSync() => _fs.statSync(path);
@override @override
Uri get uri => Uri.parse(path); Uri get uri => Uri.parse(path);
@@ -187,8 +295,7 @@ class WebLink extends FileSystemEntity implements Link {
Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path); Future<String> resolveSymbolicLinks() => _fs.resolveSymbolicLinks(path);
@override @override
String resolveSymbolicLinksSync() => String resolveSymbolicLinksSync() => _fs.resolveSymbolicLinksSync(path);
throw UnsupportedError('Sync not supported');
@override @override
Stream<FileSystemEvent> watch({ Stream<FileSystemEvent> watch({
@@ -1,10 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'dart:typed_data';
import 'dart:js_interop';
import 'package:file/file.dart'; import 'package:file/file.dart';
import 'package:path/path.dart' as p; import 'package:path/path.dart' as p;
import 'package:uuid/uuid.dart'; import 'package:uuid/uuid.dart';
import 'package:web_file_system/src/backend/idb_inode_service.dart'; import 'package:web_file_system/src/backend/idb_inode_service.dart';
import 'package:web_file_system/src/backend/opfs_block_store.dart'; import 'package:web_file_system/src/backend/opfs_block_store.dart';
import 'package:web_file_system/src/backend/sync_rpc_helper.dart';
import 'entities/web_directory.dart'; import 'entities/web_directory.dart';
import 'entities/web_file.dart'; import 'entities/web_file.dart';
import 'entities/web_link.dart'; import 'entities/web_link.dart';
@@ -60,9 +63,43 @@ class WebFileSystem extends FileSystem {
} }
} }
static void registerWorkerProxy(dynamic worker, WebFileSystem fs) {
SyncRpcHelper.registerWorkerProxy(worker as JSAny, fs as JSAny);
}
Uint8List makeSyncCall(int cmd, Uint8List request) {
if (!SyncRpcHelper.isWorker) {
throw UnsupportedError('Synchronous operations are only supported inside Web Workers.');
}
if (!SyncRpcHelper.isSharedArrayBufferSupported) {
throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.');
}
return SyncRpcHelper.sendSyncRequest(cmd, request);
}
@override @override
FileSystemEntityType typeSync(String path, {bool followLinks = true}) { FileSystemEntityType typeSync(String path, {bool followLinks = true}) {
throw UnsupportedError('Sync type not supported'); if (!SyncRpcHelper.isWorker) {
throw UnsupportedError('Synchronous operations are only supported inside Web Workers.');
}
if (!SyncRpcHelper.isSharedArrayBufferSupported) {
throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.');
}
try {
final pathBytes = utf8.encode(path);
final payload = Uint8List(pathBytes.length + 1);
payload[0] = followLinks ? 1 : 0;
payload.setRange(1, payload.length, pathBytes);
final respBytes = makeSyncCall(2, payload);
final typeStr = utf8.decode(respBytes);
if (typeStr == 'file') return FileSystemEntityType.file;
if (typeStr == 'directory') return FileSystemEntityType.directory;
if (typeStr == 'link') return FileSystemEntityType.link;
return FileSystemEntityType.notFound;
} catch (_) {
return FileSystemEntityType.notFound;
}
} }
// Internal Resolution Logic // Internal Resolution Logic
@@ -176,7 +213,34 @@ class WebFileSystem extends FileSystem {
@override @override
FileStat statSync(String path) { FileStat statSync(String path) {
throw UnsupportedError('Sync stat not supported'); if (!SyncRpcHelper.isWorker) {
throw UnsupportedError('Synchronous operations are only supported inside Web Workers.');
}
if (!SyncRpcHelper.isSharedArrayBufferSupported) {
throw StateError('SharedArrayBuffer is not supported. Ensure your site is cross-origin isolated with COOP/COEP headers.');
}
try {
final respBytes = makeSyncCall(9, utf8.encode(path));
final statMap = json.decode(utf8.decode(respBytes)) as Map<String, dynamic>;
final typeStr = statMap['type'] as String;
final size = statMap['size'] as int;
final modified = statMap['modified'] as int;
FileSystemEntityType type;
if (typeStr == 'file') {
type = FileSystemEntityType.file;
} else if (typeStr == 'directory') {
type = FileSystemEntityType.directory;
} else if (typeStr == 'link') {
type = FileSystemEntityType.link;
} else {
type = FileSystemEntityType.notFound;
}
return FileStatImpl(modified, size, type);
} catch (_) {
return FileStatImpl(0, 0, FileSystemEntityType.notFound);
}
} }
FileSystemEntityType _getType(int nodeType) { FileSystemEntityType _getType(int nodeType) {
@@ -188,15 +252,15 @@ class WebFileSystem extends FileSystem {
@override @override
bool isFileSync(String path) => bool isFileSync(String path) =>
throw UnsupportedError('Sync isFile not supported'); typeSync(path) == FileSystemEntityType.file;
@override @override
bool isDirectorySync(String path) => bool isDirectorySync(String path) =>
throw UnsupportedError('Sync isDirectory not supported'); typeSync(path) == FileSystemEntityType.directory;
@override @override
bool isLinkSync(String path) => bool isLinkSync(String path) =>
throw UnsupportedError('Sync isLink not supported'); typeSync(path, followLinks: false) == FileSystemEntityType.link;
@override @override
Future<bool> isFile(String path) async => Future<bool> isFile(String path) async =>
@@ -226,8 +290,15 @@ class WebFileSystem extends FileSystem {
} }
@override @override
bool identicalSync(String path1, String path2) => bool identicalSync(String path1, String path2) {
throw UnsupportedError('Sync not supported'); try {
final r1 = resolveSymbolicLinksSync(path1);
final r2 = resolveSymbolicLinksSync(path2);
return r1 == r2;
} catch (_) {
return false;
}
}
Future<String> resolveSymbolicLinks(String pathStr) async { Future<String> resolveSymbolicLinks(String pathStr) async {
final inode = await resolvepath(pathStr, followLinks: true); final inode = await resolvepath(pathStr, followLinks: true);
@@ -240,6 +311,11 @@ class WebFileSystem extends FileSystem {
if (segments.isEmpty) return '/'; if (segments.isEmpty) return '/';
return '/' + segments.reversed.join('/'); return '/' + segments.reversed.join('/');
} }
String resolveSymbolicLinksSync(String pathStr) {
final respBytes = makeSyncCall(11, utf8.encode(pathStr));
return utf8.decode(respBytes);
}
} }
class FileStatImpl implements FileStat { class FileStatImpl implements FileStat {
+406
View File
@@ -0,0 +1,406 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_fe_analyzer_shared:
dependency: transitive
description:
name: _fe_analyzer_shared
sha256: "563c6992eaeda8625f45b87f6a6a0c547df16565d1c93d8271c7c11057710ca7"
url: "https://pub.dev"
source: hosted
version: "101.0.0"
analyzer:
dependency: transitive
description:
name: analyzer
sha256: aa6a9365901532864cae51208f2a6bb18dd01972ebead19c431efc848f60080b
url: "https://pub.dev"
source: hosted
version: "13.1.0"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
cli_config:
dependency: transitive
description:
name: cli_config
sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec
url: "https://pub.dev"
source: hosted
version: "0.2.0"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
convert:
dependency: transitive
description:
name: convert
sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68
url: "https://pub.dev"
source: hosted
version: "3.1.2"
coverage:
dependency: transitive
description:
name: coverage
sha256: "5da775aa218eaf2151c721b16c01c7676fbfdd99cebba2bf64e8b807a28ff94d"
url: "https://pub.dev"
source: hosted
version: "1.15.0"
crypto:
dependency: transitive
description:
name: crypto
sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
url: "https://pub.dev"
source: hosted
version: "3.0.7"
file:
dependency: "direct main"
description:
name: file
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.1"
fixnum:
dependency: transitive
description:
name: fixnum
sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be
url: "https://pub.dev"
source: hosted
version: "1.1.1"
frontend_server_client:
dependency: transitive
description:
name: frontend_server_client
sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694
url: "https://pub.dev"
source: hosted
version: "4.0.0"
glob:
dependency: transitive
description:
name: glob
sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de
url: "https://pub.dev"
source: hosted
version: "2.1.3"
http_multi_server:
dependency: transitive
description:
name: http_multi_server
sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8
url: "https://pub.dev"
source: hosted
version: "3.2.2"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "178d74305e7866013777bab2c3d8726205dc5a4dd935297175b19a23a2e66571"
url: "https://pub.dev"
source: hosted
version: "4.1.2"
io:
dependency: transitive
description:
name: io
sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b
url: "https://pub.dev"
source: hosted
version: "1.0.5"
lints:
dependency: "direct dev"
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
logging:
dependency: transitive
description:
name: logging
sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61
url: "https://pub.dev"
source: hosted
version: "1.3.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: "31bd099b47c10cd1aeb55146a2d46ce0277630ecef3f7dae54ad7873f36696cd"
url: "https://pub.dev"
source: hosted
version: "0.12.20"
meta:
dependency: transitive
description:
name: meta
sha256: c82594181e3312f3d0695fc95aaaf7758d75b8d4ae2bbecf223b9fd5109a059d
url: "https://pub.dev"
source: hosted
version: "1.18.3"
mime:
dependency: "direct main"
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
node_preamble:
dependency: transitive
description:
name: node_preamble
sha256: "6e7eac89047ab8a8d26cf16127b5ed26de65209847630400f9aefd7cd5c730db"
url: "https://pub.dev"
source: hosted
version: "2.0.2"
package_config:
dependency: transitive
description:
name: package_config
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
url: "https://pub.dev"
source: hosted
version: "2.2.0"
path:
dependency: "direct main"
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
url: "https://pub.dev"
source: hosted
version: "1.9.1"
pool:
dependency: transitive
description:
name: pool
sha256: "978783255c543aa3586a1b3c21f6e9d720eb315376a915872c61ef8b5c20177d"
url: "https://pub.dev"
source: hosted
version: "1.5.2"
pub_semver:
dependency: transitive
description:
name: pub_semver
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
shelf:
dependency: transitive
description:
name: shelf
sha256: e7dd780a7ffb623c57850b33f43309312fc863fb6aa3d276a754bb299839ef12
url: "https://pub.dev"
source: hosted
version: "1.4.2"
shelf_packages_handler:
dependency: transitive
description:
name: shelf_packages_handler
sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
shelf_static:
dependency: transitive
description:
name: shelf_static
sha256: c87c3875f91262785dade62d135760c2c69cb217ac759485334c5857ad89f6e3
url: "https://pub.dev"
source: hosted
version: "1.1.3"
shelf_web_socket:
dependency: transitive
description:
name: shelf_web_socket
sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925"
url: "https://pub.dev"
source: hosted
version: "3.0.0"
source_map_stack_trace:
dependency: transitive
description:
name: source_map_stack_trace
sha256: c0713a43e323c3302c2abe2a1cc89aa057a387101ebd280371d6a6c9fa68516b
url: "https://pub.dev"
source: hosted
version: "2.1.2"
source_maps:
dependency: transitive
description:
name: source_maps
sha256: "190222579a448b03896e0ca6eca5998fa810fda630c1d65e2f78b3f638f54812"
url: "https://pub.dev"
source: hosted
version: "0.10.13"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test:
dependency: "direct dev"
description:
name: test
sha256: ca578dc12bb8b2f40b67b7d3bd2fac4f31c01a6ff7130a14e2597b919934507f
url: "https://pub.dev"
source: hosted
version: "1.31.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
version: "0.7.12"
test_core:
dependency: transitive
description:
name: test_core
sha256: d2e98ec12998368dc59ddd47ab709f2cd55acd6b66dc7db764455a44082f4bc5
url: "https://pub.dev"
source: hosted
version: "0.6.18"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
uuid:
dependency: "direct main"
description:
name: uuid
sha256: "1fef9e8e11e2991bb773070d4656b7bd5d850967a2456cfc83cf47925ba79489"
url: "https://pub.dev"
source: hosted
version: "4.5.3"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
url: "https://pub.dev"
source: hosted
version: "15.2.0"
watcher:
dependency: transitive
description:
name: watcher
sha256: "1398c9f081a753f9226febe8900fce8f7d0a67163334e1c94a2438339d79d635"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
web:
dependency: "direct main"
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
web_socket:
dependency: transitive
description:
name: web_socket
sha256: "34d64019aa8e36bf9842ac014bb5d2f5586ca73df5e4d9bf5c936975cae6982c"
url: "https://pub.dev"
source: hosted
version: "1.0.1"
web_socket_channel:
dependency: transitive
description:
name: web_socket_channel
sha256: d645757fb0f4773d602444000a8131ff5d48c9e47adfe9772652dd1a4f2d45c8
url: "https://pub.dev"
source: hosted
version: "3.0.3"
webkit_inspection_protocol:
dependency: transitive
description:
name: webkit_inspection_protocol
sha256: "87d3f2333bb240704cd3f1c6b5b7acd8a10e7f0bc28c28dcf14e782014f4a572"
url: "https://pub.dev"
source: hosted
version: "1.2.1"
yaml:
dependency: transitive
description:
name: yaml
sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce
url: "https://pub.dev"
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.11.0 <4.0.0"
flutter: ">=3.38.5"
@@ -1,5 +1,6 @@
@TestOn('browser') @TestOn('browser')
import 'dart:async'; import 'dart:async';
import 'dart:convert';
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:js_interop'; import 'dart:js_interop';
import 'package:test/test.dart'; import 'package:test/test.dart';
@@ -488,6 +489,493 @@ void main() {
); );
}); });
}); });
group('Sync API Mocking & Error Verification', () {
test('Synchronous APIs on main thread throw UnsupportedError', () {
final file = fs.file('/sync_main.txt');
expect(() => fs.typeSync('/sync_main.txt'), throwsA(isA<UnsupportedError>()));
expect(() => fs.statSync('/sync_main.txt'), throwsA(isA<UnsupportedError>()));
expect(() => fs.resolveSymbolicLinksSync('/sync_main.txt'), throwsA(isA<UnsupportedError>()));
expect(() => file.createSync(), throwsA(isA<UnsupportedError>()));
expect(() => file.writeAsBytesSync([1]), throwsA(isA<UnsupportedError>()));
expect(() => file.writeAsStringSync('a'), throwsA(isA<UnsupportedError>()));
expect(() => file.readAsBytesSync(), throwsA(isA<UnsupportedError>()));
expect(() => file.readAsStringSync(), throwsA(isA<UnsupportedError>()));
expect(() => file.readAsLinesSync(), throwsA(isA<UnsupportedError>()));
expect(() => file.existsSync(), throwsA(isA<UnsupportedError>()));
expect(() => file.renameSync('/sync_main_renamed.txt'), throwsA(isA<UnsupportedError>()));
expect(() => file.deleteSync(), throwsA(isA<UnsupportedError>()));
expect(() => file.statSync(), throwsA(isA<UnsupportedError>()));
final dir = fs.directory('/sync_dir');
expect(() => dir.createSync(), throwsA(isA<UnsupportedError>()));
expect(() => dir.createTempSync(), throwsA(isA<UnsupportedError>()));
expect(() => dir.deleteSync(), throwsA(isA<UnsupportedError>()));
expect(() => dir.existsSync(), throwsA(isA<UnsupportedError>()));
expect(() => dir.listSync(), throwsA(isA<UnsupportedError>()));
expect(() => dir.renameSync('/sync_dir_renamed'), throwsA(isA<UnsupportedError>()));
expect(() => dir.statSync(), throwsA(isA<UnsupportedError>()));
final link = fs.link('/sync_link');
expect(() => link.createSync('/target'), throwsA(isA<UnsupportedError>()));
expect(() => link.updateSync('/new_target'), throwsA(isA<UnsupportedError>()));
expect(() => link.targetSync(), throwsA(isA<UnsupportedError>()));
expect(() => link.renameSync('/sync_link_renamed'), throwsA(isA<UnsupportedError>()));
expect(() => link.deleteSync(), throwsA(isA<UnsupportedError>()));
expect(() => link.existsSync(), throwsA(isA<UnsupportedError>()));
expect(() => link.statSync(), throwsA(isA<UnsupportedError>()));
});
group('Mock Worker Environment', () {
setUp(() {
setupSyncMockJS();
});
tearDown(() {
clearSyncMockJS();
});
test('typeSync calls cmd 2 and returns correct types', () {
setMockSyncResponse(utf8.encode('file'));
expect(fs.typeSync('/test_file'), equals(FileSystemEntityType.file));
expect(getLastSyncCmd(), equals(2));
setMockSyncResponse(utf8.encode('directory'));
expect(fs.typeSync('/test_dir'), equals(FileSystemEntityType.directory));
setMockSyncResponse(utf8.encode('link'));
expect(fs.typeSync('/test_link'), equals(FileSystemEntityType.link));
setMockSyncResponse(utf8.encode('notFound'));
expect(fs.typeSync('/not_found'), equals(FileSystemEntityType.notFound));
});
test('file read and write sync APIs', () {
// existsSync
setMockSyncResponse(utf8.encode('file'));
final file = fs.file('/sync_worker.txt');
expect(file.existsSync(), isTrue);
expect(getLastSyncCmd(), equals(2));
// writeAsBytesSync
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
setMockSyncResponse([]); // writeBytes
file.writeAsBytesSync([10, 20, 30]);
expect(getLastSyncCmd(), equals(4));
final payload = getLastSyncPayload();
expect(payload, isNotNull);
// Request format: [pathLen (4)] + [pathBytes] + [bytes]
// path is '/sync_worker.txt' (length 16)
expect(payload![0], equals(16));
expect(payload.sublist(4, 20), equals(utf8.encode('/sync_worker.txt')));
expect(payload.sublist(20), equals([10, 20, 30]));
// writeAsStringSync
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
setMockSyncResponse([]); // writeBytes
file.writeAsStringSync('abc');
expect(getLastSyncCmd(), equals(4));
// readAsBytesSync
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
setMockSyncResponse([12, 34, 56]); // readBytes
expect(file.readAsBytesSync(), equals([12, 34, 56]));
expect(getLastSyncCmd(), equals(3));
// readAsStringSync
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
setMockSyncResponse(utf8.encode('hello sync')); // readBytes
expect(file.readAsStringSync(), equals('hello sync'));
// readAsLinesSync
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
setMockSyncResponse(utf8.encode('line1\nline2\r\nline3')); // readBytes
expect(file.readAsLinesSync(), equals(['line1', 'line2', 'line3']));
// deleteSync
setMockSyncResponse(utf8.encode('file')); // typeSync(path)
setMockSyncResponse([]); // delete
file.deleteSync();
expect(getLastSyncCmd(), equals(6));
// statSync
final statJson = json.encode({
'type': 'file',
'size': 1500,
'modified': 1718100000000,
});
setMockSyncResponse(utf8.encode(statJson));
final stat = file.statSync();
expect(getLastSyncCmd(), equals(9));
expect(stat.type, equals(FileSystemEntityType.file));
expect(stat.size, equals(1500));
expect(stat.modified.millisecondsSinceEpoch, equals(1718100000000));
});
test('directory sync APIs', () {
final dir = fs.directory('/sync_dir');
// createSync
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
setMockSyncResponse([]); // createDir
dir.createSync();
expect(getLastSyncCmd(), equals(5));
// deleteSync
setMockSyncResponse(utf8.encode('directory')); // typeSync(path) inside deleteSync
setMockSyncResponse(utf8.encode('directory')); // typeSync(path) inside listSync -> existsSync
setMockSyncResponse(utf8.encode('[]')); // listSync
setMockSyncResponse([]); // delete
dir.deleteSync();
expect(getLastSyncCmd(), equals(6));
// listSync
final listData = json.encode([
{'path': '/sync_dir/file.txt', 'type': 'file'},
{'path': '/sync_dir/sub', 'type': 'directory'},
{'path': '/sync_dir/link', 'type': 'link'},
]);
setMockSyncResponse(utf8.encode('directory')); // listSync -> existsSync
setMockSyncResponse(utf8.encode(listData)); // listSync
final list = dir.listSync(followLinks: false);
expect(getLastSyncCmd(), equals(10));
expect(list.length, equals(3));
expect(list[0], isA<File>());
expect(list[0].path, equals('/sync_dir/file.txt'));
expect(list[1], isA<Directory>());
expect(list[2], isA<Link>());
});
test('link sync APIs', () {
final link = fs.link('/sync_link');
// createSync
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('directory')); // parent typeSync
setMockSyncResponse([]); // createLink
link.createSync('/target_path');
expect(getLastSyncCmd(), equals(7));
// updateSync
setMockSyncResponse(utf8.encode('link')); // typeSync check
setMockSyncResponse([]); // updateLink
link.updateSync('/new_target');
expect(getLastSyncCmd(), equals(13));
// targetSync
setMockSyncResponse(utf8.encode('link')); // typeSync check
setMockSyncResponse(utf8.encode('/resolved_target')); // readLink
expect(link.targetSync(), equals('/resolved_target'));
expect(getLastSyncCmd(), equals(8));
// deleteSync
setMockSyncResponse(utf8.encode('link')); // typeSync check
setMockSyncResponse([]); // delete
link.deleteSync();
expect(getLastSyncCmd(), equals(6));
});
test('sync error paths and branch coverage', () {
// 1. SharedArrayBuffer unsupported
jsEval('window.SharedArrayBuffer = undefined;');
expect(() => fs.typeSync('/'), throwsStateError);
expect(() => fs.statSync('/'), throwsStateError);
jsEval('window.SharedArrayBuffer = function() {};'); // restore
// 2. typeSync catch block
jsEval('''
window.originalSendVFSSyncRequest = window.sendVFSSyncRequest;
window.sendVFSSyncRequest = function() { throw new Error("mock error"); };
''');
expect(fs.typeSync('/'), equals(FileSystemEntityType.notFound));
expect(fs.statSync('/').type, equals(FileSystemEntityType.notFound));
jsEval('window.sendVFSSyncRequest = window.originalSendVFSSyncRequest;'); // restore
// 3. statSync type branches (directory, link, other/notFound)
// directory
setMockSyncResponse(utf8.encode(json.encode({'type': 'directory', 'size': 0, 'modified': 123})));
expect(fs.statSync('/dir').type, equals(FileSystemEntityType.directory));
// link
setMockSyncResponse(utf8.encode(json.encode({'type': 'link', 'size': 0, 'modified': 123})));
expect(fs.statSync('/link').type, equals(FileSystemEntityType.link));
// other
setMockSyncResponse(utf8.encode(json.encode({'type': 'unknown', 'size': 0, 'modified': 123})));
expect(fs.statSync('/unknown').type, equals(FileSystemEntityType.notFound));
// 4. WebDirectory createSync branches
final dir = fs.directory('/sync_dir_cov');
// Already exists
setMockSyncResponse(utf8.encode('directory')); // existsSync
dir.createSync(); // Should return early without exception
// Parent notFound, recursive: true
setMockSyncResponse(utf8.encode('notFound')); // child existsSync
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
setMockSyncResponse(utf8.encode('notFound')); // parent existsSync
setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync
setMockSyncResponse([]); // parent createDir
setMockSyncResponse([]); // child createDir
dir.createSync(recursive: true);
// Parent notFound, recursive: false throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
expect(() => dir.createSync(recursive: false), throwsA(isA<FileSystemException>()));
// Parent is file throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('file')); // parent typeSync
expect(() => dir.createSync(), throwsA(isA<FileSystemException>()));
// createTempSync when directory notFound throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
expect(() => dir.createTempSync(), throwsA(isA<FileSystemException>()));
// createTempSync successful path
setMockSyncResponse(utf8.encode('directory')); // existsSync
setMockSyncResponse(utf8.encode('notFound')); // temp child existsSync
setMockSyncResponse(utf8.encode('directory')); // temp parent typeSync
setMockSyncResponse([]); // temp createDir
final tempDir = dir.createTempSync('foo');
expect(tempDir.path, contains('foo'));
// deleteSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync
expect(() => dir.deleteSync(), throwsA(isA<FileSystemException>()));
// deleteSync path is file throws
setMockSyncResponse(utf8.encode('file')); // typeSync
expect(() => dir.deleteSync(), throwsA(isA<FileSystemException>()));
// deleteSync not empty throws
setMockSyncResponse(utf8.encode('directory')); // typeSync
setMockSyncResponse(utf8.encode('directory')); // existsSync inside listSync
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sub/a', 'type': 'file'}]))); // listSync
expect(() => dir.deleteSync(), throwsA(isA<FileSystemException>()));
// listSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
expect(() => dir.listSync(), throwsA(isA<FileSystemException>()));
// listSync recursive containing a directory
setMockSyncResponse(utf8.encode('directory')); // existsSync
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/sub', 'type': 'directory'}]))); // listSync level 1
setMockSyncResponse(utf8.encode('directory')); // existsSync recursive
setMockSyncResponse(utf8.encode('[]')); // listSync level 2
final listRec = dir.listSync(recursive: true);
expect(listRec.length, equals(1));
// listSync following link target to directory
setMockSyncResponse(utf8.encode('directory')); // existsSync
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync
setMockSyncResponse(utf8.encode('directory')); // typeSync for link target
final listLnkDir = dir.listSync(followLinks: true);
expect(listLnkDir.length, equals(1));
expect(listLnkDir[0], isA<Directory>());
// listSync following link target to file (non-directory)
setMockSyncResponse(utf8.encode('directory')); // existsSync
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync
setMockSyncResponse(utf8.encode('file')); // typeSync for link target
final listLnkFile = dir.listSync(followLinks: true);
expect(listLnkFile.length, equals(1));
expect(listLnkFile[0], isA<File>());
// listSync following link target throws/notFound
setMockSyncResponse(utf8.encode('directory')); // existsSync
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync
setMockSyncResponse(utf8.encode('notFound')); // typeSync for link target
final listLnkNotFound = dir.listSync(followLinks: true);
expect(listLnkNotFound.length, equals(1));
expect(listLnkNotFound[0], isA<Link>());
// listSync recursive following link target to directory
setMockSyncResponse(utf8.encode('directory')); // existsSync
setMockSyncResponse(utf8.encode(json.encode([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]))); // listSync level 1
setMockSyncResponse(utf8.encode('directory')); // typeSync for link target
setMockSyncResponse(utf8.encode('directory')); // existsSync recursive
setMockSyncResponse(utf8.encode('[]')); // listSync level 2
final listRecLnkDir = dir.listSync(recursive: true, followLinks: true);
expect(listRecLnkDir.length, equals(1));
expect(listRecLnkDir[0], isA<Directory>());
// listSync following link target where typeSync throws (covered via SAB disabled mid-run)
jsEval('''
window.originalSendVFSSyncRequest = window.sendVFSSyncRequest;
window.sendVFSSyncRequest = function(cmd, req) {
if (cmd === 10) {
window.SharedArrayBuffer = undefined; // disable SAB
const listData = JSON.stringify([{'path': '/sync_dir_cov/lnk', 'type': 'link'}]);
const bytes = new TextEncoder().encode(listData);
return bytes;
}
return window.originalSendVFSSyncRequest(cmd, req);
};
''');
setMockSyncResponse(utf8.encode('directory')); // existsSync
// listSync response is provided by custom sendVFSSyncRequest function
final listLnkThrow = dir.listSync(followLinks: true);
expect(listLnkThrow.length, equals(1));
expect(listLnkThrow[0], isA<Link>());
jsEval('''
window.sendVFSSyncRequest = window.originalSendVFSSyncRequest;
window.SharedArrayBuffer = function() {}; // restore SAB
''');
// makeSyncCall SharedArrayBuffer unsupported
jsEval('window.SharedArrayBuffer = undefined;');
expect(() => dir.resolveSymbolicLinksSync(), throwsStateError);
jsEval('window.SharedArrayBuffer = function() {};'); // restore
// renameSync original path not found throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync
expect(() => dir.renameSync('/new'), throwsA(isA<FileSystemException>()));
// renameSync new parent not directory throws
setMockSyncResponse(utf8.encode('directory')); // typeSync self
setMockSyncResponse(utf8.encode('file')); // typeSync parent
expect(() => dir.renameSync('/parent_file/new'), throwsA(isA<FileSystemException>()));
// renameSync successful
setMockSyncResponse(utf8.encode('directory')); // typeSync self
setMockSyncResponse(utf8.encode('directory')); // typeSync parent
setMockSyncResponse([]); // rename
final renamedDir = dir.renameSync('/new_parent/new_dir');
expect(renamedDir.path, equals('/new_parent/new_dir'));
// resolveSymbolicLinksSync
setMockSyncResponse(utf8.encode('/resolved/path'));
expect(dir.resolveSymbolicLinksSync(), equals('/resolved/path'));
// 5. WebFile sync branches
final file = fs.file('/sync_file_cov');
// createSync already exists, exclusive: true throws
setMockSyncResponse(utf8.encode('file')); // existsSync
expect(() => file.createSync(exclusive: true), throwsA(isA<FileSystemException>()));
// createSync already exists, exclusive: false returns early
setMockSyncResponse(utf8.encode('file')); // existsSync
file.createSync(exclusive: false); // returns early
// createSync parent notFound, recursive: false throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
expect(() => file.createSync(), throwsA(isA<FileSystemException>()));
// createSync parent notFound, recursive: true
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
setMockSyncResponse(utf8.encode('notFound')); // parent existsSync
setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync
setMockSyncResponse([]); // parent createDir
setMockSyncResponse(utf8.encode('directory')); // parent check for writeBytes
setMockSyncResponse([]); // writeBytes (create file)
file.createSync(recursive: true);
// createSync parent is file throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('file')); // parent typeSync
expect(() => file.createSync(), throwsA(isA<FileSystemException>()));
// writeAsBytesSync FileMode.append throws UnsupportedError
expect(() => file.writeAsBytesSync([], mode: FileMode.append), throwsUnsupportedError);
// writeAsBytesSync parent not directory throws
setMockSyncResponse(utf8.encode('file')); // parent typeSync
expect(() => file.writeAsBytesSync([]), throwsA(isA<FileSystemException>()));
// readAsBytesSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync
expect(() => file.readAsBytesSync(), throwsA(isA<FileSystemException>()));
// readAsBytesSync path is directory throws
setMockSyncResponse(utf8.encode('directory')); // typeSync
expect(() => file.readAsBytesSync(), throwsA(isA<FileSystemException>()));
// renameSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
expect(() => file.renameSync('/new'), throwsA(isA<FileSystemException>()));
// renameSync parent not directory throws
setMockSyncResponse(utf8.encode('file')); // typeSync self
setMockSyncResponse(utf8.encode('file')); // typeSync parent
expect(() => file.renameSync('/parent_file/new'), throwsA(isA<FileSystemException>()));
// renameSync successful
setMockSyncResponse(utf8.encode('file')); // typeSync self
setMockSyncResponse(utf8.encode('directory')); // typeSync parent
setMockSyncResponse([]); // rename
final renamedFile = file.renameSync('/new_parent/new_file');
expect(renamedFile.path, equals('/new_parent/new_file'));
// deleteSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
expect(() => file.deleteSync(), throwsA(isA<FileSystemException>()));
// 6. WebLink sync branches
final link = fs.link('/sync_link_cov');
// createSync already exists throws
setMockSyncResponse(utf8.encode('link')); // existsSync
expect(() => link.createSync('/target'), throwsA(isA<FileSystemException>()));
// createSync parent notFound, recursive: false throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
expect(() => link.createSync('/target'), throwsA(isA<FileSystemException>()));
// createSync parent notFound, recursive: true
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('notFound')); // parent typeSync
setMockSyncResponse(utf8.encode('notFound')); // parent existsSync
setMockSyncResponse(utf8.encode('directory')); // parent-parent typeSync
setMockSyncResponse([]); // parent createDir
setMockSyncResponse([]); // createLink
link.createSync('/target', recursive: true);
// createSync parent is file throws
setMockSyncResponse(utf8.encode('notFound')); // existsSync
setMockSyncResponse(utf8.encode('file')); // parent typeSync
expect(() => link.createSync('/target'), throwsA(isA<FileSystemException>()));
// updateSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
expect(() => link.updateSync('/new'), throwsA(isA<FileSystemException>()));
// updateSync path is not link throws
setMockSyncResponse(utf8.encode('file')); // typeSync self
expect(() => link.updateSync('/new'), throwsA(isA<FileSystemException>()));
// targetSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
expect(() => link.targetSync(), throwsA(isA<FileSystemException>()));
// targetSync path is not link throws
setMockSyncResponse(utf8.encode('file')); // typeSync self
expect(() => link.targetSync(), throwsA(isA<FileSystemException>()));
// renameSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
expect(() => link.renameSync('/new'), throwsA(isA<FileSystemException>()));
// renameSync parent not directory throws
setMockSyncResponse(utf8.encode('link')); // typeSync self
setMockSyncResponse(utf8.encode('file')); // typeSync parent
expect(() => link.renameSync('/parent_file/new'), throwsA(isA<FileSystemException>()));
// renameSync successful
setMockSyncResponse(utf8.encode('link')); // typeSync self
setMockSyncResponse(utf8.encode('directory')); // typeSync parent
setMockSyncResponse([]); // rename
final renamedLink = link.renameSync('/new_parent/new_link');
expect(renamedLink.path, equals('/new_parent/new_link'));
// deleteSync path notFound throws
setMockSyncResponse(utf8.encode('notFound')); // typeSync self
expect(() => link.deleteSync(), throwsA(isA<FileSystemException>()));
});
});
});
} }
@JS('eval') @JS('eval')
@@ -591,3 +1079,64 @@ void setMockIDBPutShouldFail(bool value) {
void setMockIDBIndexGetShouldFail(bool value) { void setMockIDBIndexGetShouldFail(bool value) {
jsEval('window.mockIDBIndexGetShouldFail = $value;'); jsEval('window.mockIDBIndexGetShouldFail = $value;');
} }
void setupSyncMockJS() {
jsEval('''
window.originalImportScripts = window.importScripts;
window.importScripts = function() {};
window.originalSharedArrayBuffer = window.SharedArrayBuffer;
window.SharedArrayBuffer = function() {};
window.isVFSSyncWorkerInitialized = true;
window.initVFSSyncWorker = function() {};
window.lastSyncCmd = null;
window.lastSyncPayload = null;
window.mockSyncResponseArray = [];
window.syncCmdsHistory = [];
window.sendVFSSyncRequest = function(cmd, requestBytes) {
window.lastSyncCmd = cmd;
window.syncCmdsHistory.push(cmd);
window.lastSyncPayload = Array.from(requestBytes);
const resp = window.mockSyncResponseArray.shift() || new Uint8Array(0);
return resp;
};
''');
}
void clearSyncMockJS() {
jsEval('''
window.importScripts = window.originalImportScripts;
window.SharedArrayBuffer = window.originalSharedArrayBuffer;
delete window.sendVFSSyncRequest;
delete window.lastSyncCmd;
delete window.lastSyncPayload;
delete window.mockSyncResponseArray;
delete window.syncCmdsHistory;
delete window.isVFSSyncWorkerInitialized;
delete window.initVFSSyncWorker;
''');
}
List<int> getSyncCmdsHistory() {
final jsVal = jsEval('JSON.stringify(window.syncCmdsHistory || [])');
if (jsVal == null) return [];
return List<int>.from(json.decode((jsVal as JSString).toDart));
}
int? getLastSyncCmd() {
final jsVal = jsEval('window.lastSyncCmd');
if (jsVal == null) return null;
return (jsVal as JSNumber).toDartInt;
}
List<int>? getLastSyncPayload() {
final jsVal = jsEval('window.lastSyncPayload ? JSON.stringify(window.lastSyncPayload) : null');
if (jsVal == null) return null;
return List<int>.from(json.decode((jsVal as JSString).toDart));
}
void setMockSyncResponse(List<int> bytes) {
final jsonBytes = json.encode(bytes);
jsEval('window.mockSyncResponseArray.push(new Uint8Array($jsonBytes));');
}