From ea0cf86da3a9fe102e8639c851a691c3144458ff Mon Sep 17 00:00:00 2001 From: James Prevett Date: Thu, 7 Nov 2024 15:10:43 -0600 Subject: [PATCH 01/13] Added context --- src/context.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/context.ts diff --git a/src/context.ts b/src/context.ts new file mode 100644 index 00000000..85ec50d0 --- /dev/null +++ b/src/context.ts @@ -0,0 +1,33 @@ +import { getByString, type ExtractProperties } from 'utilium'; +import * as fs from './emulation/index.js'; +import type { AbsolutePath } from './emulation/path.js'; +import { credentials, type Credentials } from './credentials.js'; + +/* eslint-disable @typescript-eslint/no-explicit-any */ +type Fn_FS = keyof ExtractProperties any>; +type Fn_Promises = keyof ExtractProperties any>; +/* eslint-enable @typescript-eslint/no-explicit-any */ + +type FnName = Fn_FS | `promises.${Fn_Promises}`; +type Fn = T extends `promises.${infer U extends Fn_Promises}` ? (typeof fs.promises)[U] : T extends Fn_FS ? (typeof fs)[T] : never; + +/** + * Allows you to restrict operations to a specific root path and set of credentials. + * Not implemented. + * @experimental + */ +export class FSContext { + public constructor( + public readonly root: AbsolutePath, + public readonly creds: Credentials = credentials + ) {} + + public call(method: K, ...args: Parameters>): ReturnType> { + // @ts-expect-error 2349 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const value = getByString(fs, method)(...args); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + } +} From 0a4a3a9fe4c6c252875b4deacff603e1e52d4aa2 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Thu, 7 Nov 2024 19:18:43 -0600 Subject: [PATCH 02/13] Update tests --- src/emulation/sync.ts | 4 ++-- tests/fs/times.test.ts | 29 +++++++++-------------------- 2 files changed, 11 insertions(+), 22 deletions(-) diff --git a/src/emulation/sync.ts b/src/emulation/sync.ts index 3f0caca3..8362178b 100644 --- a/src/emulation/sync.ts +++ b/src/emulation/sync.ts @@ -243,10 +243,10 @@ writeFileSync satisfies typeof fs.writeFileSync; * Asynchronously append data to a file, creating the file if it not yet exists. * @option encoding Defaults to `'utf8'`. * @option mode Defaults to `0644`. - * @option flag Defaults to `'a'`. + * @option flag Defaults to `'a+'`. */ export function appendFileSync(filename: fs.PathOrFileDescriptor, data: FileContents, _options: fs.WriteFileOptions = {}): void { - const options = normalizeOptions(_options, 'utf8', 'a', 0o644); + const options = normalizeOptions(_options, 'utf8', 'a+', 0o644); const flag = parseFlag(options.flag); if (!isAppendable(flag)) { throw new ErrnoError(Errno.EINVAL, 'Flag passed to appendFile must allow for appending.'); diff --git a/tests/fs/times.test.ts b/tests/fs/times.test.ts index 9e646144..e65b992b 100644 --- a/tests/fs/times.test.ts +++ b/tests/fs/times.test.ts @@ -5,14 +5,14 @@ import { ErrnoError } from '../../dist/error.js'; import { _toUnixTimestamp } from '../../dist/utils.js'; import { fs } from '../common.js'; -suite('times', () => { - const path = 'x.txt'; +const path = 'x.txt'; +suite('times', () => { function expect_assert(resource: string | number, atime: Date | number, mtime: Date | number) { const stats = typeof resource == 'string' ? fs.statSync(resource) : fs.fstatSync(resource); // check up to single-second precision since sub-second precision is OS and fs dependent - assert(_toUnixTimestamp(atime) == _toUnixTimestamp(stats.atime)); - assert(_toUnixTimestamp(mtime) == _toUnixTimestamp(stats.mtime)); + assert.equal(_toUnixTimestamp(atime), _toUnixTimestamp(stats.atime)); + assert.equal(_toUnixTimestamp(mtime), _toUnixTimestamp(stats.mtime)); } async function runTest(atime: Date | number, mtime: Date | number): Promise { @@ -24,8 +24,7 @@ suite('times', () => { assert.strictEqual(error.code, 'ENOENT'); }); - // don't close this fd - const handle = await fs.promises.open(path, 'r'); + await using handle = await fs.promises.open(path, 'r'); await handle.utimes(atime, mtime); expect_assert(handle.fd, atime, mtime); @@ -33,16 +32,6 @@ suite('times', () => { fs.utimesSync(path, atime, mtime); expect_assert(path, atime, mtime); - // some systems don't have futimes - // if there's an error, it be ENOSYS - try { - fs.futimesSync(handle.fd, atime, mtime); - expect_assert(handle.fd, atime, mtime); - } catch (error: any) { - assert(error instanceof ErrnoError); - assert.strictEqual(error.code, 'ENOSYS'); - } - try { fs.utimesSync('foobarbaz', atime, mtime); } catch (error: any) { @@ -59,11 +48,11 @@ suite('times', () => { } test('utimes works', async () => { - await runTest(new Date('1982/09/10 13:37:00'), new Date('1982/09/10 13:37:00')); - await runTest(new Date(), new Date()); - await runTest(123456.789, 123456.789); + await test('new Date(...)', () => runTest(new Date('1982/09/10 13:37:00'), new Date('1982/09/10 13:37:00'))); + await test('new Date()', () => runTest(new Date(), new Date())); + await test('number', () => runTest(123456.789, 123456.789)); const stats = fs.statSync(path); - await runTest(stats.atime, stats.mtime); + await test('from stats', () => runTest(stats.atime, stats.mtime)); }); test('read changes atime', async () => { From c726eed80433433d3b59599eff215b711cc51de1 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Mon, 11 Nov 2024 11:05:39 -0600 Subject: [PATCH 03/13] Move to interface for context --- src/context.ts | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/context.ts b/src/context.ts index 85ec50d0..52714a0c 100644 --- a/src/context.ts +++ b/src/context.ts @@ -16,18 +16,29 @@ type Fn = T extends `promises.${infer U extends Fn_Promises}` * Not implemented. * @experimental */ -export class FSContext { - public constructor( - public readonly root: AbsolutePath, - public readonly creds: Credentials = credentials - ) {} +export interface FSContext { + readonly root: AbsolutePath; + readonly creds: Credentials; - public call(method: K, ...args: Parameters>): ReturnType> { - // @ts-expect-error 2349 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const value = getByString(fs, method)(...args); + call(method: K, ...args: Parameters>): ReturnType>; +} + +/** + * Allows you to restrict operations to a specific root path and set of credentials. + * Not implemented. + * @experimental + */ +export function createContext(root: AbsolutePath, creds: Credentials = credentials): FSContext { + return { + root, + creds, + call(method: K, ...args: Parameters>): ReturnType> { + // @ts-expect-error 2349 + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment + const value = getByString(fs, method)(...args); - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - } + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return value; + }, + }; } From 422ec508ed601020ef8a69e123568baed0f61d67 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Mon, 11 Nov 2024 11:18:53 -0600 Subject: [PATCH 04/13] Fixed utimes tests --- tests/fs/times.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/fs/times.test.ts b/tests/fs/times.test.ts index e65b992b..1df87229 100644 --- a/tests/fs/times.test.ts +++ b/tests/fs/times.test.ts @@ -16,7 +16,7 @@ suite('times', () => { } async function runTest(atime: Date | number, mtime: Date | number): Promise { - await fs.promises.utimes(path, atime, mtime); + fs.utimesSync(path, atime, mtime); expect_assert(path, atime, mtime); await fs.promises.utimes('foobarbaz', atime, mtime).catch((error: ErrnoError) => { From 60888538efbd8e4bd26ce8f8ae1746657c72ff20 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sat, 16 Nov 2024 14:21:19 -0600 Subject: [PATCH 05/13] Added context binding to sync functions, `resolveMount` --- src/context.ts | 48 ++++--- src/emulation/shared.ts | 8 +- src/emulation/sync.ts | 273 ++++++++++++++++++++-------------------- tests/assignment.ts | 2 +- 4 files changed, 178 insertions(+), 153 deletions(-) diff --git a/src/context.ts b/src/context.ts index 52714a0c..f05dc469 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,37 +1,48 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { getByString, type ExtractProperties } from 'utilium'; import * as fs from './emulation/index.js'; import type { AbsolutePath } from './emulation/path.js'; -import { credentials, type Credentials } from './credentials.js'; +import { credentials as defaultCredentials, type Credentials } from './credentials.js'; -/* eslint-disable @typescript-eslint/no-explicit-any */ -type Fn_FS = keyof ExtractProperties any>; -type Fn_Promises = keyof ExtractProperties any>; -/* eslint-enable @typescript-eslint/no-explicit-any */ +type Fn_FS = Omit any>, 'mountObject'>; +type Fn_Promises = ExtractProperties any>; -type FnName = Fn_FS | `promises.${Fn_Promises}`; -type Fn = T extends `promises.${infer U extends Fn_Promises}` ? (typeof fs.promises)[U] : T extends Fn_FS ? (typeof fs)[T] : never; +type FnName = keyof Fn_FS | `promises.${keyof Fn_Promises}`; +type Fn = T extends `promises.${infer U extends keyof Fn_Promises}` ? (typeof fs.promises)[U] : T extends keyof Fn_FS ? (typeof fs)[T] : never; /** - * Allows you to restrict operations to a specific root path and set of credentials. - * Not implemented. - * @experimental + * Binds a this value for all of the functions in an object (not recursive) + * @internal */ +function _bindFunctions>(fns: T, thisValue: any): T { + return Object.fromEntries(Object.entries(fns).map(([k, v]) => [k, typeof v == 'function' ? v.bind(thisValue) : v])) as T; +} + export interface FSContext { readonly root: AbsolutePath; - readonly creds: Credentials; + readonly credentials: Credentials; +} + +export type V_Context = Partial | void | Record; +/** + * Allows you to restrict operations to a specific root path and set of credentials. + * @experimental + */ +export interface BoundContext extends Fn_FS, FSContext { call(method: K, ...args: Parameters>): ReturnType>; + + promises: Fn_Promises; } /** * Allows you to restrict operations to a specific root path and set of credentials. - * Not implemented. * @experimental */ -export function createContext(root: AbsolutePath, creds: Credentials = credentials): FSContext { - return { +export function bindContext(root: AbsolutePath, credentials: Credentials = defaultCredentials): BoundContext { + const ctx = { root, - creds, + credentials, call(method: K, ...args: Parameters>): ReturnType> { // @ts-expect-error 2349 // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment @@ -40,5 +51,10 @@ export function createContext(root: AbsolutePath, creds: Credentials = credentia // eslint-disable-next-line @typescript-eslint/no-unsafe-return return value; }, - }; + } satisfies FSContext & { call: any }; + + const fn_fs = _bindFunctions(fs, ctx); + const fn_promises = _bindFunctions(fs.promises, ctx); + + return { ...ctx, ...fn_fs, promises: fn_promises }; } diff --git a/src/emulation/shared.ts b/src/emulation/shared.ts index 48433395..6be90e27 100644 --- a/src/emulation/shared.ts +++ b/src/emulation/shared.ts @@ -8,6 +8,7 @@ import type { FileSystem } from '../filesystem.js'; import { normalizePath } from '../utils.js'; import { resolve, type AbsolutePath } from './path.js'; import { size_max } from './constants.js'; +import type { V_Context } from '../context.js'; // descriptors export const fdMap: Map = new Map(); @@ -66,15 +67,16 @@ export function umount(mountPoint: string): void { /** * Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root */ -export function resolveMount(path: string): { fs: FileSystem; path: string; mountPoint: string } { - path = normalizePath(path); +export function resolveMount(path: string, ctx: V_Context): { fs: FileSystem; path: string; mountPoint: string } { + const root = typeof ctx == 'object' && typeof ctx.root == 'string' ? ctx.root : '/'; + path = normalizePath(root + path); const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length for (const [mountPoint, fs] of sortedMounts) { // We know path is normalized, so it would be a substring of the mount point. if (mountPoint.length <= path.length && path.startsWith(mountPoint)) { path = path.slice(mountPoint.length > 1 ? mountPoint.length : 0); // Resolve the path relative to the mount point if (path === '') { - path = '/'; + path = root; } return { fs, path, mountPoint }; } diff --git a/src/emulation/sync.ts b/src/emulation/sync.ts index ac2ddcec..c0a1fd3e 100644 --- a/src/emulation/sync.ts +++ b/src/emulation/sync.ts @@ -13,13 +13,14 @@ import { Dir, Dirent } from './dir.js'; import { dirname, join, parse } from './path.js'; import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js'; import { emitChange } from './watchers.js'; +import type { V_Context } from '../context.js'; -export function renameSync(oldPath: fs.PathLike, newPath: fs.PathLike): void { +export function renameSync(this: V_Context, oldPath: fs.PathLike, newPath: fs.PathLike): void { oldPath = normalizePath(oldPath); newPath = normalizePath(newPath); - const oldMount = resolveMount(oldPath); - const newMount = resolveMount(newPath); - if (config.checkAccess && !statSync(dirname(oldPath)).hasAccess(constants.W_OK)) { + const oldMount = resolveMount(oldPath, this); + const newMount = resolveMount(newPath, this); + if (config.checkAccess && !statSync.call, Stats>(this, dirname(oldPath)).hasAccess(constants.W_OK)) { throw ErrnoError.With('EACCES', oldPath, 'rename'); } try { @@ -29,8 +30,8 @@ export function renameSync(oldPath: fs.PathLike, newPath: fs.PathLike): void { return; } - writeFileSync(newPath, readFileSync(oldPath)); - unlinkSync(oldPath); + writeFileSync.call(this, newPath, readFileSync(oldPath)); + unlinkSync.call(this, oldPath); emitChange('rename', oldPath.toString()); } catch (e) { throw fixError(e as ErrnoError, { [oldMount.path]: oldPath, [newMount.path]: newPath }); @@ -41,10 +42,10 @@ renameSync satisfies typeof fs.renameSync; /** * Test whether or not `path` exists by checking with the file system. */ -export function existsSync(path: fs.PathLike): boolean { +export function existsSync(this: V_Context, path: fs.PathLike): boolean { path = normalizePath(path); try { - const { fs, path: resolvedPath } = resolveMount(realpathSync(path)); + const { fs, path: resolvedPath } = resolveMount(realpathSync(path), this); return fs.existsSync(resolvedPath); } catch (e) { if ((e as ErrnoError).errno == Errno.ENOENT) { @@ -56,11 +57,11 @@ export function existsSync(path: fs.PathLike): boolean { } existsSync satisfies typeof fs.existsSync; -export function statSync(path: fs.PathLike, options?: { bigint?: boolean }): Stats; -export function statSync(path: fs.PathLike, options: { bigint: true }): BigIntStats; -export function statSync(path: fs.PathLike, options?: fs.StatOptions): Stats | BigIntStats { +export function statSync(this: V_Context, path: fs.PathLike, options?: { bigint?: boolean }): Stats; +export function statSync(this: V_Context, path: fs.PathLike, options: { bigint: true }): BigIntStats; +export function statSync(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Stats | BigIntStats { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(realpathSync(path)); + const { fs, path: resolved } = resolveMount(realpathSync(path), this); try { const stats = fs.statSync(resolved); if (config.checkAccess && !stats.hasAccess(constants.R_OK)) { @@ -71,18 +72,18 @@ export function statSync(path: fs.PathLike, options?: fs.StatOptions): Stats | B throw fixError(e as ErrnoError, { [resolved]: path }); } } -statSync satisfies typeof fs.statSync; +statSync satisfies fs.StatSyncFn; /** * Synchronous `lstat`. * `lstat()` is identical to `stat()`, except that if path is a symbolic link, * then the link itself is stat-ed, not the file that it refers to. */ -export function lstatSync(path: fs.PathLike, options?: { bigint?: boolean }): Stats; -export function lstatSync(path: fs.PathLike, options: { bigint: true }): BigIntStats; -export function lstatSync(path: fs.PathLike, options?: fs.StatOptions): Stats | BigIntStats { +export function lstatSync(this: V_Context, path: fs.PathLike, options?: { bigint?: boolean }): Stats; +export function lstatSync(this: V_Context, path: fs.PathLike, options: { bigint: true }): BigIntStats; +export function lstatSync(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Stats | BigIntStats { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(path); + const { fs, path: resolved } = resolveMount(path, this); try { const stats = fs.statSync(resolved); return options?.bigint ? new BigIntStats(stats) : stats; @@ -92,7 +93,7 @@ export function lstatSync(path: fs.PathLike, options?: fs.StatOptions): Stats | } lstatSync satisfies typeof fs.lstatSync; -export function truncateSync(path: fs.PathLike, len: number | null = 0): void { +export function truncateSync(this: V_Context, path: fs.PathLike, len: number | null = 0): void { using file = _openSync(path, 'r+'); len ||= 0; if (len < 0) { @@ -102,9 +103,9 @@ export function truncateSync(path: fs.PathLike, len: number | null = 0): void { } truncateSync satisfies typeof fs.truncateSync; -export function unlinkSync(path: fs.PathLike): void { +export function unlinkSync(this: V_Context, path: fs.PathLike): void { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(path); + const { fs, path: resolved } = resolveMount(path, this); try { if (config.checkAccess && !(cache.stats.getSync(path) || fs.statSync(resolved)).hasAccess(constants.W_OK)) { throw ErrnoError.With('EACCES', resolved, 'unlink'); @@ -117,13 +118,13 @@ export function unlinkSync(path: fs.PathLike): void { } unlinkSync satisfies typeof fs.unlinkSync; -function _openSync(path: fs.PathLike, _flag: fs.OpenMode, _mode?: fs.Mode | null, resolveSymlinks: boolean = true): File { +function _openSync(this: V_Context, path: fs.PathLike, _flag: fs.OpenMode, _mode?: fs.Mode | null, resolveSymlinks: boolean = true): File { path = normalizePath(path); const mode = normalizeMode(_mode, 0o644), flag = parseFlag(_flag); - path = resolveSymlinks ? realpathSync(path) : path; - const { fs, path: resolved } = resolveMount(path); + path = resolveSymlinks ? realpathSync.call(this, path) : path; + const { fs, path: resolved } = resolveMount(path, this); let stats: Stats | undefined; try { @@ -168,8 +169,8 @@ function _openSync(path: fs.PathLike, _flag: fs.OpenMode, _mode?: fs.Mode | null * Synchronous file open. * @see http://www.manpagez.com/man/2/open/ */ -export function openSync(path: fs.PathLike, flag: fs.OpenMode, mode: fs.Mode | null = constants.F_OK): number { - return file2fd(_openSync(path, flag, mode, true)); +export function openSync(this: V_Context, path: fs.PathLike, flag: fs.OpenMode, mode: fs.Mode | null = constants.F_OK): number { + return file2fd(_openSync.call(this, path, flag, mode, true)); } openSync satisfies typeof fs.openSync; @@ -177,13 +178,13 @@ openSync satisfies typeof fs.openSync; * Opens a file or symlink * @internal */ -export function lopenSync(path: fs.PathLike, flag: string, mode?: fs.Mode | null): number { - return file2fd(_openSync(path, flag, mode, false)); +export function lopenSync(this: V_Context, path: fs.PathLike, flag: string, mode?: fs.Mode | null): number { + return file2fd(_openSync.call(this, path, flag, mode, false)); } -function _readFileSync(fname: string, flag: string, resolveSymlinks: boolean): Uint8Array { +function _readFileSync(this: V_Context, fname: string, flag: string, resolveSymlinks: boolean): Uint8Array { // Get file. - using file = _openSync(fname, flag, 0o644, resolveSymlinks); + using file = _openSync.call(this, fname, flag, 0o644, resolveSymlinks); const stat = file.statSync(); // Allocate buffer. const data = new Uint8Array(stat.size); @@ -197,15 +198,15 @@ function _readFileSync(fname: string, flag: string, resolveSymlinks: boolean): U * @option flag Defaults to `'r'`. * @returns file contents */ -export function readFileSync(path: fs.PathOrFileDescriptor, options?: { flag?: string } | null): Buffer; -export function readFileSync(path: fs.PathOrFileDescriptor, options?: (fs.EncodingOption & { flag?: string }) | BufferEncoding | null): string; -export function readFileSync(path: fs.PathOrFileDescriptor, _options: fs.WriteFileOptions | null = {}): FileContents { +export function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options?: { flag?: string } | null): Buffer; +export function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, options?: (fs.EncodingOption & { flag?: string }) | BufferEncoding | null): string; +export function readFileSync(this: V_Context, path: fs.PathOrFileDescriptor, _options: fs.WriteFileOptions | null = {}): FileContents { const options = normalizeOptions(_options, null, 'r', 0o644); const flag = parseFlag(options.flag); if (!isReadable(flag)) { throw new ErrnoError(Errno.EINVAL, 'Flag passed to readFile must allow for reading.'); } - const data: Buffer = Buffer.from(_readFileSync(typeof path == 'number' ? fd2file(path).path : path.toString(), options.flag, true)); + const data: Buffer = Buffer.from(_readFileSync.call(this, typeof path == 'number' ? fd2file(path).path : path.toString(), options.flag, true)); return options.encoding ? data.toString(options.encoding) : data; } readFileSync satisfies typeof fs.readFileSync; @@ -218,9 +219,9 @@ readFileSync satisfies typeof fs.readFileSync; * @option mode Defaults to `0644`. * @option flag Defaults to `'w'`. */ -export function writeFileSync(path: fs.PathOrFileDescriptor, data: FileContents, options?: fs.WriteFileOptions): void; -export function writeFileSync(path: fs.PathOrFileDescriptor, data: FileContents, encoding?: BufferEncoding): void; -export function writeFileSync(path: fs.PathOrFileDescriptor, data: FileContents, _options: fs.WriteFileOptions | BufferEncoding = {}): void { +export function writeFileSync(this: V_Context, path: fs.PathOrFileDescriptor, data: FileContents, options?: fs.WriteFileOptions): void; +export function writeFileSync(this: V_Context, path: fs.PathOrFileDescriptor, data: FileContents, encoding?: BufferEncoding): void; +export function writeFileSync(this: V_Context, path: fs.PathOrFileDescriptor, data: FileContents, _options: fs.WriteFileOptions | BufferEncoding = {}): void { const options = normalizeOptions(_options, 'utf8', 'w+', 0o644); const flag = parseFlag(options.flag); if (!isWriteable(flag)) { @@ -233,7 +234,7 @@ export function writeFileSync(path: fs.PathOrFileDescriptor, data: FileContents, if (!encodedData) { throw new ErrnoError(Errno.EINVAL, 'Data not specified'); } - using file = _openSync(typeof path == 'number' ? fd2file(path).path : path.toString(), flag, options.mode, true); + using file = _openSync.call(this, typeof path == 'number' ? fd2file(path).path : path.toString(), flag, options.mode, true); file.writeSync(encodedData, 0, encodedData.byteLength, 0); emitChange('change', path.toString()); } @@ -245,7 +246,7 @@ writeFileSync satisfies typeof fs.writeFileSync; * @option mode Defaults to `0644`. * @option flag Defaults to `'a+'`. */ -export function appendFileSync(filename: fs.PathOrFileDescriptor, data: FileContents, _options: fs.WriteFileOptions = {}): void { +export function appendFileSync(this: V_Context, filename: fs.PathOrFileDescriptor, data: FileContents, _options: fs.WriteFileOptions = {}): void { const options = normalizeOptions(_options, 'utf8', 'a+', 0o644); const flag = parseFlag(options.flag); if (!isAppendable(flag)) { @@ -255,7 +256,7 @@ export function appendFileSync(filename: fs.PathOrFileDescriptor, data: FileCont throw new ErrnoError(Errno.EINVAL, 'Encoding not specified'); } const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding!) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - using file = _openSync(typeof filename == 'number' ? fd2file(filename).path : filename.toString(), flag, options.mode, true); + using file = _openSync.call(this, typeof filename == 'number' ? fd2file(filename).path : filename.toString(), flag, options.mode, true); file.writeSync(encodedData, 0, encodedData.byteLength); } appendFileSync satisfies typeof fs.appendFileSync; @@ -265,21 +266,21 @@ appendFileSync satisfies typeof fs.appendFileSync; * `fstat()` is identical to `stat()`, except that the file to be stat-ed is * specified by the file descriptor `fd`. */ -export function fstatSync(fd: number, options?: { bigint?: boolean }): Stats; -export function fstatSync(fd: number, options: { bigint: true }): BigIntStats; -export function fstatSync(fd: number, options?: fs.StatOptions): Stats | BigIntStats { +export function fstatSync(this: V_Context, fd: number, options?: { bigint?: boolean }): Stats; +export function fstatSync(this: V_Context, fd: number, options: { bigint: true }): BigIntStats; +export function fstatSync(this: V_Context, fd: number, options?: fs.StatOptions): Stats | BigIntStats { const stats: Stats = fd2file(fd).statSync(); return options?.bigint ? new BigIntStats(stats) : stats; } fstatSync satisfies typeof fs.fstatSync; -export function closeSync(fd: number): void { +export function closeSync(this: V_Context, fd: number): void { fd2file(fd).closeSync(); fdMap.delete(fd); } closeSync satisfies typeof fs.closeSync; -export function ftruncateSync(fd: number, len: number | null = 0): void { +export function ftruncateSync(this: V_Context, fd: number, len: number | null = 0): void { len ||= 0; if (len < 0) { throw new ErrnoError(Errno.EINVAL); @@ -288,12 +289,12 @@ export function ftruncateSync(fd: number, len: number | null = 0): void { } ftruncateSync satisfies typeof fs.ftruncateSync; -export function fsyncSync(fd: number): void { +export function fsyncSync(this: V_Context, fd: number): void { fd2file(fd).syncSync(); } fsyncSync satisfies typeof fs.fsyncSync; -export function fdatasyncSync(fd: number): void { +export function fdatasyncSync(this: V_Context, fd: number): void { fd2file(fd).datasyncSync(); } fdatasyncSync satisfies typeof fs.fdatasyncSync; @@ -306,9 +307,9 @@ fdatasyncSync satisfies typeof fs.fdatasyncSync; * @param position Offset from the beginning of the file where this data should be written. * If position is null, the data will be written at the current position. */ -export function writeSync(fd: number, data: ArrayBufferView, offset?: number | null, length?: number | null, position?: number | null): number; -export function writeSync(fd: number, data: string, position?: number | null, encoding?: BufferEncoding | null): number; -export function writeSync(fd: number, data: FileContents, posOrOff?: number | null, lenOrEnc?: BufferEncoding | number | null, pos?: number | null): number { +export function writeSync(this: V_Context, fd: number, data: ArrayBufferView, offset?: number | null, length?: number | null, position?: number | null): number; +export function writeSync(this: V_Context, fd: number, data: string, position?: number | null, encoding?: BufferEncoding | null): number; +export function writeSync(this: V_Context, fd: number, data: FileContents, posOrOff?: number | null, lenOrEnc?: BufferEncoding | number | null, pos?: number | null): number { let buffer: Uint8Array, offset: number | undefined, length: number, position: number | null; if (typeof data === 'string') { // Signature 1: (fd, string, [position?, [encoding?]]) @@ -333,8 +334,8 @@ export function writeSync(fd: number, data: FileContents, posOrOff?: number | nu } writeSync satisfies typeof fs.writeSync; -export function readSync(fd: number, buffer: ArrayBufferView, options?: fs.ReadSyncOptions): number; -export function readSync(fd: number, buffer: ArrayBufferView, offset: number, length: number, position?: fs.ReadPosition | null): number; +export function readSync(this: V_Context, fd: number, buffer: ArrayBufferView, options?: fs.ReadSyncOptions): number; +export function readSync(this: V_Context, fd: number, buffer: ArrayBufferView, offset: number, length: number, position?: fs.ReadPosition | null): number; /** * Read data from the file specified by `fd`. * @param buffer The buffer that the data will be written to. @@ -343,7 +344,7 @@ export function readSync(fd: number, buffer: ArrayBufferView, offset: number, le * @param position An integer specifying where to begin reading from in the file. * If position is null, data will be read from the current file position. */ -export function readSync(fd: number, buffer: ArrayBufferView, options?: fs.ReadSyncOptions | number, length?: number, position?: fs.ReadPosition | null): number { +export function readSync(this: V_Context, fd: number, buffer: ArrayBufferView, options?: fs.ReadSyncOptions | number, length?: number, position?: fs.ReadPosition | null): number { const file = fd2file(fd); const offset = typeof options == 'object' ? options.offset : options; if (typeof options == 'object') { @@ -360,12 +361,12 @@ export function readSync(fd: number, buffer: ArrayBufferView, options?: fs.ReadS } readSync satisfies typeof fs.readSync; -export function fchownSync(fd: number, uid: number, gid: number): void { +export function fchownSync(this: V_Context, fd: number, uid: number, gid: number): void { fd2file(fd).chownSync(uid, gid); } fchownSync satisfies typeof fs.fchownSync; -export function fchmodSync(fd: number, mode: number | string): void { +export function fchmodSync(this: V_Context, fd: number, mode: number | string): void { const numMode = normalizeMode(mode, -1); if (numMode < 0) { throw new ErrnoError(Errno.EINVAL, `Invalid mode.`); @@ -377,14 +378,14 @@ fchmodSync satisfies typeof fs.fchmodSync; /** * Change the file timestamps of a file referenced by the supplied file descriptor. */ -export function futimesSync(fd: number, atime: string | number | Date, mtime: string | number | Date): void { +export function futimesSync(this: V_Context, fd: number, atime: string | number | Date, mtime: string | number | Date): void { fd2file(fd).utimesSync(normalizeTime(atime), normalizeTime(mtime)); } futimesSync satisfies typeof fs.futimesSync; -export function rmdirSync(path: fs.PathLike): void { +export function rmdirSync(this: V_Context, path: fs.PathLike): void { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(realpathSync(path)); + const { fs, path: resolved } = resolveMount(realpathSync(path), this); try { const stats = cache.stats.getSync(path) || fs.statSync(resolved); if (!stats.isDirectory()) { @@ -404,15 +405,15 @@ rmdirSync satisfies typeof fs.rmdirSync; /** * Synchronous `mkdir`. Mode defaults to `o777`. */ -export function mkdirSync(path: fs.PathLike, options: fs.MakeDirectoryOptions & { recursive: true }): string | undefined; -export function mkdirSync(path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false }) | null): void; -export function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined; -export function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined | void { +export function mkdirSync(this: V_Context, path: fs.PathLike, options: fs.MakeDirectoryOptions & { recursive: true }): string | undefined; +export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false }) | null): void; +export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined; +export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): string | undefined | void { options = typeof options === 'object' ? options : { mode: options }; const mode = normalizeMode(options?.mode, 0o777); - path = realpathSync(path); - const { fs, path: resolved } = resolveMount(path); + path = realpathSync.call(this, path); + const { fs, path: resolved } = resolveMount(path, this); const errorPaths: Record = { [resolved]: path }; try { @@ -442,21 +443,31 @@ export function mkdirSync(path: fs.PathLike, options?: fs.Mode | fs.MakeDirector } mkdirSync satisfies typeof fs.mkdirSync; -export function readdirSync(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null): string[]; -export function readdirSync(path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & { withFileTypes?: false }): Buffer[]; -export function readdirSync(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null): string[] | Buffer[]; -export function readdirSync(path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes: true }): Dirent[]; export function readdirSync( + this: V_Context, + path: fs.PathLike, + options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null +): string[]; +export function readdirSync(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & { withFileTypes?: false }): Buffer[]; +export function readdirSync( + this: V_Context, + path: fs.PathLike, + options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null +): string[] | Buffer[]; +export function readdirSync(this: V_Context, path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes: true }): Dirent[]; +export function readdirSync( + this: V_Context, path: fs.PathLike, options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null ): string[] | Dirent[] | Buffer[]; export function readdirSync( + this: V_Context, path: fs.PathLike, options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null ): string[] | Dirent[] | Buffer[] { options = typeof options === 'object' ? options : { encoding: options }; path = normalizePath(path); - const { fs, path: resolved } = resolveMount(realpathSync(path)); + const { fs, path: resolved } = resolveMount(realpathSync(path), this); let entries: string[]; try { const stats = cache.stats.getSync(path) || fs.statSync(resolved); @@ -487,7 +498,7 @@ export function readdirSync( } if (!entryStat.isDirectory() || !options?.recursive) continue; - for (const subEntry of readdirSync(join(path, entry), { ...options, _isIndirect: true })) { + for (const subEntry of readdirSync.call(this, join(path, entry), { ...options, _isIndirect: true })) { if (subEntry instanceof Dirent) { subEntry.path = join(entry, subEntry.path); values.push(subEntry); @@ -506,9 +517,7 @@ export function readdirSync( } readdirSync satisfies typeof fs.readdirSync; -// SYMLINK METHODS - -export function linkSync(targetPath: fs.PathLike, linkPath: fs.PathLike): void { +export function linkSync(this: V_Context, targetPath: fs.PathLike, linkPath: fs.PathLike): void { targetPath = normalizePath(targetPath); if (config.checkAccess && !statSync(dirname(targetPath)).hasAccess(constants.R_OK)) { throw ErrnoError.With('EACCES', dirname(targetPath), 'link'); @@ -518,8 +527,8 @@ export function linkSync(targetPath: fs.PathLike, linkPath: fs.PathLike): void { throw ErrnoError.With('EACCES', dirname(linkPath), 'link'); } - const { fs, path } = resolveMount(targetPath); - const link = resolveMount(linkPath); + const { fs, path } = resolveMount(targetPath, this); + const link = resolveMount(linkPath, this); if (fs != link.fs) { throw ErrnoError.With('EXDEV', linkPath, 'link'); } @@ -540,25 +549,25 @@ linkSync satisfies typeof fs.linkSync; * @param path link path * @param type can be either `'dir'` or `'file'` (default is `'file'`) */ -export function symlinkSync(target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | null = 'file'): void { +export function symlinkSync(this: V_Context, target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | null = 'file'): void { if (!['file', 'dir', 'junction'].includes(type!)) { throw new ErrnoError(Errno.EINVAL, 'Invalid type: ' + type); } - if (existsSync(path)) { + if (existsSync.call(this, path)) { throw ErrnoError.With('EEXIST', path.toString(), 'symlink'); } - writeFileSync(path, target.toString()); - const file = _openSync(path, 'r+', 0o644, false); + writeFileSync.call(this, path, target.toString()); + const file = _openSync.call(this, path, 'r+', 0o644, false); file._setTypeSync(constants.S_IFLNK); } symlinkSync satisfies typeof fs.symlinkSync; -export function readlinkSync(path: fs.PathLike, options?: fs.BufferEncodingOption): Buffer; -export function readlinkSync(path: fs.PathLike, options: fs.EncodingOption | BufferEncoding): string; -export function readlinkSync(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Buffer | string; -export function readlinkSync(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Buffer | string { - const value: Buffer = Buffer.from(_readFileSync(path.toString(), 'r', false)); +export function readlinkSync(this: V_Context, path: fs.PathLike, options?: fs.BufferEncodingOption): Buffer; +export function readlinkSync(this: V_Context, path: fs.PathLike, options: fs.EncodingOption | BufferEncoding): string; +export function readlinkSync(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Buffer | string; +export function readlinkSync(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Buffer | string { + const value: Buffer = Buffer.from(_readFileSync.call(this, path.toString(), 'r', false)); const encoding = typeof options == 'object' ? options?.encoding : options; if (encoding == 'buffer') { return value; @@ -567,31 +576,29 @@ export function readlinkSync(path: fs.PathLike, options?: fs.EncodingOption | Bu } readlinkSync satisfies typeof fs.readlinkSync; -// PROPERTY OPERATIONS - -export function chownSync(path: fs.PathLike, uid: number, gid: number): void { - const fd = openSync(path, 'r+'); +export function chownSync(this: V_Context, path: fs.PathLike, uid: number, gid: number): void { + const fd = openSync.call(this, path, 'r+'); fchownSync(fd, uid, gid); closeSync(fd); } chownSync satisfies typeof fs.chownSync; -export function lchownSync(path: fs.PathLike, uid: number, gid: number): void { - const fd = lopenSync(path, 'r+'); +export function lchownSync(this: V_Context, path: fs.PathLike, uid: number, gid: number): void { + const fd = lopenSync.call(this, path, 'r+'); fchownSync(fd, uid, gid); closeSync(fd); } lchownSync satisfies typeof fs.lchownSync; -export function chmodSync(path: fs.PathLike, mode: fs.Mode): void { - const fd = openSync(path, 'r+'); +export function chmodSync(this: V_Context, path: fs.PathLike, mode: fs.Mode): void { + const fd = openSync.call(this, path, 'r+'); fchmodSync(fd, mode); closeSync(fd); } chmodSync satisfies typeof fs.chmodSync; -export function lchmodSync(path: fs.PathLike, mode: number | string): void { - const fd = lopenSync(path, 'r+'); +export function lchmodSync(this: V_Context, path: fs.PathLike, mode: number | string): void { + const fd = lopenSync.call(this, path, 'r+'); fchmodSync(fd, mode); closeSync(fd); } @@ -600,8 +607,8 @@ lchmodSync satisfies typeof fs.lchmodSync; /** * Change file timestamps of the file referenced by the supplied path. */ -export function utimesSync(path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): void { - const fd = openSync(path, 'r+'); +export function utimesSync(this: V_Context, path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): void { + const fd = openSync.call(this, path, 'r+'); futimesSync(fd, atime, mtime); closeSync(fd); } @@ -610,20 +617,20 @@ utimesSync satisfies typeof fs.utimesSync; /** * Change file timestamps of the file referenced by the supplied path. */ -export function lutimesSync(path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): void { - const fd = lopenSync(path, 'r+'); +export function lutimesSync(this: V_Context, path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): void { + const fd = lopenSync.call(this, path, 'r+'); futimesSync(fd, atime, mtime); closeSync(fd); } lutimesSync satisfies typeof fs.lutimesSync; -export function realpathSync(path: fs.PathLike, options: fs.BufferEncodingOption): Buffer; -export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption): string; -export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs.BufferEncodingOption): string | Buffer { +export function realpathSync(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption): Buffer; +export function realpathSync(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption): string; +export function realpathSync(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption | fs.BufferEncodingOption): string | Buffer { path = normalizePath(path); const { base, dir } = parse(path); - const lpath = join(dir == '/' ? '/' : cache.paths.getSync(dir) || realpathSync(dir), base); - const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath); + const lpath = join(dir == '/' ? '/' : cache.paths.getSync(dir) || realpathSync.call(this, dir), base); + const { fs, path: resolvedPath, mountPoint } = resolveMount(lpath, this); try { const stats = fs.statSync(resolvedPath); @@ -631,8 +638,8 @@ export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs return lpath; } - const target = mountPoint + readlinkSync(lpath, options).toString(); - return cache.paths.getSync(target) || realpathSync(target); + const target = mountPoint + readlinkSync.call(this, lpath, options).toString(); + return cache.paths.getSync(target) || realpathSync.call(this, target); } catch (e) { if ((e as ErrnoError).code == 'ENOENT') { return path; @@ -642,9 +649,9 @@ export function realpathSync(path: fs.PathLike, options?: fs.EncodingOption | fs } realpathSync satisfies Omit; -export function accessSync(path: fs.PathLike, mode: number = 0o600): void { +export function accessSync(this: V_Context, path: fs.PathLike, mode: number = 0o600): void { if (!config.checkAccess) return; - if (!statSync(path).hasAccess(mode)) { + if (!statSync.call, Stats>(this, path).hasAccess(mode)) { throw new ErrnoError(Errno.EACCES); } } @@ -654,7 +661,7 @@ accessSync satisfies typeof fs.accessSync; * Synchronous `rm`. Removes files or directories (recursively). * @param path The path to the file or directory to remove. */ -export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptions): void { +export function rmSync(this: V_Context, path: fs.PathLike, options?: fs.RmOptions & InternalOptions): void { path = normalizePath(path); let stats: Stats | undefined; @@ -673,16 +680,16 @@ export function rmSync(path: fs.PathLike, options?: fs.RmOptions & InternalOptio switch (stats.mode & constants.S_IFMT) { case constants.S_IFDIR: if (options?.recursive) { - for (const entry of readdirSync(path, { _isIndirect: true })) { - rmSync(join(path, entry), { ...options, _isIndirect: true }); + for (const entry of readdirSync.call(this, path, { _isIndirect: true })) { + rmSync.call(this, join(path, entry), { ...options, _isIndirect: true }); } } - rmdirSync(path); + rmdirSync.call(this, path); break; case constants.S_IFREG: case constants.S_IFLNK: - unlinkSync(path); + unlinkSync.call(this, path); break; case constants.S_IFBLK: case constants.S_IFCHR: @@ -705,14 +712,14 @@ rmSync satisfies typeof fs.rmSync; * @param options The encoding (or an object including `encoding`). * @returns The path to the created temporary directory, encoded as a string or buffer. */ -export function mkdtempSync(prefix: string, options: fs.BufferEncodingOption): Buffer; -export function mkdtempSync(prefix: string, options?: fs.EncodingOption): string; -export function mkdtempSync(prefix: string, options?: fs.EncodingOption | fs.BufferEncodingOption): string | Buffer { +export function mkdtempSync(this: V_Context, prefix: string, options: fs.BufferEncodingOption): Buffer; +export function mkdtempSync(this: V_Context, prefix: string, options?: fs.EncodingOption): string; +export function mkdtempSync(this: V_Context, prefix: string, options?: fs.EncodingOption | fs.BufferEncodingOption): string | Buffer { const encoding = typeof options === 'object' ? options?.encoding : options || 'utf8'; const fsName = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`; const resolvedPath = '/tmp/' + fsName; - mkdirSync(resolvedPath); + mkdirSync.call(this, resolvedPath); return encoding == 'buffer' ? Buffer.from(resolvedPath) : resolvedPath; } @@ -723,7 +730,7 @@ mkdtempSync satisfies typeof fs.mkdtempSync; * @param flags Optional flags for the copy operation. Currently supports these flags: * - `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails. */ -export function copyFileSync(source: fs.PathLike, destination: fs.PathLike, flags?: number): void { +export function copyFileSync(this: V_Context, source: fs.PathLike, destination: fs.PathLike, flags?: number): void { source = normalizePath(source); destination = normalizePath(destination); @@ -731,7 +738,7 @@ export function copyFileSync(source: fs.PathLike, destination: fs.PathLike, flag throw new ErrnoError(Errno.EEXIST, 'Destination file already exists.', destination, 'copyFile'); } - writeFileSync(destination, readFileSync(source)); + writeFileSync.call(this, destination, readFileSync(source)); emitChange('rename', destination.toString()); } copyFileSync satisfies typeof fs.copyFileSync; @@ -743,7 +750,7 @@ copyFileSync satisfies typeof fs.copyFileSync; * @param position The position in the file where to begin reading. * @returns The number of bytes read. */ -export function readvSync(fd: number, buffers: readonly NodeJS.ArrayBufferView[], position?: number): number { +export function readvSync(this: V_Context, fd: number, buffers: readonly NodeJS.ArrayBufferView[], position?: number): number { const file = fd2file(fd); let bytesRead = 0; @@ -762,7 +769,7 @@ readvSync satisfies typeof fs.readvSync; * @param position The position in the file where to begin writing. * @returns The number of bytes written. */ -export function writevSync(fd: number, buffers: readonly ArrayBufferView[], position?: number): number { +export function writevSync(this: V_Context, fd: number, buffers: readonly ArrayBufferView[], position?: number): number { const file = fd2file(fd); let bytesWritten = 0; @@ -781,7 +788,7 @@ writevSync satisfies typeof fs.writevSync; * @returns A `Dir` object representing the opened directory. * @todo Handle options */ -export function opendirSync(path: fs.PathLike, options?: fs.OpenDirOptions): Dir { +export function opendirSync(this: V_Context, path: fs.PathLike, options?: fs.OpenDirOptions): Dir { path = normalizePath(path); return new Dir(path); } @@ -799,13 +806,13 @@ opendirSync satisfies typeof fs.opendirSync; * - `preserveTimestamps`: Preserve file timestamps. * - `recursive`: If `true`, copies directories recursively. */ -export function cpSync(source: fs.PathLike, destination: fs.PathLike, opts?: fs.CopySyncOptions): void { +export function cpSync(this: V_Context, source: fs.PathLike, destination: fs.PathLike, opts?: fs.CopySyncOptions): void { source = normalizePath(source); destination = normalizePath(destination); - const srcStats = lstatSync(source); // Use lstat to follow symlinks if not dereferencing + const srcStats = lstatSync.call, Stats>(this, source); // Use lstat to follow symlinks if not dereferencing - if (opts?.errorOnExist && existsSync(destination)) { + if (opts?.errorOnExist && existsSync.call(this, destination)) { throw new ErrnoError(Errno.EEXIST, 'Destination file or directory already exists.', destination, 'cp'); } @@ -814,17 +821,17 @@ export function cpSync(source: fs.PathLike, destination: fs.PathLike, opts?: fs. if (!opts?.recursive) { throw new ErrnoError(Errno.EISDIR, source + ' is a directory (not copied)', source, 'cp'); } - mkdirSync(destination, { recursive: true }); // Ensure the destination directory exists - for (const dirent of readdirSync(source, { withFileTypes: true })) { + mkdirSync.call(this, destination, { recursive: true }); // Ensure the destination directory exists + for (const dirent of readdirSync.call(this, source, { withFileTypes: true })) { if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) { continue; // Skip if the filter returns false } - cpSync(join(source, dirent.name), join(destination, dirent.name), opts); + cpSync.call(this, join(source, dirent.name), join(destination, dirent.name), opts); } break; case constants.S_IFREG: case constants.S_IFLNK: - copyFileSync(source, destination); + copyFileSync.call(this, source, destination); break; case constants.S_IFBLK: case constants.S_IFCHR: @@ -836,7 +843,7 @@ export function cpSync(source: fs.PathLike, destination: fs.PathLike, opts?: fs. // Optionally preserve timestamps if (opts?.preserveTimestamps) { - utimesSync(destination, srcStats.atime, srcStats.mtime); + utimesSync.call(this, destination, srcStats.atime, srcStats.mtime); } } cpSync satisfies typeof fs.cpSync; @@ -846,11 +853,11 @@ cpSync satisfies typeof fs.cpSync; * In case of an error, the err.code will be one of Common System Errors. * @param path A path to an existing file or directory on the file system to be queried. */ -export function statfsSync(path: fs.PathLike, options?: fs.StatFsOptions & { bigint?: false }): fs.StatsFs; -export function statfsSync(path: fs.PathLike, options: fs.StatFsOptions & { bigint: true }): fs.BigIntStatsFs; -export function statfsSync(path: fs.PathLike, options?: fs.StatFsOptions): fs.StatsFs | fs.BigIntStatsFs; -export function statfsSync(path: fs.PathLike, options?: fs.StatFsOptions): fs.StatsFs | fs.BigIntStatsFs { +export function statfsSync(this: V_Context, path: fs.PathLike, options?: fs.StatFsOptions & { bigint?: false }): fs.StatsFs; +export function statfsSync(this: V_Context, path: fs.PathLike, options: fs.StatFsOptions & { bigint: true }): fs.BigIntStatsFs; +export function statfsSync(this: V_Context, path: fs.PathLike, options?: fs.StatFsOptions): fs.StatsFs | fs.BigIntStatsFs; +export function statfsSync(this: V_Context, path: fs.PathLike, options?: fs.StatFsOptions): fs.StatsFs | fs.BigIntStatsFs { path = normalizePath(path); - const { fs } = resolveMount(path); + const { fs } = resolveMount(path, this); return _statfs(fs, options?.bigint); } diff --git a/tests/assignment.ts b/tests/assignment.ts index aaed81fd..92af6136 100644 --- a/tests/assignment.ts +++ b/tests/assignment.ts @@ -7,7 +7,7 @@ Notes on omissions and exclusions: - __promisify__ is omitted as it is metadata - native is omitted as zenfs isn't native - - ReadStream and WriteStream are excluded since they are polfilled from another module + - ReadStream and WriteStream are excluded since they are polyfilled from another module */ import { fs as zen } from '../src/index.js'; From c4dd1598dbb47f7859b8dba23513a035b921b1ab Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 13:54:33 -0600 Subject: [PATCH 06/13] Added context support to promises and tests Removed `BoundContext.call` --- src/context.ts | 21 +--- src/emulation/dir.ts | 10 +- src/emulation/promises.ts | 226 ++++++++++++++++++++------------------ src/emulation/shared.ts | 5 +- src/emulation/sync.ts | 10 +- tests/common.ts | 7 +- tests/fs/dir.test.ts | 4 +- tests/fs/watch.test.ts | 4 +- tests/setup/context.ts | 6 + 9 files changed, 154 insertions(+), 139 deletions(-) create mode 100644 tests/setup/context.ts diff --git a/src/context.ts b/src/context.ts index f05dc469..dffd8ada 100644 --- a/src/context.ts +++ b/src/context.ts @@ -1,15 +1,12 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -import { getByString, type ExtractProperties } from 'utilium'; +import { type ExtractProperties } from 'utilium'; +import { credentials as defaultCredentials, type Credentials } from './credentials.js'; import * as fs from './emulation/index.js'; import type { AbsolutePath } from './emulation/path.js'; -import { credentials as defaultCredentials, type Credentials } from './credentials.js'; type Fn_FS = Omit any>, 'mountObject'>; type Fn_Promises = ExtractProperties any>; -type FnName = keyof Fn_FS | `promises.${keyof Fn_Promises}`; -type Fn = T extends `promises.${infer U extends keyof Fn_Promises}` ? (typeof fs.promises)[U] : T extends keyof Fn_FS ? (typeof fs)[T] : never; - /** * Binds a this value for all of the functions in an object (not recursive) * @internal @@ -30,8 +27,6 @@ export type V_Context = Partial | void | Record; * @experimental */ export interface BoundContext extends Fn_FS, FSContext { - call(method: K, ...args: Parameters>): ReturnType>; - promises: Fn_Promises; } @@ -43,15 +38,9 @@ export function bindContext(root: AbsolutePath, credentials: Credentials = defau const ctx = { root, credentials, - call(method: K, ...args: Parameters>): ReturnType> { - // @ts-expect-error 2349 - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - const value = getByString(fs, method)(...args); - - // eslint-disable-next-line @typescript-eslint/no-unsafe-return - return value; - }, - } satisfies FSContext & { call: any }; + } satisfies FSContext; + + fs.mkdirSync(root, { recursive: true }); const fn_fs = _bindFunctions(fs, ctx); const fn_promises = _bindFunctions(fs.promises, ctx); diff --git a/src/emulation/dir.ts b/src/emulation/dir.ts index 52eeac69..ac2ad4e2 100644 --- a/src/emulation/dir.ts +++ b/src/emulation/dir.ts @@ -5,6 +5,7 @@ import type { Callback } from '../utils.js'; import { basename } from './path.js'; import { readdir } from './promises.js'; import { readdirSync } from './sync.js'; +import type { V_Context } from '../context.js'; export class Dirent implements _Dirent { public get name(): string { @@ -57,7 +58,10 @@ export class Dir implements _Dir { protected _entries?: Dirent[]; - public constructor(public readonly path: string) {} + public constructor( + public readonly path: string, + protected readonly context: V_Context + ) {} /** * Asynchronously close the directory's underlying resource handle. @@ -83,7 +87,7 @@ export class Dir implements _Dir { protected async _read(): Promise { this.checkClosed(); - this._entries ??= await readdir(this.path, { withFileTypes: true }); + this._entries ??= await readdir.call>(this.context, this.path, { withFileTypes: true }); if (!this._entries.length) { return null; } @@ -112,7 +116,7 @@ export class Dir implements _Dir { */ public readSync(): Dirent | null { this.checkClosed(); - this._entries ??= readdirSync(this.path, { withFileTypes: true }); + this._entries ??= readdirSync.call(this.context, this.path, { withFileTypes: true }); if (!this._entries.length) { return null; } diff --git a/src/emulation/promises.ts b/src/emulation/promises.ts index 29934560..c78fc7b3 100644 --- a/src/emulation/promises.ts +++ b/src/emulation/promises.ts @@ -20,6 +20,7 @@ import { dirname, join, parse, resolve } from './path.js'; import { _statfs, fd2file, fdMap, file2fd, fixError, resolveMount, type InternalOptions, type ReaddirOptions } from './shared.js'; import { ReadStream, WriteStream } from './streams.js'; import { FSWatcher, emitChange } from './watchers.js'; +import type { V_Context } from '../context.js'; export * as constants from './constants.js'; export class FileHandle implements promises.FileHandle { @@ -383,12 +384,12 @@ export class FileHandle implements promises.FileHandle { } } -export async function rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promise { +export async function rename(this: V_Context, oldPath: fs.PathLike, newPath: fs.PathLike): Promise { oldPath = normalizePath(oldPath); newPath = normalizePath(newPath); - const src = resolveMount(oldPath); - const dst = resolveMount(newPath); - if (config.checkAccess && !(await stat(dirname(oldPath))).hasAccess(constants.W_OK)) { + const src = resolveMount(oldPath, this); + const dst = resolveMount(newPath, this); + if (config.checkAccess && !(await stat.call(this, dirname(oldPath))).hasAccess(constants.W_OK)) { throw ErrnoError.With('EACCES', oldPath, 'rename'); } try { @@ -398,8 +399,8 @@ export async function rename(oldPath: fs.PathLike, newPath: fs.PathLike): Promis emitChange('change', newPath.toString()); return; } - await writeFile(newPath, await readFile(oldPath)); - await unlink(oldPath); + await writeFile.call(this, newPath, await readFile(oldPath)); + await unlink.call(this, oldPath); emitChange('rename', oldPath.toString()); } catch (e) { throw fixError(e as ErrnoError, { [src.path]: oldPath, [dst.path]: newPath }); @@ -410,9 +411,9 @@ rename satisfies typeof promises.rename; /** * Test whether or not `path` exists by checking with the file system. */ -export async function exists(path: fs.PathLike): Promise { +export async function exists(this: V_Context, path: fs.PathLike): Promise { try { - const { fs, path: resolved } = resolveMount(await realpath(path)); + const { fs, path: resolved } = resolveMount(await realpath(path), this); return await fs.exists(resolved); } catch (e) { if (e instanceof ErrnoError && e.code == 'ENOENT') { @@ -423,12 +424,12 @@ export async function exists(path: fs.PathLike): Promise { } } -export async function stat(path: fs.PathLike, options: fs.BigIntOptions): Promise; -export async function stat(path: fs.PathLike, options?: { bigint?: false }): Promise; -export async function stat(path: fs.PathLike, options?: fs.StatOptions): Promise; -export async function stat(path: fs.PathLike, options?: fs.StatOptions): Promise { +export async function stat(this: V_Context, path: fs.PathLike, options: fs.BigIntOptions): Promise; +export async function stat(this: V_Context, path: fs.PathLike, options?: { bigint?: false }): Promise; +export async function stat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Promise; +export async function stat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Promise { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(await realpath(path)); + const { fs, path: resolved } = resolveMount(await realpath(path), this); try { const stats = await fs.stat(resolved); if (config.checkAccess && !stats.hasAccess(constants.R_OK)) { @@ -446,11 +447,11 @@ stat satisfies typeof promises.stat; * `lstat()` is identical to `stat()`, except that if path is a symbolic link, * then the link itself is stat-ed, not the file that it refers to. */ -export async function lstat(path: fs.PathLike, options?: { bigint?: boolean }): Promise; -export async function lstat(path: fs.PathLike, options: { bigint: true }): Promise; -export async function lstat(path: fs.PathLike, options?: fs.StatOptions): Promise { +export async function lstat(this: V_Context, path: fs.PathLike, options?: { bigint?: boolean }): Promise; +export async function lstat(this: V_Context, path: fs.PathLike, options: { bigint: true }): Promise; +export async function lstat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Promise { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(path); + const { fs, path: resolved } = resolveMount(path, this); try { const stats = await fs.stat(resolved); return options?.bigint ? new BigIntStats(stats) : stats; @@ -462,15 +463,15 @@ lstat satisfies typeof promises.lstat; // FILE-ONLY METHODS -export async function truncate(path: fs.PathLike, len: number = 0): Promise { - await using handle = await open(path, 'r+'); +export async function truncate(this: V_Context, path: fs.PathLike, len: number = 0): Promise { + await using handle = await open.call(this, path, 'r+'); await handle.truncate(len); } truncate satisfies typeof promises.truncate; -export async function unlink(path: fs.PathLike): Promise { +export async function unlink(this: V_Context, path: fs.PathLike): Promise { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(path); + const { fs, path: resolved } = resolveMount(path, this); try { if (config.checkAccess && !(await (cache.stats.getAsync(path) || fs.stat(resolved))).hasAccess(constants.W_OK)) { throw ErrnoError.With('EACCES', resolved, 'unlink'); @@ -487,13 +488,13 @@ unlink satisfies typeof promises.unlink; * Opens a file. This helper handles the complexity of file flags. * @internal */ -async function _open(path: fs.PathLike, _flag: fs.OpenMode, _mode: fs.Mode = 0o644, resolveSymlinks: boolean): Promise { +async function _open(this: V_Context, path: fs.PathLike, _flag: fs.OpenMode, _mode: fs.Mode = 0o644, resolveSymlinks: boolean): Promise { path = normalizePath(path); const mode = normalizeMode(_mode, 0o644), flag = parseFlag(_flag); path = resolveSymlinks ? await realpath(path) : path; - const { fs, path: resolved } = resolveMount(path); + const { fs, path: resolved } = resolveMount(path, this); const stats = await fs.stat(resolved).catch(() => null); @@ -541,8 +542,8 @@ async function _open(path: fs.PathLike, _flag: fs.OpenMode, _mode: fs.Mode = 0o6 * @param flag Handles the complexity of the various file modes. See its API for more details. * @param mode Mode to use to open the file. Can be ignored if the filesystem doesn't support permissions. */ -export async function open(path: fs.PathLike, flag: fs.OpenMode = 'r', mode: fs.Mode = 0o644): Promise { - return await _open(path, flag, mode, true); +export async function open(this: V_Context, path: fs.PathLike, flag: fs.OpenMode = 'r', mode: fs.Mode = 0o644): Promise { + return await _open.call(this, path, flag, mode, true); } open satisfies typeof promises.open; @@ -552,18 +553,24 @@ open satisfies typeof promises.open; * @option flag Defaults to `'r'`. * @returns the file data */ -export async function readFile(path: fs.PathLike | promises.FileHandle, options?: { encoding?: null; flag?: fs.OpenMode } | null): Promise; -export async function readFile(path: fs.PathLike | promises.FileHandle, options: { encoding: BufferEncoding; flag?: fs.OpenMode } | BufferEncoding): Promise; +export async function readFile(this: V_Context, path: fs.PathLike | promises.FileHandle, options?: { encoding?: null; flag?: fs.OpenMode } | null): Promise; export async function readFile( + this: V_Context, + path: fs.PathLike | promises.FileHandle, + options: { encoding: BufferEncoding; flag?: fs.OpenMode } | BufferEncoding +): Promise; +export async function readFile( + this: V_Context, path: fs.PathLike | promises.FileHandle, options?: (fs.ObjectEncodingOptions & { flag?: fs.OpenMode }) | BufferEncoding | null ): Promise; export async function readFile( + this: V_Context, path: fs.PathLike | promises.FileHandle, _options?: (fs.ObjectEncodingOptions & { flag?: fs.OpenMode }) | BufferEncoding | null ): Promise { const options = normalizeOptions(_options, null, 'r', 0o644); - await using handle: FileHandle | promises.FileHandle = typeof path == 'object' && 'fd' in path ? path : await open(path as string, options.flag, options.mode); + await using handle: FileHandle | promises.FileHandle = typeof path == 'object' && 'fd' in path ? path : await open.call(this, path as string, options.flag, options.mode); return await handle.readFile(options); } readFile satisfies typeof promises.readFile; @@ -577,12 +584,13 @@ readFile satisfies typeof promises.readFile; * @option flag Defaults to `'w'`. */ export async function writeFile( + this: V_Context, path: fs.PathLike | promises.FileHandle, data: FileContents | Stream | Iterable | AsyncIterable, _options?: (fs.ObjectEncodingOptions & { mode?: fs.Mode; flag?: fs.OpenMode; flush?: boolean }) | BufferEncoding | null ): Promise { const options = normalizeOptions(_options, 'utf8', 'w+', 0o644); - await using handle = path instanceof FileHandle ? path : await open((path as fs.PathLike).toString(), options.flag, options.mode); + await using handle = path instanceof FileHandle ? path : await open.call(this, (path as fs.PathLike).toString(), options.flag, options.mode); const _data = typeof data == 'string' ? data : data; if (typeof _data != 'string' && !(_data instanceof Uint8Array)) { @@ -599,6 +607,7 @@ writeFile satisfies typeof promises.writeFile; * @option flag Defaults to `'a'`. */ export async function appendFile( + this: V_Context, path: fs.PathLike | promises.FileHandle, data: FileContents, _options?: BufferEncoding | (fs.EncodingOption & { mode?: fs.Mode; flag?: fs.OpenMode }) | null @@ -612,7 +621,7 @@ export async function appendFile( throw new ErrnoError(Errno.EINVAL, 'Encoding not specified'); } const encodedData = typeof data == 'string' ? Buffer.from(data, options.encoding!) : new Uint8Array(data.buffer, data.byteOffset, data.byteLength); - await using handle: FileHandle | promises.FileHandle = typeof path == 'object' && 'fd' in path ? path : await open(path as string, options.flag, options.mode); + await using handle: FileHandle | promises.FileHandle = typeof path == 'object' && 'fd' in path ? path : await open.call(this, path as string, options.flag, options.mode); await handle.appendFile(encodedData, options); } @@ -620,9 +629,9 @@ appendFile satisfies typeof promises.appendFile; // DIRECTORY-ONLY METHODS -export async function rmdir(path: fs.PathLike): Promise { - path = await realpath(path); - const { fs, path: resolved } = resolveMount(path); +export async function rmdir(this: V_Context, path: fs.PathLike): Promise { + path = await realpath.call(this, path); + const { fs, path: resolved } = resolveMount(path, this); try { const stats = await (cache.stats.getAsync(path) || fs.stat(resolved)); if (!stats) { @@ -648,15 +657,15 @@ rmdir satisfies typeof promises.rmdir; * @param options Either the file mode, or an object optionally specifying the file mode and whether parent folders * should be created. If a string is passed, it is parsed as an octal integer. If not specified, defaults to `0o777`. */ -export async function mkdir(path: fs.PathLike, options: fs.MakeDirectoryOptions & { recursive: true }): Promise; -export async function mkdir(path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false | undefined }) | null): Promise; -export async function mkdir(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise; -export async function mkdir(path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise { +export async function mkdir(this: V_Context, path: fs.PathLike, options: fs.MakeDirectoryOptions & { recursive: true }): Promise; +export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mode | (fs.MakeDirectoryOptions & { recursive?: false | undefined }) | null): Promise; +export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise; +export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mode | fs.MakeDirectoryOptions | null): Promise { options = typeof options === 'object' ? options : { mode: options }; const mode = normalizeMode(options?.mode, 0o777); - path = await realpath(path); - const { fs, path: resolved } = resolveMount(path); + path = await realpath.call(this, path); + const { fs, path: resolved } = resolveMount(path, this); const errorPaths: Record = { [resolved]: path }; try { @@ -695,29 +704,36 @@ mkdir satisfies typeof promises.mkdir; * @param path A path to a file. If a URL is provided, it must use the `file:` protocol. * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. If not provided, `'utf8'`. */ -export async function readdir(path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null): Promise; -export async function readdir(path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & { withFileTypes?: false }): Promise; export async function readdir( + this: V_Context, + path: fs.PathLike, + options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null +): Promise; +export async function readdir(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption & ReaddirOptions & { withFileTypes?: false }): Promise; +export async function readdir( + this: V_Context, path: fs.PathLike, options?: (fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes?: false }) | BufferEncoding | null ): Promise; -export async function readdir(path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes: true }): Promise; +export async function readdir(this: V_Context, path: fs.PathLike, options: fs.ObjectEncodingOptions & ReaddirOptions & { withFileTypes: true }): Promise; export async function readdir( + this: V_Context, path: fs.PathLike, options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null ): Promise; export async function readdir( + this: V_Context, path: fs.PathLike, options?: (ReaddirOptions & (fs.ObjectEncodingOptions | fs.BufferEncodingOption)) | BufferEncoding | null ): Promise { options = typeof options === 'object' ? options : { encoding: options }; - path = await realpath(path); + path = await realpath.call(this, path); const handleError = (e: ErrnoError) => { throw fixError(e, { [resolved]: path }); }; - const { fs, path: resolved } = resolveMount(path); + const { fs, path: resolved } = resolveMount(path, this); const _stats = cache.stats.getAsync(path) || fs.stat(resolved).catch(handleError); cache.stats.setAsync(path, _stats); @@ -776,12 +792,12 @@ export async function readdir( } readdir satisfies typeof promises.readdir; -export async function link(targetPath: fs.PathLike, linkPath: fs.PathLike): Promise { +export async function link(this: V_Context, targetPath: fs.PathLike, linkPath: fs.PathLike): Promise { targetPath = normalizePath(targetPath); linkPath = normalizePath(linkPath); - const { fs, path } = resolveMount(targetPath); - const link = resolveMount(linkPath); + const { fs, path } = resolveMount(targetPath, this); + const link = resolveMount(linkPath, this); if (fs != link.fs) { throw ErrnoError.With('EXDEV', linkPath, 'link'); @@ -792,7 +808,7 @@ export async function link(targetPath: fs.PathLike, linkPath: fs.PathLike): Prom throw ErrnoError.With('EACCES', dirname(path), 'link'); } - if (config.checkAccess && !(await stat(dirname(linkPath))).hasAccess(constants.W_OK)) { + if (config.checkAccess && !(await stat.call(this, dirname(linkPath))).hasAccess(constants.W_OK)) { throw ErrnoError.With('EACCES', dirname(linkPath), 'link'); } @@ -812,54 +828,52 @@ link satisfies typeof promises.link; * @param path link path * @param type can be either `'dir'` or `'file'` (default is `'file'`) */ -export async function symlink(target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | string | null = 'file'): Promise { +export async function symlink(this: V_Context, target: fs.PathLike, path: fs.PathLike, type: fs.symlink.Type | string | null = 'file'): Promise { if (!['file', 'dir', 'junction'].includes(type!)) { throw new ErrnoError(Errno.EINVAL, 'Invalid symlink type: ' + type); } - if (await exists(path)) { + if (await exists.call(this, path)) { throw ErrnoError.With('EEXIST', path.toString(), 'symlink'); } - await using handle = await _open(path, 'w+', 0o644, false); + await using handle = await _open.call(this, path, 'w+', 0o644, false); await handle.writeFile(target.toString()); await handle.file.chmod(constants.S_IFLNK); } symlink satisfies typeof promises.symlink; -export async function readlink(path: fs.PathLike, options: fs.BufferEncodingOption): Promise; -export async function readlink(path: fs.PathLike, options?: fs.EncodingOption | null): Promise; -export async function readlink(path: fs.PathLike, options?: fs.BufferEncodingOption | fs.EncodingOption | string | null): Promise; -export async function readlink(path: fs.PathLike, options?: fs.BufferEncodingOption | fs.EncodingOption | string | null): Promise { - await using handle = await _open(normalizePath(path), 'r', 0o644, false); +export async function readlink(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption): Promise; +export async function readlink(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption | null): Promise; +export async function readlink(this: V_Context, path: fs.PathLike, options?: fs.BufferEncodingOption | fs.EncodingOption | string | null): Promise; +export async function readlink(this: V_Context, path: fs.PathLike, options?: fs.BufferEncodingOption | fs.EncodingOption | string | null): Promise { + await using handle = await _open.call(this, normalizePath(path), 'r', 0o644, false); const value = await handle.readFile(); const encoding = typeof options == 'object' ? options?.encoding : options; return encoding == 'buffer' ? value : value.toString(encoding! as BufferEncoding); } readlink satisfies typeof promises.readlink; -// PROPERTY OPERATIONS - -export async function chown(path: fs.PathLike, uid: number, gid: number): Promise { - await using handle = await open(path, 'r+'); +export async function chown(this: V_Context, path: fs.PathLike, uid: number, gid: number): Promise { + await using handle = await open.call(this, path, 'r+'); await handle.chown(uid, gid); } chown satisfies typeof promises.chown; -export async function lchown(path: fs.PathLike, uid: number, gid: number): Promise { - await using handle: FileHandle = await _open(path, 'r+', 0o644, false); +export async function lchown(this: V_Context, path: fs.PathLike, uid: number, gid: number): Promise { + await using handle: FileHandle = await _open.call(this, path, 'r+', 0o644, false); await handle.chown(uid, gid); } lchown satisfies typeof promises.lchown; -export async function chmod(path: fs.PathLike, mode: fs.Mode): Promise { - await using handle = await open(path, 'r+'); +export async function chmod(this: V_Context, path: fs.PathLike, mode: fs.Mode): Promise { + await using handle = await open.call(this, path, 'r+'); await handle.chmod(mode); } chmod satisfies typeof promises.chmod; -export async function lchmod(path: fs.PathLike, mode: fs.Mode): Promise { - await using handle: FileHandle = await _open(path, 'r+', 0o644, false); +export async function lchmod(this: V_Context, path: fs.PathLike, mode: fs.Mode): Promise { + await using handle: FileHandle = await _open.call(this, path, 'r+', 0o644, false); await handle.chmod(mode); } lchmod satisfies typeof promises.lchmod; @@ -867,8 +881,8 @@ lchmod satisfies typeof promises.lchmod; /** * Change file timestamps of the file referenced by the supplied path. */ -export async function utimes(path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): Promise { - await using handle = await open(path, 'r+'); +export async function utimes(this: V_Context, path: fs.PathLike, atime: string | number | Date, mtime: string | number | Date): Promise { + await using handle = await open.call(this, path, 'r+'); await handle.utimes(atime, mtime); } utimes satisfies typeof promises.utimes; @@ -876,8 +890,8 @@ utimes satisfies typeof promises.utimes; /** * Change file timestamps of the file referenced by the supplied path. */ -export async function lutimes(path: fs.PathLike, atime: fs.TimeLike, mtime: fs.TimeLike): Promise { - await using handle: FileHandle = await _open(path, 'r+', 0o644, false); +export async function lutimes(this: V_Context, path: fs.PathLike, atime: fs.TimeLike, mtime: fs.TimeLike): Promise { + await using handle: FileHandle = await _open.call(this, path, 'r+', 0o644, false); await handle.utimes(new Date(atime), new Date(mtime)); } lutimes satisfies typeof promises.lutimes; @@ -888,15 +902,15 @@ lutimes satisfies typeof promises.lutimes; * @param options The encoding (or an object specifying the encoding), used as the encoding of the result. Defaults to `'utf8'`. * @todo handle options */ -export async function realpath(path: fs.PathLike, options: fs.BufferEncodingOption): Promise; -export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding): Promise; -export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Promise { +export async function realpath(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption): Promise; +export async function realpath(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding): Promise; +export async function realpath(this: V_Context, path: fs.PathLike, options?: fs.EncodingOption | BufferEncoding | fs.BufferEncodingOption): Promise { path = normalizePath(path); if (cache.paths.hasAsync(path)) return cache.paths.getAsync(path)!; const { base, dir } = parse(path); - const realDir = dir == '/' ? '/' : await (cache.paths.getAsync(dir) || realpath(dir)); + const realDir = dir == '/' ? '/' : await (cache.paths.getAsync(dir) || realpath.call(this, dir)); const lpath = join(realDir, base); - const { fs, path: resolvedPath } = resolveMount(lpath); + const { fs, path: resolvedPath } = resolveMount(lpath, this); try { const _stats = cache.stats.getAsync(lpath) || fs.stat(resolvedPath); @@ -906,9 +920,9 @@ export async function realpath(path: fs.PathLike, options?: fs.EncodingOption | return lpath; } - const target = resolve(realDir, await readlink(lpath)); + const target = resolve(realDir, (await readlink.call(this, lpath)).toString()); - const real = cache.paths.getAsync(target) || realpath(target); + const real = cache.paths.getAsync(target) || realpath.call(this, target); cache.paths.setAsync(path, real); return await real; } catch (e) { @@ -958,9 +972,9 @@ export function watch(filename: fs.PathLike, options: } watch satisfies typeof promises.watch; -export async function access(path: fs.PathLike, mode: number = constants.F_OK): Promise { +export async function access(this: V_Context, path: fs.PathLike, mode: number = constants.F_OK): Promise { if (!config.checkAccess) return; - const stats = await stat(path); + const stats = await stat.call(this, path); if (!stats.hasAccess(mode)) { throw new ErrnoError(Errno.EACCES); } @@ -971,11 +985,11 @@ access satisfies typeof promises.access; * Asynchronous `rm`. Removes files or directories (recursively). * @param path The path to the file or directory to remove. */ -export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOptions) { +export async function rm(this: V_Context, path: fs.PathLike, options?: fs.RmOptions & InternalOptions) { path = normalizePath(path); const stats = await (cache.stats.getAsync(path) || - stat(path).catch((error: ErrnoError) => { + stat.call>(this, path).catch((error: ErrnoError) => { if (error.code == 'ENOENT' && options?.force) return undefined; throw error; })); @@ -989,18 +1003,18 @@ export async function rm(path: fs.PathLike, options?: fs.RmOptions & InternalOpt switch (stats.mode & constants.S_IFMT) { case constants.S_IFDIR: if (options?.recursive) { - for (const entry of await readdir(path, { _isIndirect: true })) { - await rm(join(path, entry), { ...options, _isIndirect: true }); + for (const entry of await readdir.call>(this, path, { _isIndirect: true })) { + await rm.call(this, join(path, entry), { ...options, _isIndirect: true }); } } - await rmdir(path); + await rmdir.call(this, path); break; case constants.S_IFREG: case constants.S_IFLNK: case constants.S_IFBLK: case constants.S_IFCHR: - await unlink(path); + await unlink.call(this, path); break; case constants.S_IFIFO: case constants.S_IFSOCK: @@ -1021,14 +1035,14 @@ rm satisfies typeof promises.rm; * @param options The encoding (or an object including `encoding`). * @returns The path to the created temporary directory, encoded as a string or buffer. */ -export async function mkdtemp(prefix: string, options?: fs.EncodingOption): Promise; -export async function mkdtemp(prefix: string, options?: fs.BufferEncodingOption): Promise; -export async function mkdtemp(prefix: string, options?: fs.EncodingOption | fs.BufferEncodingOption): Promise { +export async function mkdtemp(this: V_Context, prefix: string, options?: fs.EncodingOption): Promise; +export async function mkdtemp(this: V_Context, prefix: string, options?: fs.BufferEncodingOption): Promise; +export async function mkdtemp(this: V_Context, prefix: string, options?: fs.EncodingOption | fs.BufferEncodingOption): Promise { const encoding = typeof options === 'object' ? options?.encoding : options || 'utf8'; const fsName = `${prefix}${Date.now()}-${Math.random().toString(36).slice(2)}`; const resolvedPath = '/tmp/' + fsName; - await mkdir(resolvedPath); + await mkdir.call(this, resolvedPath); return encoding == 'buffer' ? Buffer.from(resolvedPath) : resolvedPath; } @@ -1041,15 +1055,15 @@ mkdtemp satisfies typeof promises.mkdtemp; * @param mode Optional flags for the copy operation. Currently supports these flags: * * `fs.constants.COPYFILE_EXCL`: If the destination file already exists, the operation fails. */ -export async function copyFile(src: fs.PathLike, dest: fs.PathLike, mode?: number): Promise { +export async function copyFile(this: V_Context, src: fs.PathLike, dest: fs.PathLike, mode?: number): Promise { src = normalizePath(src); dest = normalizePath(dest); - if (mode && mode & constants.COPYFILE_EXCL && (await exists(dest))) { + if (mode && mode & constants.COPYFILE_EXCL && (await exists.call(this, dest))) { throw new ErrnoError(Errno.EEXIST, 'Destination file already exists.', dest, 'copyFile'); } - await writeFile(dest, await readFile(src)); + await writeFile.call(this, dest, await readFile.call(this, src)); emitChange('rename', dest.toString()); } copyFile satisfies typeof promises.copyFile; @@ -1061,9 +1075,9 @@ copyFile satisfies typeof promises.copyFile; * @returns A `Dir` object representing the opened directory. * @todo Use options */ -export function opendir(path: fs.PathLike, options?: fs.OpenDirOptions): Promise { +export function opendir(this: V_Context, path: fs.PathLike, options?: fs.OpenDirOptions): Promise { path = normalizePath(path); - return Promise.resolve(new Dir(path)); + return Promise.resolve(new Dir(path, this)); } opendir satisfies typeof promises.opendir; @@ -1079,13 +1093,13 @@ opendir satisfies typeof promises.opendir; * * `preserveTimestamps`: Preserve file timestamps. * * `recursive`: If `true`, copies directories recursively. */ -export async function cp(source: fs.PathLike, destination: fs.PathLike, opts?: fs.CopyOptions): Promise { +export async function cp(this: V_Context, source: fs.PathLike, destination: fs.PathLike, opts?: fs.CopyOptions): Promise { source = normalizePath(source); destination = normalizePath(destination); - const srcStats = await lstat(source); // Use lstat to follow symlinks if not dereferencing + const srcStats = await lstat.call>(this, source); // Use lstat to follow symlinks if not dereferencing - if (opts?.errorOnExist && (await exists(destination))) { + if (opts?.errorOnExist && (await exists.call(this, destination))) { throw new ErrnoError(Errno.EEXIST, 'Destination file or directory already exists.', destination, 'cp'); } @@ -1095,21 +1109,21 @@ export async function cp(source: fs.PathLike, destination: fs.PathLike, opts?: f throw new ErrnoError(Errno.EISDIR, source + ' is a directory (not copied)', source, 'cp'); } const [entries] = await Promise.all( - [readdir(source, { withFileTypes: true }), mkdir(destination, { recursive: true })] // Ensure the destination directory exists + [readdir.call>(this, source, { withFileTypes: true }), mkdir.call(this, destination, { recursive: true })] // Ensure the destination directory exists ); const _cp = async (dirent: Dirent) => { if (opts.filter && !opts.filter(join(source, dirent.name), join(destination, dirent.name))) { return; // Skip if the filter returns false } - await cp(join(source, dirent.name), join(destination, dirent.name), opts); + await cp.call(this, join(source, dirent.name), join(destination, dirent.name), opts); }; await Promise.all(entries.map(_cp)); break; } case constants.S_IFREG: case constants.S_IFLNK: - await copyFile(source, destination); + await copyFile.call(this, source, destination); break; case constants.S_IFBLK: case constants.S_IFCHR: @@ -1121,7 +1135,7 @@ export async function cp(source: fs.PathLike, destination: fs.PathLike, opts?: f // Optionally preserve timestamps if (opts?.preserveTimestamps) { - await utimes(destination, srcStats.atime, srcStats.mtime); + await utimes.call(this, destination, srcStats.atime, srcStats.mtime); } } cp satisfies typeof promises.cp; @@ -1130,11 +1144,11 @@ cp satisfies typeof promises.cp; * @since Node v18.15.0 * @returns Fulfills with an {fs.StatFs} for the file system. */ -export async function statfs(path: fs.PathLike, opts?: fs.StatFsOptions & { bigint?: false }): Promise; -export async function statfs(path: fs.PathLike, opts: fs.StatFsOptions & { bigint: true }): Promise; -export async function statfs(path: fs.PathLike, opts?: fs.StatFsOptions): Promise; -export function statfs(path: fs.PathLike, opts?: fs.StatFsOptions): Promise { +export async function statfs(this: V_Context, path: fs.PathLike, opts?: fs.StatFsOptions & { bigint?: false }): Promise; +export async function statfs(this: V_Context, path: fs.PathLike, opts: fs.StatFsOptions & { bigint: true }): Promise; +export async function statfs(this: V_Context, path: fs.PathLike, opts?: fs.StatFsOptions): Promise; +export async function statfs(this: V_Context, path: fs.PathLike, opts?: fs.StatFsOptions): Promise { path = normalizePath(path); - const { fs } = resolveMount(path); + const { fs } = resolveMount(path, this); return Promise.resolve(_statfs(fs, opts?.bigint)); } diff --git a/src/emulation/shared.ts b/src/emulation/shared.ts index 6775a4b3..b33b068b 100644 --- a/src/emulation/shared.ts +++ b/src/emulation/shared.ts @@ -6,7 +6,7 @@ import { Errno, ErrnoError } from '../error.js'; import type { File } from '../file.js'; import type { FileSystem } from '../filesystem.js'; import { normalizePath } from '../utils.js'; -import { resolve, type AbsolutePath } from './path.js'; +import { join, resolve, type AbsolutePath } from './path.js'; import { size_max } from './constants.js'; import type { V_Context } from '../context.js'; import { paths as pathCache } from './cache.js'; @@ -72,7 +72,7 @@ export function umount(mountPoint: string): void { */ export function resolveMount(path: string, ctx: V_Context): { fs: FileSystem; path: string; mountPoint: string } { const root = typeof ctx == 'object' && typeof ctx.root == 'string' ? ctx.root : '/'; - path = normalizePath(root + path); + path = normalizePath(join(root, path)); const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length for (const [mountPoint, fs] of sortedMounts) { // We know path is normalized, so it would be a substring of the mount point. @@ -122,6 +122,7 @@ export function fixError(e: E, paths: Record { +const setup = await import(setupPath).catch(error => { console.log('Failed to import test setup:'); throw error; }); -export { fs }; +export const fs = (setup.fs || defaultFS) as typeof defaultFS; diff --git a/tests/fs/dir.test.ts b/tests/fs/dir.test.ts index ebe3b037..62ddb6f7 100644 --- a/tests/fs/dir.test.ts +++ b/tests/fs/dir.test.ts @@ -1,6 +1,6 @@ import assert, { rejects } from 'node:assert'; import { suite, test } from 'node:test'; -import { fs } from '../common.js'; +import { fs, type Dirent } from '../common.js'; const testFile = 'test-file.txt'; fs.writeFileSync(testFile, 'Sample content'); @@ -106,7 +106,7 @@ suite('Dir', () => { test('asynchronous iteration', async () => { const dir = new fs.Dir(testDirPath); - const dirents: fs.Dirent[] = []; + const dirents: Dirent[] = []; for await (const dirent of dir) { dirents.push(dirent); diff --git a/tests/fs/watch.test.ts b/tests/fs/watch.test.ts index f00f3965..5a3fa3e3 100644 --- a/tests/fs/watch.test.ts +++ b/tests/fs/watch.test.ts @@ -1,6 +1,6 @@ import assert from 'node:assert'; import { suite, test } from 'node:test'; -import { fs } from '../common.js'; +import { fs, type Stats } from '../common.js'; const testDir = '/test-watch-dir'; const testFile = `${testDir}/test.txt`; @@ -33,7 +33,7 @@ suite('Watch Features', () => { }); test('fs.watchFile should detect changes to a file', async () => { - const listener = (curr: fs.Stats, prev: fs.Stats) => { + const listener = (curr: Stats, prev: Stats) => { assert(curr.mtimeMs != prev.mtimeMs); fs.unwatchFile(testFile, listener); }; diff --git a/tests/setup/context.ts b/tests/setup/context.ts new file mode 100644 index 00000000..611c7dd5 --- /dev/null +++ b/tests/setup/context.ts @@ -0,0 +1,6 @@ +import { bindContext } from '../../dist/context.js'; +import { copy, data } from './common.js'; + +copy(data); + +export const fs = bindContext('/new_root'); From 2851644ce2db908885cdd81425d51db76987858d Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 14:53:50 -0600 Subject: [PATCH 07/13] Fixed context test copying, updated `resolveMount` to include root --- src/emulation/promises.ts | 12 ++++++------ src/emulation/shared.ts | 13 ++++++++++--- src/emulation/sync.ts | 8 ++++---- tests/setup/common.ts | 7 ++++--- tests/setup/context.ts | 4 ++-- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/src/emulation/promises.ts b/src/emulation/promises.ts index c78fc7b3..73032620 100644 --- a/src/emulation/promises.ts +++ b/src/emulation/promises.ts @@ -413,7 +413,7 @@ rename satisfies typeof promises.rename; */ export async function exists(this: V_Context, path: fs.PathLike): Promise { try { - const { fs, path: resolved } = resolveMount(await realpath(path), this); + const { fs, path: resolved } = resolveMount(await realpath.call(this, path), this); return await fs.exists(resolved); } catch (e) { if (e instanceof ErrnoError && e.code == 'ENOENT') { @@ -429,7 +429,7 @@ export async function stat(this: V_Context, path: fs.PathLike, options?: { bigin export async function stat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Promise; export async function stat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions): Promise { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(await realpath(path), this); + const { fs, path: resolved } = resolveMount(await realpath.call(this, path), this); try { const stats = await fs.stat(resolved); if (config.checkAccess && !stats.hasAccess(constants.R_OK)) { @@ -493,7 +493,7 @@ async function _open(this: V_Context, path: fs.PathLike, _flag: fs.OpenMode, _mo const mode = normalizeMode(_mode, 0o644), flag = parseFlag(_flag); - path = resolveSymlinks ? await realpath(path) : path; + path = resolveSymlinks ? await realpath.call(this, path) : path; const { fs, path: resolved } = resolveMount(path, this); const stats = await fs.stat(resolved).catch(() => null); @@ -665,7 +665,7 @@ export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mod const mode = normalizeMode(options?.mode, 0o777); path = await realpath.call(this, path); - const { fs, path: resolved } = resolveMount(path, this); + const { fs, path: resolved, root } = resolveMount(path, this); const errorPaths: Record = { [resolved]: path }; try { @@ -690,7 +690,7 @@ export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mod await fs.mkdir(dir, mode); emitChange('rename', dir); } - return dirs[0]; + return root + dirs[0]; } catch (e) { throw fixError(e as ErrnoError, errorPaths); } @@ -771,7 +771,7 @@ export async function readdir( if (!options?.recursive || !entryStats?.isDirectory()) return; - for (const subEntry of await readdir(join(path, entry), { ...options, _isIndirect: true })) { + for (const subEntry of await readdir.call(this, join(path, entry), { ...options, _isIndirect: true })) { if (subEntry instanceof Dirent) { subEntry.path = join(entry, subEntry.path); values.push(subEntry); diff --git a/src/emulation/shared.ts b/src/emulation/shared.ts index b33b068b..8b7a77e3 100644 --- a/src/emulation/shared.ts +++ b/src/emulation/shared.ts @@ -67,10 +67,17 @@ export function umount(mountPoint: string): void { pathCache.clear(); } +export interface ResolvedMount { + fs: FileSystem; + path: string; + mountPoint: string; + root: string; +} + /** * Gets the internal `FileSystem` for the path, then returns it along with the path relative to the FS' root */ -export function resolveMount(path: string, ctx: V_Context): { fs: FileSystem; path: string; mountPoint: string } { +export function resolveMount(path: string, ctx: V_Context): ResolvedMount { const root = typeof ctx == 'object' && typeof ctx.root == 'string' ? ctx.root : '/'; path = normalizePath(join(root, path)); const sortedMounts = [...mounts].sort((a, b) => (a[0].length > b[0].length ? -1 : 1)); // descending order of the string length @@ -81,11 +88,11 @@ export function resolveMount(path: string, ctx: V_Context): { fs: FileSystem; pa if (path === '') { path = root; } - return { fs, path, mountPoint }; + return { fs, path, mountPoint, root }; } } - throw new ErrnoError(Errno.EIO, 'ZenFS not initialized with a file system'); + throw new ErrnoError(Errno.EIO, 'No file system'); } /** diff --git a/src/emulation/sync.ts b/src/emulation/sync.ts index ac66734c..d732388b 100644 --- a/src/emulation/sync.ts +++ b/src/emulation/sync.ts @@ -386,7 +386,7 @@ futimesSync satisfies typeof fs.futimesSync; export function rmdirSync(this: V_Context, path: fs.PathLike): void { path = normalizePath(path); - const { fs, path: resolved } = resolveMount(realpathSync(path), this); + const { fs, path: resolved } = resolveMount(realpathSync.call(this, path), this); try { const stats = cache.stats.get(path) || fs.statSync(resolved); if (!stats.isDirectory()) { @@ -414,7 +414,7 @@ export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode const mode = normalizeMode(options?.mode, 0o777); path = realpathSync.call(this, path); - const { fs, path: resolved } = resolveMount(path, this); + const { fs, path: resolved, root } = resolveMount(path, this); const errorPaths: Record = { [resolved]: path }; try { @@ -437,7 +437,7 @@ export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode fs.mkdirSync(dir, mode); emitChange('rename', dir); } - return dirs[0]; + return root + dirs[0]; } catch (e) { throw fixError(e as ErrnoError, errorPaths); } @@ -468,7 +468,7 @@ export function readdirSync( ): string[] | Dirent[] | Buffer[] { options = typeof options === 'object' ? options : { encoding: options }; path = normalizePath(path); - const { fs, path: resolved } = resolveMount(realpathSync(path), this); + const { fs, path: resolved } = resolveMount(realpathSync.call(this, path), this); let entries: string[]; try { const stats = cache.stats.get(path) || fs.statSync(resolved); diff --git a/tests/setup/common.ts b/tests/setup/common.ts index c663e45c..e48984d4 100644 --- a/tests/setup/common.ts +++ b/tests/setup/common.ts @@ -1,6 +1,7 @@ import { join, relative } from 'node:path'; import { statSync, readFileSync, readdirSync, existsSync, mkdirSync } from 'node:fs'; -import { fs } from '../../dist/index.js'; +import { fs as _fs } from '../../dist/index.js'; +import type { BoundContext } from '../../dist/context.js'; export const data = join(import.meta.dirname, '../data'); @@ -10,7 +11,7 @@ if (!existsSync(tmp)) { mkdirSync(tmp); } -export function copy(_path: string) { +export function copy(_path: string, fs: typeof _fs | BoundContext = _fs) { const path = relative(data, _path) || '/'; const stats = statSync(_path); @@ -23,6 +24,6 @@ export function copy(_path: string) { fs.mkdirSync(path); } for (const file of readdirSync(_path)) { - copy(join(_path, file)); + copy(join(_path, file), fs); } } diff --git a/tests/setup/context.ts b/tests/setup/context.ts index 611c7dd5..40472acb 100644 --- a/tests/setup/context.ts +++ b/tests/setup/context.ts @@ -1,6 +1,6 @@ import { bindContext } from '../../dist/context.js'; import { copy, data } from './common.js'; -copy(data); - export const fs = bindContext('/new_root'); + +copy(data, fs); From 760f8b4fca7476508db5f5c353f9dc1455d3c275 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 15:07:35 -0600 Subject: [PATCH 08/13] Added context to callback functions Fixed `mkdir` return value Fixed `Dir` tests not using `opendir` --- src/emulation/async.ts | 378 +++++++++++++++++++++----------------- src/emulation/dir.ts | 8 +- src/emulation/promises.ts | 2 +- src/emulation/sync.ts | 2 +- tests/fs/dir.test.ts | 18 +- 5 files changed, 218 insertions(+), 190 deletions(-) diff --git a/src/emulation/async.ts b/src/emulation/async.ts index ba406c6d..c66b8230 100644 --- a/src/emulation/async.ts +++ b/src/emulation/async.ts @@ -11,15 +11,16 @@ import * as promises from './promises.js'; import { fd2file } from './shared.js'; import { ReadStream, WriteStream } from './streams.js'; import { FSWatcher, StatWatcher } from './watchers.js'; +import type { V_Context } from '../context.js'; const nop = () => {}; /** * Asynchronous rename. No arguments other than a possible exception are given to the completion callback. */ -export function rename(oldPath: fs.PathLike, newPath: fs.PathLike, cb: Callback = nop): void { - promises - .rename(oldPath, newPath) +export function rename(this: V_Context, oldPath: fs.PathLike, newPath: fs.PathLike, cb: Callback = nop): void { + promises.rename + .call(this, oldPath, newPath) .then(() => cb()) .catch(cb); } @@ -30,22 +31,22 @@ rename satisfies Omit; * Then call the callback argument with either true or false. * @deprecated Use {@link stat} or {@link access} instead. */ -export function exists(path: fs.PathLike, cb: (exists: boolean) => unknown = nop): void { - promises - .exists(path) +export function exists(this: V_Context, path: fs.PathLike, cb: (exists: boolean) => unknown = nop): void { + promises.exists + .call(this, path) .then(cb) .catch(() => cb(false)); } exists satisfies Omit; -export function stat(path: fs.PathLike, callback: Callback<[Stats]>): void; -export function stat(path: fs.PathLike, options: { bigint?: false }, callback: Callback<[Stats]>): void; -export function stat(path: fs.PathLike, options: { bigint: true }, callback: Callback<[BigIntStats]>): void; -export function stat(path: fs.PathLike, options: fs.StatOptions, callback: Callback<[Stats] | [BigIntStats]>): void; -export function stat(path: fs.PathLike, options?: fs.StatOptions | Callback<[Stats]>, callback: Callback<[Stats]> | Callback<[BigIntStats]> = nop): void { +export function stat(this: V_Context, path: fs.PathLike, callback: Callback<[Stats]>): void; +export function stat(this: V_Context, path: fs.PathLike, options: { bigint?: false }, callback: Callback<[Stats]>): void; +export function stat(this: V_Context, path: fs.PathLike, options: { bigint: true }, callback: Callback<[BigIntStats]>): void; +export function stat(this: V_Context, path: fs.PathLike, options: fs.StatOptions, callback: Callback<[Stats] | [BigIntStats]>): void; +export function stat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions | Callback<[Stats]>, callback: Callback<[Stats]> | Callback<[BigIntStats]> = nop): void { callback = typeof options == 'function' ? options : callback; - promises - .stat(path, typeof options != 'function' ? options : {}) + promises.stat + .call(this, path, typeof options != 'function' ? options : {}) .then(stats => (callback as Callback<[Stats] | [BigIntStats]>)(undefined, stats as any)) .catch(callback); } @@ -56,34 +57,34 @@ stat satisfies Omit; * `lstat()` is identical to `stat()`, except that if path is a symbolic link, * then the link itself is stat-ed, not the file that it refers to. */ -export function lstat(path: fs.PathLike, callback: Callback<[Stats]>): void; -export function lstat(path: fs.PathLike, options: fs.StatOptions & { bigint?: false }, callback: Callback<[Stats]>): void; -export function lstat(path: fs.PathLike, options: fs.StatOptions & { bigint: true }, callback: Callback<[BigIntStats]>): void; -export function lstat(path: fs.PathLike, options: fs.StatOptions, callback: Callback<[Stats | BigIntStats]>): void; -export function lstat(path: fs.PathLike, options?: fs.StatOptions | Callback<[Stats]>, callback: Callback<[Stats]> | Callback<[BigIntStats]> = nop): void { +export function lstat(this: V_Context, path: fs.PathLike, callback: Callback<[Stats]>): void; +export function lstat(this: V_Context, path: fs.PathLike, options: fs.StatOptions & { bigint?: false }, callback: Callback<[Stats]>): void; +export function lstat(this: V_Context, path: fs.PathLike, options: fs.StatOptions & { bigint: true }, callback: Callback<[BigIntStats]>): void; +export function lstat(this: V_Context, path: fs.PathLike, options: fs.StatOptions, callback: Callback<[Stats | BigIntStats]>): void; +export function lstat(this: V_Context, path: fs.PathLike, options?: fs.StatOptions | Callback<[Stats]>, callback: Callback<[Stats]> | Callback<[BigIntStats]> = nop): void { callback = typeof options == 'function' ? options : callback; - promises - .lstat(path, typeof options != 'function' ? options : ({} as object)) + promises.lstat + .call>(this, path, typeof options != 'function' ? options : ({} as object)) .then(stats => (callback as Callback<[Stats] | [BigIntStats]>)(undefined, stats)) .catch(callback); } lstat satisfies Omit; -export function truncate(path: fs.PathLike, cb?: Callback): void; -export function truncate(path: fs.PathLike, len: number, cb?: Callback): void; -export function truncate(path: fs.PathLike, cbLen: number | Callback = 0, cb: Callback = nop): void { +export function truncate(this: V_Context, path: fs.PathLike, cb?: Callback): void; +export function truncate(this: V_Context, path: fs.PathLike, len: number, cb?: Callback): void; +export function truncate(this: V_Context, path: fs.PathLike, cbLen: number | Callback = 0, cb: Callback = nop): void { cb = typeof cbLen === 'function' ? cbLen : cb; const len = typeof cbLen === 'number' ? cbLen : 0; - promises - .truncate(path, len) + promises.truncate + .call(this, path, len) .then(() => cb()) .catch(cb); } truncate satisfies Omit; -export function unlink(path: fs.PathLike, cb: Callback = nop): void { - promises - .unlink(path) +export function unlink(this: V_Context, path: fs.PathLike, cb: Callback = nop): void { + promises.unlink + .call(this, path) .then(() => cb()) .catch(cb); } @@ -111,13 +112,13 @@ unlink satisfies Omit; * * @see http://www.manpagez.com/man/2/open/ */ -export function open(path: fs.PathLike, flag: string, cb?: Callback<[number]>): void; -export function open(path: fs.PathLike, flag: string, mode: number | string, cb?: Callback<[number]>): void; -export function open(path: fs.PathLike, flag: string, cbMode?: number | string | Callback<[number]>, cb: Callback<[number]> = nop): void { +export function open(this: V_Context, path: fs.PathLike, flag: string, cb?: Callback<[number]>): void; +export function open(this: V_Context, path: fs.PathLike, flag: string, mode: number | string, cb?: Callback<[number]>): void; +export function open(this: V_Context, path: fs.PathLike, flag: string, cbMode?: number | string | Callback<[number]>, cb: Callback<[number]> = nop): void { const mode = normalizeMode(cbMode, 0o644); cb = typeof cbMode === 'function' ? cbMode : cb; - promises - .open(path, flag, mode) + promises.open + .call(this, path, flag, mode) .then(handle => cb(undefined, handle.fd)) .catch(cb); } @@ -129,14 +130,19 @@ open satisfies Omit; * @option flag Defaults to `'r'`. * @param cb If no encoding is specified, then the raw buffer is returned. */ -export function readFile(filename: fs.PathLike, cb: Callback<[Uint8Array]>): void; -export function readFile(filename: fs.PathLike, options: { flag?: string }, callback?: Callback<[Uint8Array]>): void; -export function readFile(filename: fs.PathLike, options: { encoding: BufferEncoding; flag?: string } | BufferEncoding, cb: Callback<[string]>): void; -export function readFile(filename: fs.PathLike, options?: fs.WriteFileOptions | BufferEncoding | Callback<[Uint8Array]>, cb: Callback<[string]> | Callback<[Uint8Array]> = nop) { +export function readFile(this: V_Context, filename: fs.PathLike, cb: Callback<[Uint8Array]>): void; +export function readFile(this: V_Context, filename: fs.PathLike, options: { flag?: string }, callback?: Callback<[Uint8Array]>): void; +export function readFile(this: V_Context, filename: fs.PathLike, options: { encoding: BufferEncoding; flag?: string } | BufferEncoding, cb: Callback<[string]>): void; +export function readFile( + this: V_Context, + filename: fs.PathLike, + options?: fs.WriteFileOptions | BufferEncoding | Callback<[Uint8Array]>, + cb: Callback<[string]> | Callback<[Uint8Array]> = nop +) { cb = typeof options === 'function' ? options : cb; - promises - .readFile(filename, typeof options === 'function' ? null : options) + promises.readFile + .call(this, filename, typeof options === 'function' ? null : options) .then(data => (cb as Callback<[string | Uint8Array]>)(undefined, data)) .catch(cb); } @@ -152,13 +158,13 @@ readFile satisfies Omit; * @option mode Defaults to `0644`. * @option flag Defaults to `'w'`. */ -export function writeFile(filename: fs.PathLike, data: FileContents, cb?: Callback): void; -export function writeFile(filename: fs.PathLike, data: FileContents, encoding?: BufferEncoding, cb?: Callback): void; -export function writeFile(filename: fs.PathLike, data: FileContents, options?: fs.WriteFileOptions, cb?: Callback): void; -export function writeFile(filename: fs.PathLike, data: FileContents, cbEncOpts?: fs.WriteFileOptions | Callback, cb: Callback = nop): void { +export function writeFile(this: V_Context, filename: fs.PathLike, data: FileContents, cb?: Callback): void; +export function writeFile(this: V_Context, filename: fs.PathLike, data: FileContents, encoding?: BufferEncoding, cb?: Callback): void; +export function writeFile(this: V_Context, filename: fs.PathLike, data: FileContents, options?: fs.WriteFileOptions, cb?: Callback): void; +export function writeFile(this: V_Context, filename: fs.PathLike, data: FileContents, cbEncOpts?: fs.WriteFileOptions | Callback, cb: Callback = nop): void { cb = typeof cbEncOpts === 'function' ? cbEncOpts : cb; - promises - .writeFile(filename, data, typeof cbEncOpts != 'function' ? cbEncOpts : null) + promises.writeFile + .call(this, filename, data, typeof cbEncOpts != 'function' ? cbEncOpts : null) .then(() => cb(undefined)) .catch(cb); } @@ -172,10 +178,11 @@ writeFile satisfies Omit; * @option mode Defaults to `0644`. * @option flag Defaults to `'a'`. */ -export function appendFile(filename: fs.PathLike, data: FileContents, cb?: Callback): void; -export function appendFile(filename: fs.PathLike, data: FileContents, options?: fs.EncodingOption & { mode?: fs.Mode; flag?: fs.OpenMode }, cb?: Callback): void; -export function appendFile(filename: fs.PathLike, data: FileContents, encoding?: BufferEncoding, cb?: Callback): void; +export function appendFile(this: V_Context, filename: fs.PathLike, data: FileContents, cb?: Callback): void; +export function appendFile(this: V_Context, filename: fs.PathLike, data: FileContents, options?: fs.EncodingOption & { mode?: fs.Mode; flag?: fs.OpenMode }, cb?: Callback): void; +export function appendFile(this: V_Context, filename: fs.PathLike, data: FileContents, encoding?: BufferEncoding, cb?: Callback): void; export function appendFile( + this: V_Context, filename: fs.PathLike, data: FileContents, cbEncOpts?: (fs.EncodingOption & { mode?: fs.Mode; flag?: fs.OpenMode }) | Callback, @@ -183,8 +190,8 @@ export function appendFile( ): void { const optionsOrEncoding = typeof cbEncOpts != 'function' ? cbEncOpts : undefined; cb = typeof cbEncOpts === 'function' ? cbEncOpts : cb; - promises - .appendFile(filename, data, optionsOrEncoding) + promises.appendFile + .call(this, filename, data, optionsOrEncoding) .then(() => cb()) .catch(cb); } @@ -194,10 +201,10 @@ appendFile satisfies Omit; * Asynchronous `fstat`. * `fstat()` is identical to `stat()`, except that the file to be stat-ed is specified by the file descriptor `fd`. */ -export function fstat(fd: number, cb: Callback<[Stats]>): void; -export function fstat(fd: number, options: fs.StatOptions & { bigint?: false }, cb: Callback<[Stats]>): void; -export function fstat(fd: number, options: fs.StatOptions & { bigint: true }, cb: Callback<[BigIntStats]>): void; -export function fstat(fd: number, options?: fs.StatOptions | Callback<[Stats]>, cb: Callback<[Stats]> | Callback<[BigIntStats]> = nop): void { +export function fstat(this: V_Context, fd: number, cb: Callback<[Stats]>): void; +export function fstat(this: V_Context, fd: number, options: fs.StatOptions & { bigint?: false }, cb: Callback<[Stats]>): void; +export function fstat(this: V_Context, fd: number, options: fs.StatOptions & { bigint: true }, cb: Callback<[BigIntStats]>): void; +export function fstat(this: V_Context, fd: number, options?: fs.StatOptions | Callback<[Stats]>, cb: Callback<[Stats]> | Callback<[BigIntStats]> = nop): void { cb = typeof options == 'function' ? options : cb; fd2file(fd) @@ -207,7 +214,7 @@ export function fstat(fd: number, options?: fs.StatOptions | Callback<[Stats]>, } fstat satisfies Omit; -export function close(fd: number, cb: Callback = nop): void { +export function close(this: V_Context, fd: number, cb: Callback = nop): void { new promises.FileHandle(fd) .close() .then(() => cb()) @@ -215,9 +222,9 @@ export function close(fd: number, cb: Callback = nop): void { } close satisfies Omit; -export function ftruncate(fd: number, cb?: Callback): void; -export function ftruncate(fd: number, len?: number, cb?: Callback): void; -export function ftruncate(fd: number, lenOrCB?: number | Callback, cb: Callback = nop): void { +export function ftruncate(this: V_Context, fd: number, cb?: Callback): void; +export function ftruncate(this: V_Context, fd: number, len?: number, cb?: Callback): void; +export function ftruncate(this: V_Context, fd: number, lenOrCB?: number | Callback, cb: Callback = nop): void { const length = typeof lenOrCB === 'number' ? lenOrCB : 0; cb = typeof lenOrCB === 'function' ? lenOrCB : cb; const file = fd2file(fd); @@ -230,7 +237,7 @@ export function ftruncate(fd: number, lenOrCB?: number | Callback, cb: Callback } ftruncate satisfies Omit; -export function fsync(fd: number, cb: Callback = nop): void { +export function fsync(this: V_Context, fd: number, cb: Callback = nop): void { fd2file(fd) .sync() .then(() => cb()) @@ -238,7 +245,7 @@ export function fsync(fd: number, cb: Callback = nop): void { } fsync satisfies Omit; -export function fdatasync(fd: number, cb: Callback = nop): void { +export function fdatasync(this: V_Context, fd: number, cb: Callback = nop): void { fd2file(fd) .datasync() .then(() => cb()) @@ -256,12 +263,13 @@ fdatasync satisfies Omit; * If position is null, the data will be written at the current position. * @param cb The number specifies the number of bytes written into the file. */ -export function write(fd: number, buffer: Uint8Array, offset: number, length: number, cb?: Callback<[number, Uint8Array]>): void; -export function write(fd: number, buffer: Uint8Array, offset: number, length: number, position?: number, cb?: Callback<[number, Uint8Array]>): void; -export function write(fd: number, data: FileContents, cb?: Callback<[number, string]>): void; -export function write(fd: number, data: FileContents, position?: number, cb?: Callback<[number, string]>): void; -export function write(fd: number, data: FileContents, position: number | null, encoding: BufferEncoding, cb?: Callback<[number, string]>): void; +export function write(this: V_Context, fd: number, buffer: Uint8Array, offset: number, length: number, cb?: Callback<[number, Uint8Array]>): void; +export function write(this: V_Context, fd: number, buffer: Uint8Array, offset: number, length: number, position?: number, cb?: Callback<[number, Uint8Array]>): void; +export function write(this: V_Context, fd: number, data: FileContents, cb?: Callback<[number, string]>): void; +export function write(this: V_Context, fd: number, data: FileContents, position?: number, cb?: Callback<[number, string]>): void; +export function write(this: V_Context, fd: number, data: FileContents, position: number | null, encoding: BufferEncoding, cb?: Callback<[number, string]>): void; export function write( + this: V_Context, fd: number, data: FileContents, cbPosOff?: number | Callback<[number, string]> | null, @@ -325,7 +333,7 @@ write satisfies Omit; * If position is null, data will be read from the current file position. * @param cb The number is the number of bytes read */ -export function read(fd: number, buffer: Uint8Array, offset: number, length: number, position?: number, cb: Callback<[number, Uint8Array]> = nop): void { +export function read(this: V_Context, fd: number, buffer: Uint8Array, offset: number, length: number, position?: number, cb: Callback<[number, Uint8Array]> = nop): void { new promises.FileHandle(fd) .read(buffer, offset, length, position) .then(({ bytesRead, buffer }) => cb(undefined, bytesRead, buffer)) @@ -333,7 +341,7 @@ export function read(fd: number, buffer: Uint8Array, offset: number, length: num } read satisfies Omit; -export function fchown(fd: number, uid: number, gid: number, cb: Callback = nop): void { +export function fchown(this: V_Context, fd: number, uid: number, gid: number, cb: Callback = nop): void { new promises.FileHandle(fd) .chown(uid, gid) .then(() => cb()) @@ -341,7 +349,7 @@ export function fchown(fd: number, uid: number, gid: number, cb: Callback = nop) } fchown satisfies Omit; -export function fchmod(fd: number, mode: string | number, cb: Callback): void { +export function fchmod(this: V_Context, fd: number, mode: string | number, cb: Callback): void { new promises.FileHandle(fd) .chmod(mode) .then(() => cb()) @@ -352,7 +360,7 @@ fchmod satisfies Omit; /** * Change the file timestamps of a file referenced by the supplied file descriptor. */ -export function futimes(fd: number, atime: number | Date, mtime: number | Date, cb: Callback = nop): void { +export function futimes(this: V_Context, fd: number, atime: number | Date, mtime: number | Date, cb: Callback = nop): void { new promises.FileHandle(fd) .utimes(atime, mtime) .then(() => cb()) @@ -360,9 +368,9 @@ export function futimes(fd: number, atime: number | Date, mtime: number | Date, } futimes satisfies Omit; -export function rmdir(path: fs.PathLike, cb: Callback = nop): void { - promises - .rmdir(path) +export function rmdir(this: V_Context, path: fs.PathLike, cb: Callback = nop): void { + promises.rmdir + .call(this, path) .then(() => cb()) .catch(cb); } @@ -372,9 +380,9 @@ rmdir satisfies Omit; * Asynchronous `mkdir`. * @param mode defaults to `0777` */ -export function mkdir(path: fs.PathLike, mode?: fs.Mode, cb: Callback = nop): void { - promises - .mkdir(path, mode) +export function mkdir(this: V_Context, path: fs.PathLike, mode?: fs.Mode, cb: Callback = nop): void { + promises.mkdir + .call(this, path, mode) .then(() => cb()) .catch(cb); } @@ -385,23 +393,28 @@ mkdir satisfies Omit; * The callback gets two arguments `(err, files)` where `files` is an array of * the names of the files in the directory excluding `'.'` and `'..'`. */ -export function readdir(path: fs.PathLike, cb: Callback<[string[]]>): void; -export function readdir(path: fs.PathLike, options: { withFileTypes?: false }, cb: Callback<[string[]]>): void; -export function readdir(path: fs.PathLike, options: { withFileTypes: true }, cb: Callback<[Dirent[]]>): void; -export function readdir(path: fs.PathLike, _options: { withFileTypes?: boolean } | Callback<[string[]]>, cb: Callback<[string[]]> | Callback<[Dirent[]]> = nop): void { +export function readdir(this: V_Context, path: fs.PathLike, cb: Callback<[string[]]>): void; +export function readdir(this: V_Context, path: fs.PathLike, options: { withFileTypes?: false }, cb: Callback<[string[]]>): void; +export function readdir(this: V_Context, path: fs.PathLike, options: { withFileTypes: true }, cb: Callback<[Dirent[]]>): void; +export function readdir( + this: V_Context, + path: fs.PathLike, + _options: { withFileTypes?: boolean } | Callback<[string[]]>, + cb: Callback<[string[]]> | Callback<[Dirent[]]> = nop +): void { cb = typeof _options == 'function' ? _options : cb; const options = typeof _options != 'function' ? _options : {}; - promises - .readdir(path, options as object) + promises.readdir + .call(this, path, options as object) .then(entries => cb(undefined, entries as any)) .catch(cb); } readdir satisfies Omit; -export function link(existing: fs.PathLike, newpath: fs.PathLike, cb: Callback = nop): void { - promises - .link(existing, newpath) +export function link(this: V_Context, existing: fs.PathLike, newpath: fs.PathLike, cb: Callback = nop): void { + promises.link + .call(this, existing, newpath) .then(() => cb()) .catch(cb); } @@ -413,62 +426,63 @@ link satisfies Omit; * @param path link path * Type defaults to file */ -export function symlink(target: fs.PathLike, path: fs.PathLike, cb?: Callback): void; -export function symlink(target: fs.PathLike, path: fs.PathLike, type?: fs.symlink.Type, cb?: Callback): void; -export function symlink(target: fs.PathLike, path: fs.PathLike, typeOrCB?: fs.symlink.Type | Callback, cb: Callback = nop): void { +export function symlink(this: V_Context, target: fs.PathLike, path: fs.PathLike, cb?: Callback): void; +export function symlink(this: V_Context, target: fs.PathLike, path: fs.PathLike, type?: fs.symlink.Type, cb?: Callback): void; +export function symlink(this: V_Context, target: fs.PathLike, path: fs.PathLike, typeOrCB?: fs.symlink.Type | Callback, cb: Callback = nop): void { const type = typeof typeOrCB === 'string' ? typeOrCB : 'file'; cb = typeof typeOrCB === 'function' ? typeOrCB : cb; - promises - .symlink(target, path, type) + promises.symlink + .call(this, target, path, type) .then(() => cb()) .catch(cb); } symlink satisfies Omit; -export function readlink(path: fs.PathLike, callback: Callback<[string]>): void; -export function readlink(path: fs.PathLike, options: fs.BufferEncodingOption, callback: Callback<[Uint8Array]>): void; -export function readlink(path: fs.PathLike, options: fs.EncodingOption, callback: Callback<[string | Uint8Array]>): void; -export function readlink(path: fs.PathLike, options: fs.EncodingOption, callback: Callback<[string]>): void; +export function readlink(this: V_Context, path: fs.PathLike, callback: Callback<[string]>): void; +export function readlink(this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption, callback: Callback<[Uint8Array]>): void; +export function readlink(this: V_Context, path: fs.PathLike, options: fs.EncodingOption, callback: Callback<[string | Uint8Array]>): void; +export function readlink(this: V_Context, path: fs.PathLike, options: fs.EncodingOption, callback: Callback<[string]>): void; export function readlink( + this: V_Context, path: fs.PathLike, options: fs.BufferEncodingOption | fs.EncodingOption | Callback<[string]>, callback: Callback<[string]> | Callback<[Uint8Array]> = nop ): void { callback = typeof options == 'function' ? options : callback; - promises - .readlink(path) + promises.readlink + .call(this, path) .then(result => (callback as Callback<[string | Uint8Array]>)(undefined, result)) .catch(callback); } readlink satisfies Omit; -export function chown(path: fs.PathLike, uid: number, gid: number, cb: Callback = nop): void { - promises - .chown(path, uid, gid) +export function chown(this: V_Context, path: fs.PathLike, uid: number, gid: number, cb: Callback = nop): void { + promises.chown + .call(this, path, uid, gid) .then(() => cb()) .catch(cb); } chown satisfies Omit; -export function lchown(path: fs.PathLike, uid: number, gid: number, cb: Callback = nop): void { - promises - .lchown(path, uid, gid) +export function lchown(this: V_Context, path: fs.PathLike, uid: number, gid: number, cb: Callback = nop): void { + promises.lchown + .call(this, path, uid, gid) .then(() => cb()) .catch(cb); } lchown satisfies Omit; -export function chmod(path: fs.PathLike, mode: number | string, cb: Callback = nop): void { - promises - .chmod(path, mode) +export function chmod(this: V_Context, path: fs.PathLike, mode: number | string, cb: Callback = nop): void { + promises.chmod + .call(this, path, mode) .then(() => cb()) .catch(cb); } chmod satisfies Omit; -export function lchmod(path: fs.PathLike, mode: number | string, cb: Callback = nop): void { - promises - .lchmod(path, mode) +export function lchmod(this: V_Context, path: fs.PathLike, mode: number | string, cb: Callback = nop): void { + promises.lchmod + .call(this, path, mode) .then(() => cb()) .catch(cb); } @@ -477,9 +491,9 @@ lchmod satisfies Omit; /** * Change file timestamps of the file referenced by the supplied path. */ -export function utimes(path: fs.PathLike, atime: number | Date, mtime: number | Date, cb: Callback = nop): void { - promises - .utimes(path, atime, mtime) +export function utimes(this: V_Context, path: fs.PathLike, atime: number | Date, mtime: number | Date, cb: Callback = nop): void { + promises.utimes + .call(this, path, atime, mtime) .then(() => cb()) .catch(cb); } @@ -488,9 +502,9 @@ utimes satisfies Omit; /** * Change file timestamps of the file referenced by the supplied path. */ -export function lutimes(path: fs.PathLike, atime: number | Date, mtime: number | Date, cb: Callback = nop): void { - promises - .lutimes(path, atime, mtime) +export function lutimes(this: V_Context, path: fs.PathLike, atime: number | Date, mtime: number | Date, cb: Callback = nop): void { + promises.lutimes + .call(this, path, atime, mtime) .then(() => cb()) .catch(cb); } @@ -500,24 +514,24 @@ lutimes satisfies Omit; * Asynchronous `realpath`. The callback gets two arguments * `(err, resolvedPath)`. May use `process.cwd` to resolve relative paths. */ -export function realpath(path: fs.PathLike, cb?: Callback<[string]>): void; -export function realpath(path: fs.PathLike, options: fs.EncodingOption, cb: Callback<[string]>): void; -export function realpath(path: fs.PathLike, arg2?: Callback<[string]> | fs.EncodingOption, cb: Callback<[string]> = nop): void { +export function realpath(this: V_Context, path: fs.PathLike, cb?: Callback<[string]>): void; +export function realpath(this: V_Context, path: fs.PathLike, options: fs.EncodingOption, cb: Callback<[string]>): void; +export function realpath(this: V_Context, path: fs.PathLike, arg2?: Callback<[string]> | fs.EncodingOption, cb: Callback<[string]> = nop): void { cb = typeof arg2 === 'function' ? arg2 : cb; - promises - .realpath(path, typeof arg2 === 'function' ? null : arg2) + promises.realpath + .call(this, path, typeof arg2 === 'function' ? null : arg2) .then(result => cb(undefined, result)) .catch(cb); } realpath satisfies Omit; -export function access(path: fs.PathLike, cb: Callback): void; -export function access(path: fs.PathLike, mode: number, cb: Callback): void; -export function access(path: fs.PathLike, cbMode: number | Callback, cb: Callback = nop): void { +export function access(this: V_Context, path: fs.PathLike, cb: Callback): void; +export function access(this: V_Context, path: fs.PathLike, mode: number, cb: Callback): void; +export function access(this: V_Context, path: fs.PathLike, cbMode: number | Callback, cb: Callback = nop): void { const mode = typeof cbMode === 'number' ? cbMode : R_OK; cb = typeof cbMode === 'function' ? cbMode : cb; - promises - .access(path, mode) + promises.access + .call(this, path, mode) .then(() => cb()) .catch(cb); } @@ -536,9 +550,10 @@ const statWatchers: Map void): void; -export function watchFile(path: fs.PathLike, options: { persistent?: boolean; interval?: number }, listener: (curr: Stats, prev: Stats) => void): void; +export function watchFile(this: V_Context, path: fs.PathLike, listener: (curr: Stats, prev: Stats) => void): void; +export function watchFile(this: V_Context, path: fs.PathLike, options: { persistent?: boolean; interval?: number }, listener: (curr: Stats, prev: Stats) => void): void; export function watchFile( + this: V_Context, path: fs.PathLike, options: { persistent?: boolean; interval?: number } | ((curr: Stats, prev: Stats) => void), listener?: (curr: Stats, prev: Stats) => void @@ -585,7 +600,7 @@ watchFile satisfies Omit; * @param path The path to the file to stop watching. * @param listener Optional listener to remove. */ -export function unwatchFile(path: fs.PathLike, listener: (curr: Stats, prev: Stats) => void = nop): void { +export function unwatchFile(this: V_Context, path: fs.PathLike, listener: (curr: Stats, prev: Stats) => void = nop): void { const normalizedPath = normalizePath(path.toString()); const entry = statWatchers.get(normalizedPath); @@ -605,9 +620,14 @@ export function unwatchFile(path: fs.PathLike, listener: (curr: Stats, prev: Sta } unwatchFile satisfies Omit; -export function watch(path: fs.PathLike, listener?: (event: string, filename: string) => any): FSWatcher; -export function watch(path: fs.PathLike, options: { persistent?: boolean }, listener?: (event: string, filename: string) => any): FSWatcher; -export function watch(path: fs.PathLike, options?: fs.WatchOptions | ((event: string, filename: string) => any), listener?: (event: string, filename: string) => any): FSWatcher { +export function watch(this: V_Context, path: fs.PathLike, listener?: (event: string, filename: string) => any): FSWatcher; +export function watch(this: V_Context, path: fs.PathLike, options: { persistent?: boolean }, listener?: (event: string, filename: string) => any): FSWatcher; +export function watch( + this: V_Context, + path: fs.PathLike, + options?: fs.WatchOptions | ((event: string, filename: string) => any), + listener?: (event: string, filename: string) => any +): FSWatcher { const watcher = new FSWatcher(normalizePath(path), typeof options == 'object' ? options : {}); listener = typeof options == 'function' ? options : listener; watcher.on('change', listener || nop); @@ -652,7 +672,8 @@ interface WriteStreamOptions extends StreamOptions { * @param options Options for the ReadStream and file opening (e.g., `encoding`, `highWaterMark`, `mode`). * @returns A ReadStream object for interacting with the file's contents. */ -export function createReadStream(path: fs.PathLike, options?: BufferEncoding | ReadStreamOptions): ReadStream { +export function createReadStream(this: V_Context, path: fs.PathLike, options?: BufferEncoding | ReadStreamOptions): ReadStream { + const context = this; options = typeof options == 'object' ? options : { encoding: options }; let handle: promises.FileHandle; const stream = new ReadStream({ @@ -660,7 +681,7 @@ export function createReadStream(path: fs.PathLike, options?: BufferEncoding | R encoding: options.encoding || 'utf8', async read(size: number) { try { - handle ||= await promises.open(path, 'r', options?.mode); + handle ||= await promises.open.call(context, path, 'r', options?.mode); const result = await handle.read(new Uint8Array(size), 0, size, handle.file.position); stream.push(!result.bytesRead ? null : result.buffer.slice(0, result.bytesRead)); handle.file.position += result.bytesRead; @@ -692,14 +713,15 @@ createReadStream satisfies Omit; * @param options Options for the WriteStream and file opening (e.g., `encoding`, `highWaterMark`, `mode`). * @returns A WriteStream object for writing to the file. */ -export function createWriteStream(path: fs.PathLike, options?: BufferEncoding | WriteStreamOptions): WriteStream { +export function createWriteStream(this: V_Context, path: fs.PathLike, options?: BufferEncoding | WriteStreamOptions): WriteStream { + const context = this; options = typeof options == 'object' ? options : { encoding: options }; let handle: promises.FileHandle; const stream = new WriteStream({ highWaterMark: options?.highWaterMark, async write(chunk: Uint8Array, encoding: BufferEncoding, callback: (error?: Error) => void) { try { - handle ||= await promises.open(path, 'w', options?.mode || 0o666); + handle ||= await promises.open.call(context, path, 'w', options?.mode || 0o666); await handle.write(chunk, 0, encoding); callback(undefined); } catch (error: any) { @@ -727,12 +749,12 @@ export function createWriteStream(path: fs.PathLike, options?: BufferEncoding | } createWriteStream satisfies Omit; -export function rm(path: fs.PathLike, callback: Callback): void; -export function rm(path: fs.PathLike, options: fs.RmOptions, callback: Callback): void; -export function rm(path: fs.PathLike, options: fs.RmOptions | Callback, callback: Callback = nop): void { +export function rm(this: V_Context, path: fs.PathLike, callback: Callback): void; +export function rm(this: V_Context, path: fs.PathLike, options: fs.RmOptions, callback: Callback): void; +export function rm(this: V_Context, path: fs.PathLike, options: fs.RmOptions | Callback, callback: Callback = nop): void { callback = typeof options === 'function' ? options : callback; - promises - .rm(path, typeof options === 'function' ? undefined : options) + promises.rm + .call(this, path, typeof options === 'function' ? undefined : options) .then(() => callback(undefined)) .catch(callback); } @@ -742,24 +764,29 @@ rm satisfies Omit; * Asynchronously creates a unique temporary directory. * Generates six random characters to be appended behind a required prefix to create a unique temporary directory. */ -export function mkdtemp(prefix: string, callback: Callback<[string]>): void; -export function mkdtemp(prefix: string, options: fs.EncodingOption, callback: Callback<[string]>): void; -export function mkdtemp(prefix: string, options: fs.BufferEncodingOption, callback: Callback<[Buffer]>): void; -export function mkdtemp(prefix: string, options: fs.EncodingOption | fs.BufferEncodingOption | Callback<[string]>, callback: Callback<[Buffer]> | Callback<[string]> = nop): void { +export function mkdtemp(this: V_Context, prefix: string, callback: Callback<[string]>): void; +export function mkdtemp(this: V_Context, prefix: string, options: fs.EncodingOption, callback: Callback<[string]>): void; +export function mkdtemp(this: V_Context, prefix: string, options: fs.BufferEncodingOption, callback: Callback<[Buffer]>): void; +export function mkdtemp( + this: V_Context, + prefix: string, + options: fs.EncodingOption | fs.BufferEncodingOption | Callback<[string]>, + callback: Callback<[Buffer]> | Callback<[string]> = nop +): void { callback = typeof options === 'function' ? options : callback; - promises - .mkdtemp(prefix, typeof options != 'function' ? (options as fs.EncodingOption) : null) + promises.mkdtemp + .call>(this, prefix, typeof options != 'function' ? (options as fs.EncodingOption) : null) .then(result => (callback as Callback<[string | Buffer]>)(undefined, result)) .catch(callback); } mkdtemp satisfies Omit; -export function copyFile(src: fs.PathLike, dest: fs.PathLike, callback: Callback): void; -export function copyFile(src: fs.PathLike, dest: fs.PathLike, flags: number, callback: Callback): void; -export function copyFile(src: fs.PathLike, dest: fs.PathLike, flags: number | Callback, callback: Callback = nop): void { +export function copyFile(this: V_Context, src: fs.PathLike, dest: fs.PathLike, callback: Callback): void; +export function copyFile(this: V_Context, src: fs.PathLike, dest: fs.PathLike, flags: number, callback: Callback): void; +export function copyFile(this: V_Context, src: fs.PathLike, dest: fs.PathLike, flags: number | Callback, callback: Callback = nop): void { callback = typeof flags === 'function' ? flags : callback; - promises - .copyFile(src, dest, typeof flags === 'function' ? undefined : flags) + promises.copyFile + .call(this, src, dest, typeof flags === 'function' ? undefined : flags) .then(() => callback(undefined)) .catch(callback); } @@ -767,9 +794,9 @@ copyFile satisfies Omit; type readvCb = Callback<[number, NodeJS.ArrayBufferView[]]>; -export function readv(fd: number, buffers: NodeJS.ArrayBufferView[], cb: readvCb): void; -export function readv(fd: number, buffers: NodeJS.ArrayBufferView[], position: number, cb: readvCb): void; -export function readv(fd: number, buffers: NodeJS.ArrayBufferView[], position: number | readvCb, cb: readvCb = nop): void { +export function readv(this: V_Context, fd: number, buffers: NodeJS.ArrayBufferView[], cb: readvCb): void; +export function readv(this: V_Context, fd: number, buffers: NodeJS.ArrayBufferView[], position: number, cb: readvCb): void; +export function readv(this: V_Context, fd: number, buffers: NodeJS.ArrayBufferView[], position: number | readvCb, cb: readvCb = nop): void { cb = typeof position === 'function' ? position : cb; new promises.FileHandle(fd) .readv(buffers, typeof position === 'function' ? undefined : position) @@ -780,9 +807,9 @@ readv satisfies Omit; type writevCb = Callback<[number, NodeJS.ArrayBufferView[]]>; -export function writev(fd: number, buffers: Uint8Array[], cb: writevCb): void; -export function writev(fd: number, buffers: Uint8Array[], position: number, cb: writevCb): void; -export function writev(fd: number, buffers: Uint8Array[], position: number | writevCb, cb: writevCb = nop) { +export function writev(this: V_Context, fd: number, buffers: Uint8Array[], cb: writevCb): void; +export function writev(this: V_Context, fd: number, buffers: Uint8Array[], position: number, cb: writevCb): void; +export function writev(this: V_Context, fd: number, buffers: Uint8Array[], position: number | writevCb, cb: writevCb = nop) { cb = typeof position === 'function' ? position : cb; new promises.FileHandle(fd) .writev(buffers, typeof position === 'function' ? undefined : position) @@ -791,42 +818,47 @@ export function writev(fd: number, buffers: Uint8Array[], position: number | wri } writev satisfies Omit; -export function opendir(path: fs.PathLike, cb: Callback<[Dir]>): void; -export function opendir(path: fs.PathLike, options: fs.OpenDirOptions, cb: Callback<[Dir]>): void; -export function opendir(path: fs.PathLike, options: fs.OpenDirOptions | Callback<[Dir]>, cb: Callback<[Dir]> = nop): void { +export function opendir(this: V_Context, path: fs.PathLike, cb: Callback<[Dir]>): void; +export function opendir(this: V_Context, path: fs.PathLike, options: fs.OpenDirOptions, cb: Callback<[Dir]>): void; +export function opendir(this: V_Context, path: fs.PathLike, options: fs.OpenDirOptions | Callback<[Dir]>, cb: Callback<[Dir]> = nop): void { cb = typeof options === 'function' ? options : cb; - promises - .opendir(path, typeof options === 'function' ? undefined : options) + promises.opendir + .call(this, path, typeof options === 'function' ? undefined : options) .then(result => cb(undefined, result)) .catch(cb); } opendir satisfies Omit; -export function cp(source: fs.PathLike, destination: fs.PathLike, callback: Callback): void; -export function cp(source: fs.PathLike, destination: fs.PathLike, opts: fs.CopyOptions, callback: Callback): void; -export function cp(source: fs.PathLike, destination: fs.PathLike, opts: fs.CopyOptions | Callback, callback: Callback = nop): void { +export function cp(this: V_Context, source: fs.PathLike, destination: fs.PathLike, callback: Callback): void; +export function cp(this: V_Context, source: fs.PathLike, destination: fs.PathLike, opts: fs.CopyOptions, callback: Callback): void; +export function cp(this: V_Context, source: fs.PathLike, destination: fs.PathLike, opts: fs.CopyOptions | Callback, callback: Callback = nop): void { callback = typeof opts === 'function' ? opts : callback; - promises - .cp(source, destination, typeof opts === 'function' ? undefined : opts) + promises.cp + .call(this, source, destination, typeof opts === 'function' ? undefined : opts) .then(() => callback(undefined)) .catch(callback); } cp satisfies Omit; -export function statfs(path: fs.PathLike, callback: Callback<[fs.StatsFs]>): void; -export function statfs(path: fs.PathLike, options: fs.StatFsOptions & { bigint?: false }, callback: Callback<[fs.StatsFs]>): void; -export function statfs(path: fs.PathLike, options: fs.StatFsOptions & { bigint: true }, callback: Callback<[fs.BigIntStatsFs]>): void; -export function statfs(path: fs.PathLike, options?: fs.StatFsOptions | Callback<[fs.StatsFs]>, callback: Callback<[fs.StatsFs]> | Callback<[fs.BigIntStatsFs]> = nop): void { +export function statfs(this: V_Context, path: fs.PathLike, callback: Callback<[fs.StatsFs]>): void; +export function statfs(this: V_Context, path: fs.PathLike, options: fs.StatFsOptions & { bigint?: false }, callback: Callback<[fs.StatsFs]>): void; +export function statfs(this: V_Context, path: fs.PathLike, options: fs.StatFsOptions & { bigint: true }, callback: Callback<[fs.BigIntStatsFs]>): void; +export function statfs( + this: V_Context, + path: fs.PathLike, + options?: fs.StatFsOptions | Callback<[fs.StatsFs]>, + callback: Callback<[fs.StatsFs]> | Callback<[fs.BigIntStatsFs]> = nop +): void { callback = typeof options === 'function' ? options : callback; - promises - .statfs(path, typeof options === 'function' ? undefined : options) + promises.statfs + .call(this, path, typeof options === 'function' ? undefined : options) .then(result => (callback as Callback<[fs.StatsFs | fs.BigIntStatsFs]>)(undefined, result)) .catch(callback); } statfs satisfies Omit; -export async function openAsBlob(path: fs.PathLike, options?: fs.OpenAsBlobOptions): Promise { - const handle = await promises.open(path.toString(), 'r'); +export async function openAsBlob(this: V_Context, path: fs.PathLike, options?: fs.OpenAsBlobOptions): Promise { + const handle = await promises.open.call(this, path.toString(), 'r'); const buffer = await handle.readFile(); await handle.close(); return new Blob([buffer], options); diff --git a/src/emulation/dir.ts b/src/emulation/dir.ts index ac2ad4e2..c606914e 100644 --- a/src/emulation/dir.ts +++ b/src/emulation/dir.ts @@ -88,9 +88,7 @@ export class Dir implements _Dir { protected async _read(): Promise { this.checkClosed(); this._entries ??= await readdir.call>(this.context, this.path, { withFileTypes: true }); - if (!this._entries.length) { - return null; - } + if (!this._entries.length) return null; return this._entries.shift() ?? null; } @@ -117,9 +115,7 @@ export class Dir implements _Dir { public readSync(): Dirent | null { this.checkClosed(); this._entries ??= readdirSync.call(this.context, this.path, { withFileTypes: true }); - if (!this._entries.length) { - return null; - } + if (!this._entries.length) return null; return this._entries.shift() ?? null; } diff --git a/src/emulation/promises.ts b/src/emulation/promises.ts index 73032620..d7cc91aa 100644 --- a/src/emulation/promises.ts +++ b/src/emulation/promises.ts @@ -690,7 +690,7 @@ export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mod await fs.mkdir(dir, mode); emitChange('rename', dir); } - return root + dirs[0]; + return dirs[0]?.slice(root.length); } catch (e) { throw fixError(e as ErrnoError, errorPaths); } diff --git a/src/emulation/sync.ts b/src/emulation/sync.ts index d732388b..7f0c8273 100644 --- a/src/emulation/sync.ts +++ b/src/emulation/sync.ts @@ -437,7 +437,7 @@ export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode fs.mkdirSync(dir, mode); emitChange('rename', dir); } - return root + dirs[0]; + return dirs[0]?.slice(root.length); } catch (e) { throw fixError(e as ErrnoError, errorPaths); } diff --git a/tests/fs/dir.test.ts b/tests/fs/dir.test.ts index 62ddb6f7..7cdb4492 100644 --- a/tests/fs/dir.test.ts +++ b/tests/fs/dir.test.ts @@ -59,7 +59,7 @@ suite('Dirent', () => { suite('Dir', () => { test('read()', async () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); const dirent1 = await dir.read(); assert(dirent1 instanceof fs.Dirent); @@ -76,7 +76,7 @@ suite('Dir', () => { }); test('readSync()', () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); const dirent1 = dir.readSync(); assert(dirent1 instanceof fs.Dirent); @@ -93,19 +93,19 @@ suite('Dir', () => { }); test('close()', async () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); await dir.close(); rejects(dir.read(), 'Can not use closed Dir'); }); test('closeSync()', () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); dir.closeSync(); assert.throws(() => dir.readSync(), 'Can not use closed Dir'); }); test('asynchronous iteration', async () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); const dirents: Dirent[] = []; for await (const dirent of dir) { @@ -119,26 +119,26 @@ suite('Dir', () => { }); test('read after directory is closed', async () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); await dir.close(); await assert.rejects(dir.read(), 'Can not use closed Dir'); }); test('readSync after directory is closed', () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); dir.closeSync(); assert.throws(() => dir.readSync(), 'Can not use closed Dir'); }); test('close multiple times', async () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); await dir.close(); await dir.close(); // Should not throw an error assert(dir['closed']); }); test('closeSync multiple times', () => { - const dir = new fs.Dir(testDirPath); + const dir = fs.opendirSync(testDirPath); dir.closeSync(); dir.closeSync(); // Should not throw an error assert(dir['closed']); From b83f4a81c68df734376b1a204fadca284215ff78 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 15:13:15 -0600 Subject: [PATCH 09/13] Fixed `mkdir` return value --- src/emulation/promises.ts | 2 +- src/emulation/sync.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/emulation/promises.ts b/src/emulation/promises.ts index d7cc91aa..5eb3749a 100644 --- a/src/emulation/promises.ts +++ b/src/emulation/promises.ts @@ -690,7 +690,7 @@ export async function mkdir(this: V_Context, path: fs.PathLike, options?: fs.Mod await fs.mkdir(dir, mode); emitChange('rename', dir); } - return dirs[0]?.slice(root.length); + return root.length == 1 ? dirs[0] : dirs[0]?.slice(root.length); } catch (e) { throw fixError(e as ErrnoError, errorPaths); } diff --git a/src/emulation/sync.ts b/src/emulation/sync.ts index 7f0c8273..aa0269e5 100644 --- a/src/emulation/sync.ts +++ b/src/emulation/sync.ts @@ -437,7 +437,7 @@ export function mkdirSync(this: V_Context, path: fs.PathLike, options?: fs.Mode fs.mkdirSync(dir, mode); emitChange('rename', dir); } - return dirs[0]?.slice(root.length); + return root.length == 1 ? dirs[0] : dirs[0]?.slice(root.length); } catch (e) { throw fixError(e as ErrnoError, errorPaths); } From 25f6e5e9d39939ce3c95a8ea8e64fd2c12bb0872 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 15:20:03 -0600 Subject: [PATCH 10/13] Add more context tests --- tests/context.test.ts | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/context.test.ts diff --git a/tests/context.test.ts b/tests/context.test.ts new file mode 100644 index 00000000..552ac89a --- /dev/null +++ b/tests/context.test.ts @@ -0,0 +1,18 @@ +import { suite, test } from 'node:test'; +import assert from 'node:assert'; +import { bindContext } from '../src/context.js'; +import * as fs from '../src/emulation/index.js'; + +const c_fs = bindContext('/new_root'); + +suite('Context', () => { + test('create a file', () => { + c_fs.writeFileSync('/example.txt', 'not in real root!'); + assert.deepEqual(fs.readdirSync('/'), ['new_root']); + assert.deepEqual(fs.readdirSync('/new_root'), ['example.txt']); + }); + + test('break-out fails', () => { + assert.deepEqual(c_fs.readdirSync('/../../'), ['example.txt']); + }); +}); From 50ba60a8f3cd224176a662efe8a95ef7be27c835 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 15:23:32 -0600 Subject: [PATCH 11/13] Added export --- src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.ts b/src/index.ts index f92c27f6..55315ddb 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,6 +9,7 @@ export * from './backends/store/simple.js'; export * from './backends/store/store.js'; export * from './backends/backend.js'; export * from './config.js'; +export * from './context.js'; export * from './credentials.js'; export * from './devices.js'; export { default as devices } from './devices.js'; From 3aade929ba82942300f58d15ab24f30375d36bff Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 18:23:48 -0600 Subject: [PATCH 12/13] Removed auto-creation of directory for `bindContext` --- src/context.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/context.ts b/src/context.ts index dffd8ada..c3477446 100644 --- a/src/context.ts +++ b/src/context.ts @@ -40,8 +40,6 @@ export function bindContext(root: AbsolutePath, credentials: Credentials = defau credentials, } satisfies FSContext; - fs.mkdirSync(root, { recursive: true }); - const fn_fs = _bindFunctions(fs, ctx); const fn_promises = _bindFunctions(fs.promises, ctx); From 444ccfd4df3ddb6ccacafbb99b209e00e6976085 Mon Sep 17 00:00:00 2001 From: James Prevett Date: Sun, 24 Nov 2024 18:26:29 -0600 Subject: [PATCH 13/13] Make directories for context tests --- tests/context.test.ts | 1 + tests/setup/context.ts | 3 +++ 2 files changed, 4 insertions(+) diff --git a/tests/context.test.ts b/tests/context.test.ts index 552ac89a..12924bee 100644 --- a/tests/context.test.ts +++ b/tests/context.test.ts @@ -3,6 +3,7 @@ import assert from 'node:assert'; import { bindContext } from '../src/context.js'; import * as fs from '../src/emulation/index.js'; +fs.mkdirSync('/new_root'); const c_fs = bindContext('/new_root'); suite('Context', () => { diff --git a/tests/setup/context.ts b/tests/setup/context.ts index 40472acb..a8704e38 100644 --- a/tests/setup/context.ts +++ b/tests/setup/context.ts @@ -1,6 +1,9 @@ import { bindContext } from '../../dist/context.js'; +import { fs as _fs } from '../../dist/index.js'; import { copy, data } from './common.js'; +_fs.mkdirSync('/new_root'); + export const fs = bindContext('/new_root'); copy(data, fs);