From 0b1914706842fe74b49f207ac01f91e73eb2374d Mon Sep 17 00:00:00 2001 From: "James P." Date: Tue, 14 May 2024 10:39:13 -0500 Subject: [PATCH] Added code --- package-lock.json | 54 +++++--- package.json | 6 +- src/backend.ts | 342 ++++++++++++++++++++++++++++++++++++++++++++++ src/emscripten.ts | 69 ++++++++++ src/fs.ts | 0 src/index.ts | 2 +- src/plugin.ts | 319 ++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 772 insertions(+), 20 deletions(-) create mode 100644 src/emscripten.ts delete mode 100644 src/fs.ts create mode 100644 src/plugin.ts diff --git a/package-lock.json b/package-lock.json index 48957d9..fb5b7d3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,12 +8,9 @@ "name": "@zenfs/emscripten", "version": "0.0.1", "license": "MIT", - "dependencies": { - "@zenfs/core": "^0.7.0" - }, "devDependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@types/emscripten": "^1.39.10", + "@types/emscripten": "^1.39.12", "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", "esbuild": "^0.17.18", @@ -24,6 +21,9 @@ }, "engines": { "node": ">= 18" + }, + "peerDependencies": { + "@zenfs/core": "^0.10.0" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -210,9 +210,9 @@ } }, "node_modules/@types/emscripten": { - "version": "1.39.10", - "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.10.tgz", - "integrity": "sha512-TB/6hBkYQJxsZHSqyeuO1Jt0AB/bW6G7rHt9g7lML7SOF6lbgcHvw/Lr+69iqN0qxgXLhWKScAon73JNnptuDw==", + "version": "1.39.12", + "resolved": "https://registry.npmjs.org/@types/emscripten/-/emscripten-1.39.12.tgz", + "integrity": "sha512-AQImDBgudQfMqUBfrjZYilRxoHDzTBp+ejh+g1fY67eSMalwIKtBXofjpyI0JBgNpHGzxeGAR2QDya0wxW9zbA==", "dev": true }, "node_modules/@types/json-schema": { @@ -225,6 +225,7 @@ "version": "20.12.7", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "peer": true, "dependencies": { "undici-types": "~5.26.4" } @@ -232,6 +233,7 @@ "node_modules/@types/readable-stream": { "version": "4.0.11", "license": "MIT", + "peer": true, "dependencies": { "@types/node": "*", "safe-buffer": "~5.1.1" @@ -439,15 +441,17 @@ "license": "ISC" }, "node_modules/@zenfs/core": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@zenfs/core/-/core-0.7.0.tgz", - "integrity": "sha512-gVXBrNkvhpmAUSzWBrT6GMUxAZ3++FXb6rgA4RB1H9dvXmgZf96Sz2jgf5jL2W35eMeeCo6SlE6Q9OqojoRInQ==", + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@zenfs/core/-/core-0.10.0.tgz", + "integrity": "sha512-LPcmiThDJujYiRwYEVcsR3I/zadBFuho/BZz61WKw2E8qV884US4WotcWCxBneCV/tF2THTnJdliC8KUIvEukQ==", + "peer": true, "dependencies": { "@types/node": "^20.12.5", "@types/readable-stream": "^4.0.10", "buffer": "^6.0.3", "minimatch": "^9.0.3", - "readable-stream": "^4.5.2" + "readable-stream": "^4.5.2", + "utilium": "^0.2.1" }, "bin": { "build": "scripts/build.js", @@ -460,6 +464,7 @@ "node_modules/abort-controller": { "version": "3.0.0", "license": "MIT", + "peer": true, "dependencies": { "event-target-shim": "^5.0.0" }, @@ -562,7 +567,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/brace-expansion": { "version": "2.0.1", @@ -600,6 +606,7 @@ } ], "license": "MIT", + "peer": true, "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" @@ -926,6 +933,7 @@ "node_modules/event-target-shim": { "version": "5.0.1", "license": "MIT", + "peer": true, "engines": { "node": ">=6" } @@ -933,6 +941,7 @@ "node_modules/events": { "version": "3.3.0", "license": "MIT", + "peer": true, "engines": { "node": ">=0.8.x" } @@ -1162,7 +1171,8 @@ "url": "https://feross.org/support" } ], - "license": "BSD-3-Clause" + "license": "BSD-3-Clause", + "peer": true }, "node_modules/ignore": { "version": "5.3.1", @@ -1527,6 +1537,7 @@ "node_modules/process": { "version": "0.11.10", "license": "MIT", + "peer": true, "engines": { "node": ">= 0.6.0" } @@ -1561,6 +1572,7 @@ "node_modules/readable-stream": { "version": "4.5.2", "license": "MIT", + "peer": true, "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", @@ -1627,7 +1639,8 @@ }, "node_modules/safe-buffer": { "version": "5.1.2", - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/semver": { "version": "7.6.0", @@ -1686,6 +1699,7 @@ "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", + "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -1706,7 +1720,8 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/strip-ansi": { "version": "6.0.1", @@ -1827,7 +1842,8 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "peer": true }, "node_modules/uri-js": { "version": "4.4.1", @@ -1837,6 +1853,12 @@ "punycode": "^2.1.0" } }, + "node_modules/utilium": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/utilium/-/utilium-0.2.1.tgz", + "integrity": "sha512-uLn55gYhtxFcS2X6rgvd3+aIEx5xVA3GBQgBkyJRdKAHxXXYiyB5P6ZmL/94HIWvgZaVs8xnNRCiiiRUBqyUIA==", + "peer": true + }, "node_modules/vscode-oniguruma": { "version": "1.7.0", "dev": true, diff --git a/package.json b/package.json index f22eb5a..5e8bb7f 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ }, "devDependencies": { "@fal-works/esbuild-plugin-global-externals": "^2.1.2", - "@types/emscripten": "^1.39.10", + "@types/emscripten": "^1.39.12", "@typescript-eslint/eslint-plugin": "^7.7.0", "@typescript-eslint/parser": "^7.7.0", "esbuild": "^0.17.18", @@ -46,7 +46,7 @@ "typedoc": "^0.25.1", "typescript": "5.2.2" }, - "dependencies": { - "@zenfs/core": "^0.7.0" + "peerDependencies": { + "@zenfs/core": "^0.10.0" } } diff --git a/src/backend.ts b/src/backend.ts index e69de29..d4a008c 100644 --- a/src/backend.ts +++ b/src/backend.ts @@ -0,0 +1,342 @@ +import { FileSystemMetadata, Sync, FileSystem } from '@zenfs/core/filesystem.js'; +import { Stats, FileType } from '@zenfs/core/stats.js'; +import { File } from '@zenfs/core/file.js'; +import { ErrnoError, Errno, errorMessages } from '@zenfs/core/error.js'; +import { Cred } from '@zenfs/core/cred.js'; +import { Buffer } from 'buffer'; +import type { Backend } from '@zenfs/core'; +import * as emscripten from './emscripten.js'; +import { basename, dirname } from '@zenfs/core/emulation/path.js'; + +/** + * @hidden + */ +function convertError(e: FS.ErrnoError & { node?: FS.FSNode }, path: string = ''): ErrnoError { + const errno = e.errno; + let parent = e.node; + const paths: string[] = []; + while (parent) { + paths.unshift(parent.name); + if (parent === parent.parent) { + break; + } + parent = parent.parent; + } + return new ErrnoError(errno, errorMessages[errno], paths.length > 0 ? '/' + paths.join('/') : path); +} + +export class EmscriptenFile extends File { + constructor( + protected _fs: EmscriptenFS, + protected _FS: typeof FS, + public readonly path: string, + protected _stream: emscripten.Stream + ) { + super(); + } + public get position(): number { + return; + } + public async close(): Promise { + return this.closeSync(); + } + public closeSync(): void { + try { + this._FS.close(this._stream); + } catch (e) { + throw convertError(e, this.path); + } + } + public async stat(): Promise { + return this.statSync(); + } + public statSync(): Stats { + try { + return this._fs.statSync(this.path); + } catch (e) { + throw convertError(e, this.path); + } + } + public async truncate(len: number): Promise { + return this.truncateSync(len); + } + public truncateSync(len: number): void { + try { + this._FS.ftruncate(this._stream.fd, len); + } catch (e) { + throw convertError(e, this.path); + } + } + public async write(buffer: Buffer, offset: number, length: number, position: number): Promise { + return this.writeSync(buffer, offset, length, position); + } + public writeSync(buffer: Buffer, offset: number, length: number, position: number | null): number { + try { + // Emscripten is particular about what position is set to. + const emPosition = position === null ? undefined : position; + return this._FS.write(this._stream, buffer, offset, length, emPosition); + } catch (e) { + throw convertError(e, this.path); + } + } + public async read(buffer: TBuffer, offset: number, length: number, position: number): Promise<{ bytesRead: number; buffer: TBuffer }> { + return { bytesRead: this.readSync(buffer, offset, length, position), buffer }; + } + public readSync(buffer: ArrayBufferView, offset: number, length: number, position: number | null): number { + try { + // Emscripten is particular about what position is set to. + const emPosition = position === null ? undefined : position; + return this._FS.read(this._stream, buffer, offset, length, emPosition); + } catch (e) { + throw convertError(e, this.path); + } + } + public async sync(): Promise { + this.syncSync(); + } + public syncSync(): void { + // NOP. + } + public async chown(uid: number, gid: number): Promise { + return this.chownSync(uid, gid); + } + public chownSync(uid: number, gid: number): void { + try { + this._FS.fchown(this._stream.fd, uid, gid); + } catch (e) { + throw convertError(e, this.path); + } + } + public async chmod(mode: number): Promise { + return this.chmodSync(mode); + } + public chmodSync(mode: number): void { + try { + this._FS.fchmod(this._stream.fd, mode); + } catch (e) { + throw convertError(e, this.path); + } + } + public async utimes(atime: Date, mtime: Date): Promise { + return this.utimesSync(atime, mtime); + } + public utimesSync(atime: Date, mtime: Date): void { + this._fs.utimesSync(this.path, atime, mtime); + } + public async _setType(type: FileType): Promise { + throw ErrnoError.With('ENOSYS', this.path, '_setType'); + } + public _setTypeSync(type: FileType): void { + throw ErrnoError.With('ENOSYS', this.path, '_setType'); + } +} + +/** + * Configuration options for Emscripten file system. + */ +export interface EmscriptenOptions { + /** + * The Emscripten file system to use + */ + FS: typeof FS; +} + +/** + * Mounts an Emscripten file system into the BrowserFS file system. + */ +export class EmscriptenFS extends Sync(FileSystem) { + public constructor( + /** + * The Emscripten FS + */ + protected em: typeof FS + ) { + super(); + } + + public metadata(): FileSystemMetadata { + const name = 'DB_NAME' in this.em && typeof this.em.DB_NAME == 'function' ? this.em.DB_NAME() : super.metadata().name; + return { + ...super.metadata(), + name, + }; + } + + public syncSync(path: string, data: Uint8Array, stats: Readonly): void { + try { + this.em.writeFile(path, data); + this.em.chmod(path, stats.mode); + } catch (e) { + throw convertError(e, path); + } + } + + public renameSync(oldPath: string, newPath: string, cred: Cred): void { + try { + this.em.rename(oldPath, newPath); + } catch (e) { + throw convertError(e, e.errno != Errno.ENOENT ? '' : this.existsSync(oldPath, cred) ? newPath : oldPath); + } + } + + public statSync(path: string): Stats { + try { + const stats = this.em.stat(path); + const itemType = this.modeToFileType(stats.mode); + return new Stats({ + mode: itemType | stats.mode, + size: stats.size, + atimeMs: stats.atime.getTime(), + mtimeMs: stats.mtime.getTime(), + ctimeMs: stats.ctime.getTime(), + }); + } catch (e) { + throw convertError(e, path); + } + } + + public createFileSync(path: string): EmscriptenFile { + try { + const node = this.em.createDataFile(dirname(path), basename(path), new Uint8Array(), true, true, true); + const stream = new this.em.FSStream(); + stream.object = node; + return new EmscriptenFile(this, this.em, path, stream); + } catch (e) { + throw convertError(e, path); + } + } + + public openFileSync(path: string, flag: string): EmscriptenFile { + try { + const stream = this.em.open(path, flag); + return new EmscriptenFile(this, this.em, path, stream); + } catch (e) { + throw convertError(e, path); + } + } + + public unlinkSync(path: string): void { + try { + this.em.unlink(path); + } catch (e) { + throw convertError(e, path); + } + } + + public rmdirSync(path: string): void { + try { + this.em.rmdir(path); + } catch (e) { + throw convertError(e, path); + } + } + + public mkdirSync(path: string, mode: number): void { + try { + this.em.mkdir(path, mode); + } catch (e) { + throw convertError(e, path); + } + } + + public readdirSync(path: string): string[] { + try { + // Emscripten returns items for '.' and '..'. Node does not. + return this.em.readdir(path).filter((p: string) => p !== '.' && p !== '..'); + } catch (e) { + throw convertError(e, path); + } + } + + public truncateSync(path: string, len: number): void { + try { + this.em.truncate(path, len); + } catch (e) { + throw convertError(e, path); + } + } + + public chmodSync(path: string, mode: number) { + try { + this.em.chmod(path, mode); + } catch (e) { + throw convertError(e, path); + } + } + + public chownSync(path: string, new_uid: number, new_gid: number): void { + try { + this.em.chown(path, new_uid, new_gid); + } catch (e) { + throw convertError(e, path); + } + } + + public symlinkSync(srcpath: string, dstpath: string): void { + try { + this.em.symlink(srcpath, dstpath); + } catch (e) { + throw convertError(e); + } + } + + /** + * Right now this method is just a mask for symlinks + * @todo track hard links + */ + public linkSync(srcpath: string, dstpath: string): void { + try { + this.em.symlink(srcpath, dstpath); + } catch (e) { + throw convertError(e); + } + } + + public readlinkSync(path: string): string { + try { + return this.em.readlink(path); + } catch (e) { + throw convertError(e, path); + } + } + + public utimesSync(path: string, atime: Date, mtime: Date): void { + try { + this.em.utime(path, atime.getTime(), mtime.getTime()); + } catch (e) { + throw convertError(e, path); + } + } + + private modeToFileType(mode: number): FileType { + if (this.em.isDir(mode)) { + return FileType.DIRECTORY; + } else if (this.em.isFile(mode)) { + return FileType.FILE; + } else if (this.em.isLink(mode)) { + return FileType.SYMLINK; + } else { + throw new ErrnoError(Errno.EPERM, 'Invalid mode: ' + mode); + } + } +} + +export const Emscripten = { + name: 'EmscriptenFileSystem', + + options: { + FS: { + type: 'object', + required: true, + description: 'The Emscripten file system to use (the `FS` variable)', + }, + }, + + isAvailable(): boolean { + return true; + }, + + create(options: EmscriptenOptions) { + return new EmscriptenFS(options.FS); + }, +} satisfies Backend; diff --git a/src/emscripten.ts b/src/emscripten.ts new file mode 100644 index 0000000..0654f16 --- /dev/null +++ b/src/emscripten.ts @@ -0,0 +1,69 @@ +/** + * Defines an Emscripten file system object for use in the Emscripten virtual + * filesystem. Allows you to use synchronous BrowserFS file systems from within + * Emscripten. + * + * You can construct a BFSEmscriptenFS, mount it using its mount command, + * and then mount it into Emscripten. + * + * Adapted from Emscripten's NodeFS: + * https://raw.github.com/kripken/emscripten/master/src/library_nodefs.js + */ +import 'emscripten'; // Note: this is for types only. + +export interface Stats { + dev: number; + ino: number; + mode: number; + nlink: number; + uid: number; + gid: number; + rdev: number; + size: number; + blksize: number; + blocks: number; + atime: Date; + mtime: Date; + ctime: Date; + timestamp?: number; +} + +export interface NodeOps { + getattr(node: FS.FSNode): Stats; + setattr(node: FS.FSNode, attr: Stats): void; + lookup(parent: FS.FSNode, name: string): FS.FSNode; + mknod(parent: FS.FSNode, name: string, mode: number, dev: unknown): FS.FSNode; + rename(oldNode: FS.FSNode, newDir: FS.FSNode, newName: string): void; + unlink(parent: FS.FSNode, name: string): void; + rmdir(parent: FS.FSNode, name: string): void; + readdir(node: FS.FSNode): string[]; + symlink(parent: FS.FSNode, newName: string, oldPath: string): void; + readlink(node: FS.FSNode): string; +} + +export declare class Node extends FS.FSNode { + node_ops?: NodeOps; + stream_ops?: StreamOps; +} + +export interface StreamOps { + open(stream: FS.FSStream): void; + close(stream: FS.FSStream): void; + read(stream: FS.FSStream, buffer: Uint8Array, offset: number, length: number, position: number): number; + write(stream: FS.FSStream, buffer: Uint8Array, offset: number, length: number, position: number): number; + llseek(stream: FS.FSStream, offset: number, whence: number): number; +} + +export declare class Stream extends FS.FSStream { + fd?: number; + nfd?: number; +} + +export interface Plugin { + node_ops: NodeOps; + stream_ops: StreamOps; + mount(mount: { opts: { root: string } }): FS.FSNode; + createNode(parent: FS.FSNode, name: string, mode: number, dev?: unknown): FS.FSNode; + getMode(path: string): number; + realPath(node: FS.FSNode): string; +} diff --git a/src/fs.ts b/src/fs.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/index.ts b/src/index.ts index 0bba853..5897a0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,2 +1,2 @@ export * from './backend.js'; -export * from './fs.js'; +export * from './plugin.js'; diff --git a/src/plugin.ts b/src/plugin.ts new file mode 100644 index 0000000..88801ec --- /dev/null +++ b/src/plugin.ts @@ -0,0 +1,319 @@ +import fs, { parseFlag, type Stats } from '@zenfs/core'; +import type * as emscripten from './emscripten.js'; + +class StreamOps implements emscripten.StreamOps { + get nodefs(): typeof fs { + return this._fs.nodefs; + } + + get FS() { + return this._fs.FS; + } + + get PATH() { + return this._fs.PATH; + } + + get ERRNO_CODES() { + return this._fs.ERRNO_CODES; + } + + constructor(protected _fs: ZenFSEmscriptenPlugin) {} + + public open(stream: emscripten.Stream): void { + const path = this._fs.realPath(stream.object); + const FS = this.FS; + try { + if (FS.isFile(stream.object.mode)) { + stream.nfd = this.nodefs.openSync(path, parseFlag(stream.flags)); + } + } catch (e) { + if (!e.code) { + throw e; + } + throw new FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public close(stream: emscripten.Stream): void { + const FS = this.FS; + try { + if (FS.isFile(stream.object.mode) && stream.nfd) { + this.nodefs.closeSync(stream.nfd); + } + } catch (e) { + if (!e.code) { + throw e; + } + throw new FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public read(stream: emscripten.Stream, buffer: Uint8Array, offset: number, length: number, position: number): number { + // Avoid copying overhead by reading directly into buffer. + try { + return this.nodefs.readSync(stream.nfd, Buffer.from(buffer), offset, length, position); + } catch (e) { + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public write(stream: emscripten.Stream, buffer: Uint8Array, offset: number, length: number, position: number): number { + // Avoid copying overhead. + try { + return this.nodefs.writeSync(stream.nfd, buffer, offset, length, position); + } catch (e) { + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public llseek(stream: emscripten.Stream, offset: number, whence: number): number { + let position = offset; + if (whence === 1) { + // SEEK_CUR. + position += stream.position; + } else if (whence === 2) { + // SEEK_END. + if (this.FS.isFile(stream.object.mode)) { + try { + const stat = this.nodefs.fstatSync(stream.nfd); + position += stat.size; + } catch (e) { + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + } + + if (position < 0) { + throw new this.FS.ErrnoError(this.ERRNO_CODES.EINVAL); + } + + stream.position = position; + return position; + } +} + +class EntryOps implements emscripten.NodeOps { + get nodefs(): typeof fs { + return this._fs.nodefs; + } + + get FS() { + return this._fs.FS; + } + + get PATH() { + return this._fs.PATH; + } + + get ERRNO_CODES() { + return this._fs.ERRNO_CODES; + } + + constructor(protected _fs: ZenFSEmscriptenPlugin) {} + + public getattr(node: FS.FSNode): Stats { + const path = this._fs.realPath(node); + let stat: Stats; + try { + stat = this.nodefs.lstatSync(path); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + return stat; + } + + public setattr(node: FS.FSNode, attr: emscripten.Stats): void { + const path = this._fs.realPath(node); + try { + if (attr.mode !== undefined) { + this.nodefs.chmodSync(path, attr.mode); + // update the common node structure mode as well + node.mode = attr.mode; + } + if (attr.timestamp !== undefined) { + const date = new Date(attr.timestamp); + this.nodefs.utimesSync(path, date, date); + } + } catch (e) { + if (!e.code) { + throw e; + } + // Ignore not supported errors. Emscripten does utimesSync when it + // writes files, but never really requires the value to be set. + if (e.code !== 'ENOTSUP') { + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + if (attr.size !== undefined) { + try { + this.nodefs.truncateSync(path, attr.size); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + } + + public lookup(parent: FS.FSNode, name: string): FS.FSNode { + const path = this.PATH.join2(this._fs.realPath(parent), name); + const mode = this._fs.getMode(path); + return this._fs.createNode(parent, name, mode); + } + + public mknod(parent: FS.FSNode, name: string, mode: number, dev: number): FS.FSNode { + const node = this._fs.createNode(parent, name, mode, dev); + // create the backing node for this in the fs root as well + const path = this._fs.realPath(node); + try { + if (this.FS.isDir(node.mode)) { + this.nodefs.mkdirSync(path, node.mode); + } else { + this.nodefs.writeFileSync(path, '', { mode: node.mode }); + } + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + return node; + } + + public rename(oldNode: FS.FSNode, newDir: FS.FSNode, newName: string): void { + const oldPath = this._fs.realPath(oldNode); + const newPath = this.PATH.join2(this._fs.realPath(newDir), newName); + try { + this.nodefs.renameSync(oldPath, newPath); + // This logic is missing from the original NodeFS, + // causing Emscripten's filesystem to think that the old file still exists. + oldNode.name = newName; + oldNode.parent = newDir; + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public unlink(parent: FS.FSNode, name: string): void { + const path = this.PATH.join2(this._fs.realPath(parent), name); + try { + this.nodefs.unlinkSync(path); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public rmdir(parent: FS.FSNode, name: string) { + const path = this.PATH.join2(this._fs.realPath(parent), name); + try { + this.nodefs.rmdirSync(path); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public readdir(node: FS.FSNode): string[] { + const path = this._fs.realPath(node); + try { + // Node does not list . and .. in directory listings, + // but Emscripten expects it. + const contents = this.nodefs.readdirSync(path); + contents.push('.', '..'); + return contents; + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public symlink(parent: FS.FSNode, newName: string, oldPath: string): void { + const newPath = this.PATH.join2(this._fs.realPath(parent), newName); + try { + this.nodefs.symlinkSync(oldPath, newPath); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } + + public readlink(node: FS.FSNode): string { + const path = this._fs.realPath(node); + try { + return this.nodefs.readlinkSync(path, 'utf8'); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + } +} + +export default class ZenFSEmscriptenPlugin implements emscripten.Plugin { + public node_ops: emscripten.NodeOps = new EntryOps(this); + public stream_ops: emscripten.StreamOps = new StreamOps(this); + + constructor( + public readonly FS = globalThis.FS, + public readonly PATH = globalThis.PATH, + public readonly ERRNO_CODES = globalThis.ERRNO_CODES, + public readonly nodefs: typeof fs = fs + ) {} + + public mount(m: { opts: { root: string } }): FS.FSNode { + return this.createNode(null, '/', this.getMode(m.opts.root), 0); + } + + public createNode(parent: FS.FSNode | null, name: string, mode: number, rdev?: number): FS.FSNode { + const FS = this.FS; + if (!FS.isDir(mode) && !FS.isFile(mode) && !FS.isLink(mode)) { + throw new FS.ErrnoError(this.ERRNO_CODES.EINVAL); + } + const node: emscripten.Node = new FS.FSNode(parent, name, mode, rdev); + node.node_ops = this.node_ops; + node.stream_ops = this.stream_ops; + return node; + } + + public getMode(path: string): number { + let stat: Stats; + try { + stat = this.nodefs.lstatSync(path); + } catch (e) { + if (!e.code) { + throw e; + } + throw new this.FS.ErrnoError(this.ERRNO_CODES[e.code]); + } + return stat.mode; + } + + public realPath(node: FS.FSNode): string { + const parts: string[] = []; + while (node.parent !== node) { + parts.push(node.name); + node = node.parent; + } + parts.push(node.mount.opts.root); + parts.reverse(); + return this.PATH.join.apply(null, parts); + } +}