diff options
Diffstat (limited to 'ext/node/polyfills/_fs')
42 files changed, 3929 insertions, 0 deletions
diff --git a/ext/node/polyfills/_fs/_fs_access.ts b/ext/node/polyfills/_fs/_fs_access.ts new file mode 100644 index 000000000..9450c2f01 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_access.ts @@ -0,0 +1,127 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { + type CallbackWithError, + makeCallback, +} from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { fs } from "internal:deno_node/polyfills/internal_binding/constants.ts"; +import { codeMap } from "internal:deno_node/polyfills/internal_binding/uv.ts"; +import { + getValidatedPath, + getValidMode, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import type { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function access( + path: string | Buffer | URL, + mode: number | CallbackWithError, + callback?: CallbackWithError, +) { + if (typeof mode === "function") { + callback = mode; + mode = fs.F_OK; + } + + path = getValidatedPath(path).toString(); + mode = getValidMode(mode, "access"); + const cb = makeCallback(callback); + + Deno.lstat(path).then((info) => { + if (info.mode === null) { + // If the file mode is unavailable, we pretend it has + // the permission + cb(null); + return; + } + const m = +mode || 0; + let fileMode = +info.mode || 0; + if (Deno.build.os !== "windows" && info.uid === Deno.uid()) { + // If the user is the owner of the file, then use the owner bits of + // the file permission + fileMode >>= 6; + } + // TODO(kt3k): Also check the case when the user belong to the group + // of the file + if ((m & fileMode) === m) { + // all required flags exist + cb(null); + } else { + // some required flags don't + // deno-lint-ignore no-explicit-any + const e: any = new Error(`EACCES: permission denied, access '${path}'`); + e.path = path; + e.syscall = "access"; + e.errno = codeMap.get("EACCES"); + e.code = "EACCES"; + cb(e); + } + }, (err) => { + if (err instanceof Deno.errors.NotFound) { + // deno-lint-ignore no-explicit-any + const e: any = new Error( + `ENOENT: no such file or directory, access '${path}'`, + ); + e.path = path; + e.syscall = "access"; + e.errno = codeMap.get("ENOENT"); + e.code = "ENOENT"; + cb(e); + } else { + cb(err); + } + }); +} + +export const accessPromise = promisify(access) as ( + path: string | Buffer | URL, + mode?: number, +) => Promise<void>; + +export function accessSync(path: string | Buffer | URL, mode?: number) { + path = getValidatedPath(path).toString(); + mode = getValidMode(mode, "access"); + try { + const info = Deno.lstatSync(path.toString()); + if (info.mode === null) { + // If the file mode is unavailable, we pretend it has + // the permission + return; + } + const m = +mode! || 0; + let fileMode = +info.mode! || 0; + if (Deno.build.os !== "windows" && info.uid === Deno.uid()) { + // If the user is the owner of the file, then use the owner bits of + // the file permission + fileMode >>= 6; + } + // TODO(kt3k): Also check the case when the user belong to the group + // of the file + if ((m & fileMode) === m) { + // all required flags exist + } else { + // some required flags don't + // deno-lint-ignore no-explicit-any + const e: any = new Error(`EACCES: permission denied, access '${path}'`); + e.path = path; + e.syscall = "access"; + e.errno = codeMap.get("EACCES"); + e.code = "EACCES"; + throw e; + } + } catch (err) { + if (err instanceof Deno.errors.NotFound) { + // deno-lint-ignore no-explicit-any + const e: any = new Error( + `ENOENT: no such file or directory, access '${path}'`, + ); + e.path = path; + e.syscall = "access"; + e.errno = codeMap.get("ENOENT"); + e.code = "ENOENT"; + throw e; + } else { + throw err; + } + } +} diff --git a/ext/node/polyfills/_fs/_fs_appendFile.ts b/ext/node/polyfills/_fs/_fs_appendFile.ts new file mode 100644 index 000000000..d47afe81b --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_appendFile.ts @@ -0,0 +1,73 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + CallbackWithError, + isFd, + maybeCallback, + WriteFileOptions, +} from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { Encodings } from "internal:deno_node/polyfills/_utils.ts"; +import { + copyObject, + getOptions, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { + writeFile, + writeFileSync, +} from "internal:deno_node/polyfills/_fs/_fs_writeFile.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +/** + * TODO: Also accept 'data' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export function appendFile( + path: string | number | URL, + data: string | Uint8Array, + options: Encodings | WriteFileOptions | CallbackWithError, + callback?: CallbackWithError, +) { + callback = maybeCallback(callback || options); + options = getOptions(options, { encoding: "utf8", mode: 0o666, flag: "a" }); + + // Don't make changes directly on options object + options = copyObject(options); + + // Force append behavior when using a supplied file descriptor + if (!options.flag || isFd(path)) { + options.flag = "a"; + } + + writeFile(path, data, options, callback); +} + +/** + * TODO: Also accept 'data' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export const appendFilePromise = promisify(appendFile) as ( + path: string | number | URL, + data: string | Uint8Array, + options?: Encodings | WriteFileOptions, +) => Promise<void>; + +/** + * TODO: Also accept 'data' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export function appendFileSync( + path: string | number | URL, + data: string | Uint8Array, + options?: Encodings | WriteFileOptions, +) { + options = getOptions(options, { encoding: "utf8", mode: 0o666, flag: "a" }); + + // Don't make changes directly on options object + options = copyObject(options); + + // Force append behavior when using a supplied file descriptor + if (!options.flag || isFd(path)) { + options.flag = "a"; + } + + writeFileSync(path, data, options); +} diff --git a/ext/node/polyfills/_fs/_fs_chmod.ts b/ext/node/polyfills/_fs/_fs_chmod.ts new file mode 100644 index 000000000..3a19e5622 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_chmod.ts @@ -0,0 +1,69 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { getValidatedPath } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import * as pathModule from "internal:deno_node/polyfills/path.ts"; +import { parseFileMode } from "internal:deno_node/polyfills/internal/validators.mjs"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function chmod( + path: string | Buffer | URL, + mode: string | number, + callback: CallbackWithError, +) { + path = getValidatedPath(path).toString(); + + try { + mode = parseFileMode(mode, "mode"); + } catch (error) { + // TODO(PolarETech): Errors should not be ignored when Deno.chmod is supported on Windows. + // https://github.com/denoland/deno_std/issues/2916 + if (Deno.build.os === "windows") { + mode = 0; // set dummy value to avoid type checking error at Deno.chmod + } else { + throw error; + } + } + + Deno.chmod(pathModule.toNamespacedPath(path), mode).catch((error) => { + // Ignore NotSupportedError that occurs on windows + // https://github.com/denoland/deno_std/issues/2995 + if (!(error instanceof Deno.errors.NotSupported)) { + throw error; + } + }).then( + () => callback(null), + callback, + ); +} + +export const chmodPromise = promisify(chmod) as ( + path: string | Buffer | URL, + mode: string | number, +) => Promise<void>; + +export function chmodSync(path: string | URL, mode: string | number) { + path = getValidatedPath(path).toString(); + + try { + mode = parseFileMode(mode, "mode"); + } catch (error) { + // TODO(PolarETech): Errors should not be ignored when Deno.chmodSync is supported on Windows. + // https://github.com/denoland/deno_std/issues/2916 + if (Deno.build.os === "windows") { + mode = 0; // set dummy value to avoid type checking error at Deno.chmodSync + } else { + throw error; + } + } + + try { + Deno.chmodSync(pathModule.toNamespacedPath(path), mode); + } catch (error) { + // Ignore NotSupportedError that occurs on windows + // https://github.com/denoland/deno_std/issues/2995 + if (!(error instanceof Deno.errors.NotSupported)) { + throw error; + } + } +} diff --git a/ext/node/polyfills/_fs/_fs_chown.ts b/ext/node/polyfills/_fs/_fs_chown.ts new file mode 100644 index 000000000..55a469fba --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_chown.ts @@ -0,0 +1,56 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + type CallbackWithError, + makeCallback, +} from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { + getValidatedPath, + kMaxUserId, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import * as pathModule from "internal:deno_node/polyfills/path.ts"; +import { validateInteger } from "internal:deno_node/polyfills/internal/validators.mjs"; +import type { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +/** + * Asynchronously changes the owner and group + * of a file. + */ +export function chown( + path: string | Buffer | URL, + uid: number, + gid: number, + callback: CallbackWithError, +) { + callback = makeCallback(callback); + path = getValidatedPath(path).toString(); + validateInteger(uid, "uid", -1, kMaxUserId); + validateInteger(gid, "gid", -1, kMaxUserId); + + Deno.chown(pathModule.toNamespacedPath(path), uid, gid).then( + () => callback(null), + callback, + ); +} + +export const chownPromise = promisify(chown) as ( + path: string | Buffer | URL, + uid: number, + gid: number, +) => Promise<void>; + +/** + * Synchronously changes the owner and group + * of a file. + */ +export function chownSync( + path: string | Buffer | URL, + uid: number, + gid: number, +) { + path = getValidatedPath(path).toString(); + validateInteger(uid, "uid", -1, kMaxUserId); + validateInteger(gid, "gid", -1, kMaxUserId); + + Deno.chownSync(pathModule.toNamespacedPath(path), uid, gid); +} diff --git a/ext/node/polyfills/_fs/_fs_close.ts b/ext/node/polyfills/_fs/_fs_close.ts new file mode 100644 index 000000000..ff6082980 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_close.ts @@ -0,0 +1,21 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { getValidatedFd } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; + +export function close(fd: number, callback: CallbackWithError) { + fd = getValidatedFd(fd); + setTimeout(() => { + let error = null; + try { + Deno.close(fd); + } catch (err) { + error = err instanceof Error ? err : new Error("[non-error thrown]"); + } + callback(error); + }, 0); +} + +export function closeSync(fd: number) { + fd = getValidatedFd(fd); + Deno.close(fd); +} diff --git a/ext/node/polyfills/_fs/_fs_common.ts b/ext/node/polyfills/_fs/_fs_common.ts new file mode 100644 index 000000000..1e9f0f266 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_common.ts @@ -0,0 +1,233 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + O_APPEND, + O_CREAT, + O_EXCL, + O_RDONLY, + O_RDWR, + O_TRUNC, + O_WRONLY, +} from "internal:deno_node/polyfills/_fs/_fs_constants.ts"; +import { validateFunction } from "internal:deno_node/polyfills/internal/validators.mjs"; +import type { ErrnoException } from "internal:deno_node/polyfills/_global.d.ts"; +import { + BinaryEncodings, + Encodings, + notImplemented, + TextEncodings, +} from "internal:deno_node/polyfills/_utils.ts"; + +export type CallbackWithError = (err: ErrnoException | null) => void; + +export interface FileOptions { + encoding?: Encodings; + flag?: string; + signal?: AbortSignal; +} + +export type TextOptionsArgument = + | TextEncodings + | ({ encoding: TextEncodings } & FileOptions); +export type BinaryOptionsArgument = + | BinaryEncodings + | ({ encoding: BinaryEncodings } & FileOptions); +export type FileOptionsArgument = Encodings | FileOptions; + +export interface WriteFileOptions extends FileOptions { + mode?: number; +} + +export function isFileOptions( + fileOptions: string | WriteFileOptions | undefined, +): fileOptions is FileOptions { + if (!fileOptions) return false; + + return ( + (fileOptions as FileOptions).encoding != undefined || + (fileOptions as FileOptions).flag != undefined || + (fileOptions as FileOptions).signal != undefined || + (fileOptions as WriteFileOptions).mode != undefined + ); +} + +export function getEncoding( + optOrCallback?: + | FileOptions + | WriteFileOptions + // deno-lint-ignore no-explicit-any + | ((...args: any[]) => any) + | Encodings + | null, +): Encodings | null { + if (!optOrCallback || typeof optOrCallback === "function") { + return null; + } + + const encoding = typeof optOrCallback === "string" + ? optOrCallback + : optOrCallback.encoding; + if (!encoding) return null; + return encoding; +} + +export function checkEncoding(encoding: Encodings | null): Encodings | null { + if (!encoding) return null; + + encoding = encoding.toLowerCase() as Encodings; + if (["utf8", "hex", "base64"].includes(encoding)) return encoding; + + if (encoding === "utf-8") { + return "utf8"; + } + if (encoding === "binary") { + return "binary"; + // before this was buffer, however buffer is not used in Node + // node -e "require('fs').readFile('../world.txt', 'buffer', console.log)" + } + + const notImplementedEncodings = ["utf16le", "latin1", "ascii", "ucs2"]; + + if (notImplementedEncodings.includes(encoding as string)) { + notImplemented(`"${encoding}" encoding`); + } + + throw new Error(`The value "${encoding}" is invalid for option "encoding"`); +} + +export function getOpenOptions( + flag: string | number | undefined, +): Deno.OpenOptions { + if (!flag) { + return { create: true, append: true }; + } + + let openOptions: Deno.OpenOptions = {}; + + if (typeof flag === "string") { + switch (flag) { + case "a": { + // 'a': Open file for appending. The file is created if it does not exist. + openOptions = { create: true, append: true }; + break; + } + case "ax": + case "xa": { + // 'ax', 'xa': Like 'a' but fails if the path exists. + openOptions = { createNew: true, write: true, append: true }; + break; + } + case "a+": { + // 'a+': Open file for reading and appending. The file is created if it does not exist. + openOptions = { read: true, create: true, append: true }; + break; + } + case "ax+": + case "xa+": { + // 'ax+', 'xa+': Like 'a+' but fails if the path exists. + openOptions = { read: true, createNew: true, append: true }; + break; + } + case "r": { + // 'r': Open file for reading. An exception occurs if the file does not exist. + openOptions = { read: true }; + break; + } + case "r+": { + // 'r+': Open file for reading and writing. An exception occurs if the file does not exist. + openOptions = { read: true, write: true }; + break; + } + case "w": { + // 'w': Open file for writing. The file is created (if it does not exist) or truncated (if it exists). + openOptions = { create: true, write: true, truncate: true }; + break; + } + case "wx": + case "xw": { + // 'wx', 'xw': Like 'w' but fails if the path exists. + openOptions = { createNew: true, write: true }; + break; + } + case "w+": { + // 'w+': Open file for reading and writing. The file is created (if it does not exist) or truncated (if it exists). + openOptions = { create: true, write: true, truncate: true, read: true }; + break; + } + case "wx+": + case "xw+": { + // 'wx+', 'xw+': Like 'w+' but fails if the path exists. + openOptions = { createNew: true, write: true, read: true }; + break; + } + case "as": + case "sa": { + // 'as', 'sa': Open file for appending in synchronous mode. The file is created if it does not exist. + openOptions = { create: true, append: true }; + break; + } + case "as+": + case "sa+": { + // 'as+', 'sa+': Open file for reading and appending in synchronous mode. The file is created if it does not exist. + openOptions = { create: true, read: true, append: true }; + break; + } + case "rs+": + case "sr+": { + // 'rs+', 'sr+': Open file for reading and writing in synchronous mode. Instructs the operating system to bypass the local file system cache. + openOptions = { create: true, read: true, write: true }; + break; + } + default: { + throw new Error(`Unrecognized file system flag: ${flag}`); + } + } + } else if (typeof flag === "number") { + if ((flag & O_APPEND) === O_APPEND) { + openOptions.append = true; + } + if ((flag & O_CREAT) === O_CREAT) { + openOptions.create = true; + openOptions.write = true; + } + if ((flag & O_EXCL) === O_EXCL) { + openOptions.createNew = true; + openOptions.read = true; + openOptions.write = true; + } + if ((flag & O_TRUNC) === O_TRUNC) { + openOptions.truncate = true; + } + if ((flag & O_RDONLY) === O_RDONLY) { + openOptions.read = true; + } + if ((flag & O_WRONLY) === O_WRONLY) { + openOptions.write = true; + } + if ((flag & O_RDWR) === O_RDWR) { + openOptions.read = true; + openOptions.write = true; + } + } + + return openOptions; +} + +export { isUint32 as isFd } from "internal:deno_node/polyfills/internal/validators.mjs"; + +export function maybeCallback(cb: unknown) { + validateFunction(cb, "cb"); + + return cb as CallbackWithError; +} + +// Ensure that callbacks run in the global context. Only use this function +// for callbacks that are passed to the binding layer, callbacks that are +// invoked from JS already run in the proper scope. +export function makeCallback( + this: unknown, + cb?: (err: Error | null, result?: unknown) => void, +) { + validateFunction(cb, "cb"); + + return (...args: unknown[]) => Reflect.apply(cb!, this, args); +} diff --git a/ext/node/polyfills/_fs/_fs_constants.ts b/ext/node/polyfills/_fs/_fs_constants.ts new file mode 100644 index 000000000..761f6a9b7 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_constants.ts @@ -0,0 +1,39 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { fs } from "internal:deno_node/polyfills/internal_binding/constants.ts"; + +export const { + F_OK, + R_OK, + W_OK, + X_OK, + S_IRUSR, + S_IWUSR, + S_IXUSR, + S_IRGRP, + S_IWGRP, + S_IXGRP, + S_IROTH, + S_IWOTH, + S_IXOTH, + COPYFILE_EXCL, + COPYFILE_FICLONE, + COPYFILE_FICLONE_FORCE, + UV_FS_COPYFILE_EXCL, + UV_FS_COPYFILE_FICLONE, + UV_FS_COPYFILE_FICLONE_FORCE, + O_RDONLY, + O_WRONLY, + O_RDWR, + O_NOCTTY, + O_TRUNC, + O_APPEND, + O_DIRECTORY, + O_NOFOLLOW, + O_SYNC, + O_DSYNC, + O_SYMLINK, + O_NONBLOCK, + O_CREAT, + O_EXCL, +} = fs; diff --git a/ext/node/polyfills/_fs/_fs_copy.ts b/ext/node/polyfills/_fs/_fs_copy.ts new file mode 100644 index 000000000..0971da1eb --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_copy.ts @@ -0,0 +1,88 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { makeCallback } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { + getValidatedPath, + getValidMode, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { fs } from "internal:deno_node/polyfills/internal_binding/constants.ts"; +import { codeMap } from "internal:deno_node/polyfills/internal_binding/uv.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function copyFile( + src: string | Buffer | URL, + dest: string | Buffer | URL, + callback: CallbackWithError, +): void; +export function copyFile( + src: string | Buffer | URL, + dest: string | Buffer | URL, + mode: number, + callback: CallbackWithError, +): void; +export function copyFile( + src: string | Buffer | URL, + dest: string | Buffer | URL, + mode: number | CallbackWithError, + callback?: CallbackWithError, +) { + if (typeof mode === "function") { + callback = mode; + mode = 0; + } + const srcStr = getValidatedPath(src, "src").toString(); + const destStr = getValidatedPath(dest, "dest").toString(); + const modeNum = getValidMode(mode, "copyFile"); + const cb = makeCallback(callback); + + if ((modeNum & fs.COPYFILE_EXCL) === fs.COPYFILE_EXCL) { + Deno.lstat(destStr).then(() => { + // deno-lint-ignore no-explicit-any + const e: any = new Error( + `EEXIST: file already exists, copyfile '${srcStr}' -> '${destStr}'`, + ); + e.syscall = "copyfile"; + e.errno = codeMap.get("EEXIST"); + e.code = "EEXIST"; + cb(e); + }, (e) => { + if (e instanceof Deno.errors.NotFound) { + Deno.copyFile(srcStr, destStr).then(() => cb(null), cb); + } + cb(e); + }); + } else { + Deno.copyFile(srcStr, destStr).then(() => cb(null), cb); + } +} + +export const copyFilePromise = promisify(copyFile) as ( + src: string | Buffer | URL, + dest: string | Buffer | URL, + mode?: number, +) => Promise<void>; + +export function copyFileSync( + src: string | Buffer | URL, + dest: string | Buffer | URL, + mode?: number, +) { + const srcStr = getValidatedPath(src, "src").toString(); + const destStr = getValidatedPath(dest, "dest").toString(); + const modeNum = getValidMode(mode, "copyFile"); + + if ((modeNum & fs.COPYFILE_EXCL) === fs.COPYFILE_EXCL) { + try { + Deno.lstatSync(destStr); + throw new Error(`A file exists at the destination: ${destStr}`); + } catch (e) { + if (e instanceof Deno.errors.NotFound) { + Deno.copyFileSync(srcStr, destStr); + } + throw e; + } + } else { + Deno.copyFileSync(srcStr, destStr); + } +} diff --git a/ext/node/polyfills/_fs/_fs_dir.ts b/ext/node/polyfills/_fs/_fs_dir.ts new file mode 100644 index 000000000..e13547241 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_dir.ts @@ -0,0 +1,104 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import Dirent from "internal:deno_node/polyfills/_fs/_fs_dirent.ts"; +import { assert } from "internal:deno_node/polyfills/_util/asserts.ts"; +import { ERR_MISSING_ARGS } from "internal:deno_node/polyfills/internal/errors.ts"; +import { TextDecoder } from "internal:deno_web/08_text_encoding.js"; + +export default class Dir { + #dirPath: string | Uint8Array; + #syncIterator!: Iterator<Deno.DirEntry, undefined> | null; + #asyncIterator!: AsyncIterator<Deno.DirEntry, undefined> | null; + + constructor(path: string | Uint8Array) { + if (!path) { + throw new ERR_MISSING_ARGS("path"); + } + this.#dirPath = path; + } + + get path(): string { + if (this.#dirPath instanceof Uint8Array) { + return new TextDecoder().decode(this.#dirPath); + } + return this.#dirPath; + } + + // deno-lint-ignore no-explicit-any + read(callback?: (...args: any[]) => void): Promise<Dirent | null> { + return new Promise((resolve, reject) => { + if (!this.#asyncIterator) { + this.#asyncIterator = Deno.readDir(this.path)[Symbol.asyncIterator](); + } + assert(this.#asyncIterator); + this.#asyncIterator + .next() + .then((iteratorResult) => { + resolve( + iteratorResult.done ? null : new Dirent(iteratorResult.value), + ); + if (callback) { + callback( + null, + iteratorResult.done ? null : new Dirent(iteratorResult.value), + ); + } + }, (err) => { + if (callback) { + callback(err); + } + reject(err); + }); + }); + } + + readSync(): Dirent | null { + if (!this.#syncIterator) { + this.#syncIterator = Deno.readDirSync(this.path)![Symbol.iterator](); + } + + const iteratorResult = this.#syncIterator.next(); + if (iteratorResult.done) { + return null; + } else { + return new Dirent(iteratorResult.value); + } + } + + /** + * Unlike Node, Deno does not require managing resource ids for reading + * directories, and therefore does not need to close directories when + * finished reading. + */ + // deno-lint-ignore no-explicit-any + close(callback?: (...args: any[]) => void): Promise<void> { + return new Promise((resolve) => { + if (callback) { + callback(null); + } + resolve(); + }); + } + + /** + * Unlike Node, Deno does not require managing resource ids for reading + * directories, and therefore does not need to close directories when + * finished reading + */ + closeSync() { + //No op + } + + async *[Symbol.asyncIterator](): AsyncIterableIterator<Dirent> { + try { + while (true) { + const dirent: Dirent | null = await this.read(); + if (dirent === null) { + break; + } + yield dirent; + } + } finally { + await this.close(); + } + } +} diff --git a/ext/node/polyfills/_fs/_fs_dirent.ts b/ext/node/polyfills/_fs/_fs_dirent.ts new file mode 100644 index 000000000..5a7c243bf --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_dirent.ts @@ -0,0 +1,46 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { notImplemented } from "internal:deno_node/polyfills/_utils.ts"; + +export default class Dirent { + constructor(private entry: Deno.DirEntry) {} + + isBlockDevice(): boolean { + notImplemented("Deno does not yet support identification of block devices"); + return false; + } + + isCharacterDevice(): boolean { + notImplemented( + "Deno does not yet support identification of character devices", + ); + return false; + } + + isDirectory(): boolean { + return this.entry.isDirectory; + } + + isFIFO(): boolean { + notImplemented( + "Deno does not yet support identification of FIFO named pipes", + ); + return false; + } + + isFile(): boolean { + return this.entry.isFile; + } + + isSocket(): boolean { + notImplemented("Deno does not yet support identification of sockets"); + return false; + } + + isSymbolicLink(): boolean { + return this.entry.isSymlink; + } + + get name(): string | null { + return this.entry.name; + } +} diff --git a/ext/node/polyfills/_fs/_fs_exists.ts b/ext/node/polyfills/_fs/_fs_exists.ts new file mode 100644 index 000000000..9b0f18303 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_exists.ts @@ -0,0 +1,40 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; + +type ExistsCallback = (exists: boolean) => void; + +/** + * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + * Deprecated in node api + */ +export function exists(path: string | URL, callback: ExistsCallback) { + path = path instanceof URL ? fromFileUrl(path) : path; + Deno.lstat(path).then(() => callback(true), () => callback(false)); +} + +// The callback of fs.exists doesn't have standard callback signature. +// We need to provide special implementation for promisify. +// See https://github.com/nodejs/node/pull/13316 +const kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); +Object.defineProperty(exists, kCustomPromisifiedSymbol, { + value: (path: string | URL) => { + return new Promise((resolve) => { + exists(path, (exists) => resolve(exists)); + }); + }, +}); + +/** + * TODO: Also accept 'path' parameter as a Node polyfill Buffer or URL type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export function existsSync(path: string | URL): boolean { + path = path instanceof URL ? fromFileUrl(path) : path; + try { + Deno.lstatSync(path); + return true; + } catch (_err) { + return false; + } +} diff --git a/ext/node/polyfills/_fs/_fs_fdatasync.ts b/ext/node/polyfills/_fs/_fs_fdatasync.ts new file mode 100644 index 000000000..325ac30da --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_fdatasync.ts @@ -0,0 +1,13 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; + +export function fdatasync( + fd: number, + callback: CallbackWithError, +) { + Deno.fdatasync(fd).then(() => callback(null), callback); +} + +export function fdatasyncSync(fd: number) { + Deno.fdatasyncSync(fd); +} diff --git a/ext/node/polyfills/_fs/_fs_fstat.ts b/ext/node/polyfills/_fs/_fs_fstat.ts new file mode 100644 index 000000000..ab9cbead4 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_fstat.ts @@ -0,0 +1,60 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + BigIntStats, + CFISBIS, + statCallback, + statCallbackBigInt, + statOptions, + Stats, +} from "internal:deno_node/polyfills/_fs/_fs_stat.ts"; + +export function fstat(fd: number, callback: statCallback): void; +export function fstat( + fd: number, + options: { bigint: false }, + callback: statCallback, +): void; +export function fstat( + fd: number, + options: { bigint: true }, + callback: statCallbackBigInt, +): void; +export function fstat( + fd: number, + optionsOrCallback: statCallback | statCallbackBigInt | statOptions, + maybeCallback?: statCallback | statCallbackBigInt, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as ( + ...args: [Error] | [null, BigIntStats | Stats] + ) => void; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : { bigint: false }; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.fstat(fd).then( + (stat) => callback(null, CFISBIS(stat, options.bigint)), + (err) => callback(err), + ); +} + +export function fstatSync(fd: number): Stats; +export function fstatSync( + fd: number, + options: { bigint: false }, +): Stats; +export function fstatSync( + fd: number, + options: { bigint: true }, +): BigIntStats; +export function fstatSync( + fd: number, + options?: statOptions, +): Stats | BigIntStats { + const origin = Deno.fstatSync(fd); + return CFISBIS(origin, options?.bigint || false); +} diff --git a/ext/node/polyfills/_fs/_fs_fsync.ts b/ext/node/polyfills/_fs/_fs_fsync.ts new file mode 100644 index 000000000..02be24abc --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_fsync.ts @@ -0,0 +1,13 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; + +export function fsync( + fd: number, + callback: CallbackWithError, +) { + Deno.fsync(fd).then(() => callback(null), callback); +} + +export function fsyncSync(fd: number) { + Deno.fsyncSync(fd); +} diff --git a/ext/node/polyfills/_fs/_fs_ftruncate.ts b/ext/node/polyfills/_fs/_fs_ftruncate.ts new file mode 100644 index 000000000..9c7bfbd01 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_ftruncate.ts @@ -0,0 +1,23 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; + +export function ftruncate( + fd: number, + lenOrCallback: number | CallbackWithError, + maybeCallback?: CallbackWithError, +) { + const len: number | undefined = typeof lenOrCallback === "number" + ? lenOrCallback + : undefined; + const callback: CallbackWithError = typeof lenOrCallback === "function" + ? lenOrCallback + : maybeCallback as CallbackWithError; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.ftruncate(fd, len).then(() => callback(null), callback); +} + +export function ftruncateSync(fd: number, len?: number) { + Deno.ftruncateSync(fd, len); +} diff --git a/ext/node/polyfills/_fs/_fs_futimes.ts b/ext/node/polyfills/_fs/_fs_futimes.ts new file mode 100644 index 000000000..60f06bc34 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_futimes.ts @@ -0,0 +1,50 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; + +function getValidTime( + time: number | string | Date, + name: string, +): number | Date { + if (typeof time === "string") { + time = Number(time); + } + + if ( + typeof time === "number" && + (Number.isNaN(time) || !Number.isFinite(time)) + ) { + throw new Deno.errors.InvalidData( + `invalid ${name}, must not be infinity or NaN`, + ); + } + + return time; +} + +export function futimes( + fd: number, + atime: number | string | Date, + mtime: number | string | Date, + callback: CallbackWithError, +) { + if (!callback) { + throw new Deno.errors.InvalidData("No callback function supplied"); + } + + atime = getValidTime(atime, "atime"); + mtime = getValidTime(mtime, "mtime"); + + Deno.futime(fd, atime, mtime).then(() => callback(null), callback); +} + +export function futimesSync( + fd: number, + atime: number | string | Date, + mtime: number | string | Date, +) { + atime = getValidTime(atime, "atime"); + mtime = getValidTime(mtime, "mtime"); + + Deno.futimeSync(fd, atime, mtime); +} diff --git a/ext/node/polyfills/_fs/_fs_link.ts b/ext/node/polyfills/_fs/_fs_link.ts new file mode 100644 index 000000000..eb95a10f6 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_link.ts @@ -0,0 +1,46 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +/** + * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export function link( + existingPath: string | URL, + newPath: string | URL, + callback: CallbackWithError, +) { + existingPath = existingPath instanceof URL + ? fromFileUrl(existingPath) + : existingPath; + newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath; + + Deno.link(existingPath, newPath).then(() => callback(null), callback); +} + +/** + * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export const linkPromise = promisify(link) as ( + existingPath: string | URL, + newPath: string | URL, +) => Promise<void>; + +/** + * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +export function linkSync( + existingPath: string | URL, + newPath: string | URL, +) { + existingPath = existingPath instanceof URL + ? fromFileUrl(existingPath) + : existingPath; + newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath; + + Deno.linkSync(existingPath, newPath); +} diff --git a/ext/node/polyfills/_fs/_fs_lstat.ts b/ext/node/polyfills/_fs/_fs_lstat.ts new file mode 100644 index 000000000..c85f82a11 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_lstat.ts @@ -0,0 +1,67 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + BigIntStats, + CFISBIS, + statCallback, + statCallbackBigInt, + statOptions, + Stats, +} from "internal:deno_node/polyfills/_fs/_fs_stat.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function lstat(path: string | URL, callback: statCallback): void; +export function lstat( + path: string | URL, + options: { bigint: false }, + callback: statCallback, +): void; +export function lstat( + path: string | URL, + options: { bigint: true }, + callback: statCallbackBigInt, +): void; +export function lstat( + path: string | URL, + optionsOrCallback: statCallback | statCallbackBigInt | statOptions, + maybeCallback?: statCallback | statCallbackBigInt, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as ( + ...args: [Error] | [null, BigIntStats | Stats] + ) => void; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : { bigint: false }; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.lstat(path).then( + (stat) => callback(null, CFISBIS(stat, options.bigint)), + (err) => callback(err), + ); +} + +export const lstatPromise = promisify(lstat) as ( + & ((path: string | URL) => Promise<Stats>) + & ((path: string | URL, options: { bigint: false }) => Promise<Stats>) + & ((path: string | URL, options: { bigint: true }) => Promise<BigIntStats>) +); + +export function lstatSync(path: string | URL): Stats; +export function lstatSync( + path: string | URL, + options: { bigint: false }, +): Stats; +export function lstatSync( + path: string | URL, + options: { bigint: true }, +): BigIntStats; +export function lstatSync( + path: string | URL, + options?: statOptions, +): Stats | BigIntStats { + const origin = Deno.lstatSync(path); + return CFISBIS(origin, options?.bigint || false); +} diff --git a/ext/node/polyfills/_fs/_fs_mkdir.ts b/ext/node/polyfills/_fs/_fs_mkdir.ts new file mode 100644 index 000000000..ac4b78259 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_mkdir.ts @@ -0,0 +1,77 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; +import { denoErrorToNodeError } from "internal:deno_node/polyfills/internal/errors.ts"; +import { getValidatedPath } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { validateBoolean } from "internal:deno_node/polyfills/internal/validators.mjs"; + +/** + * TODO: Also accept 'path' parameter as a Node polyfill Buffer type once these + * are implemented. See https://github.com/denoland/deno/issues/3403 + */ +type MkdirOptions = + | { recursive?: boolean; mode?: number | undefined } + | number + | boolean; + +export function mkdir( + path: string | URL, + options?: MkdirOptions | CallbackWithError, + callback?: CallbackWithError, +) { + path = getValidatedPath(path) as string; + + let mode = 0o777; + let recursive = false; + + if (typeof options == "function") { + callback = options; + } else if (typeof options === "number") { + mode = options; + } else if (typeof options === "boolean") { + recursive = options; + } else if (options) { + if (options.recursive !== undefined) recursive = options.recursive; + if (options.mode !== undefined) mode = options.mode; + } + validateBoolean(recursive, "options.recursive"); + + Deno.mkdir(path, { recursive, mode }) + .then(() => { + if (typeof callback === "function") { + callback(null); + } + }, (err) => { + if (typeof callback === "function") { + callback(err); + } + }); +} + +export const mkdirPromise = promisify(mkdir) as ( + path: string | URL, + options?: MkdirOptions, +) => Promise<void>; + +export function mkdirSync(path: string | URL, options?: MkdirOptions) { + path = getValidatedPath(path) as string; + + let mode = 0o777; + let recursive = false; + + if (typeof options === "number") { + mode = options; + } else if (typeof options === "boolean") { + recursive = options; + } else if (options) { + if (options.recursive !== undefined) recursive = options.recursive; + if (options.mode !== undefined) mode = options.mode; + } + validateBoolean(recursive, "options.recursive"); + + try { + Deno.mkdirSync(path, { recursive, mode }); + } catch (err) { + throw denoErrorToNodeError(err as Error, { syscall: "mkdir", path }); + } +} diff --git a/ext/node/polyfills/_fs/_fs_mkdtemp.ts b/ext/node/polyfills/_fs/_fs_mkdtemp.ts new file mode 100644 index 000000000..de227b216 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_mkdtemp.ts @@ -0,0 +1,115 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Node.js contributors. All rights reserved. MIT License. + +import { + TextDecoder, + TextEncoder, +} from "internal:deno_web/08_text_encoding.js"; +import { existsSync } from "internal:deno_node/polyfills/_fs/_fs_exists.ts"; +import { + mkdir, + mkdirSync, +} from "internal:deno_node/polyfills/_fs/_fs_mkdir.ts"; +import { + ERR_INVALID_ARG_TYPE, + ERR_INVALID_OPT_VALUE_ENCODING, +} from "internal:deno_node/polyfills/internal/errors.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export type mkdtempCallback = ( + err: Error | null, + directory?: string, +) => void; + +// https://nodejs.org/dist/latest-v15.x/docs/api/fs.html#fs_fs_mkdtemp_prefix_options_callback +export function mkdtemp(prefix: string, callback: mkdtempCallback): void; +export function mkdtemp( + prefix: string, + options: { encoding: string } | string, + callback: mkdtempCallback, +): void; +export function mkdtemp( + prefix: string, + optionsOrCallback: { encoding: string } | string | mkdtempCallback, + maybeCallback?: mkdtempCallback, +) { + const callback: mkdtempCallback | undefined = + typeof optionsOrCallback == "function" ? optionsOrCallback : maybeCallback; + if (!callback) { + throw new ERR_INVALID_ARG_TYPE("callback", "function", callback); + } + + const encoding: string | undefined = parseEncoding(optionsOrCallback); + const path = tempDirPath(prefix); + + mkdir( + path, + { recursive: false, mode: 0o700 }, + (err: Error | null | undefined) => { + if (err) callback(err); + else callback(null, decode(path, encoding)); + }, + ); +} + +export const mkdtempPromise = promisify(mkdtemp) as ( + prefix: string, + options?: { encoding: string } | string, +) => Promise<string>; + +// https://nodejs.org/dist/latest-v15.x/docs/api/fs.html#fs_fs_mkdtempsync_prefix_options +export function mkdtempSync( + prefix: string, + options?: { encoding: string } | string, +): string { + const encoding: string | undefined = parseEncoding(options); + const path = tempDirPath(prefix); + + mkdirSync(path, { recursive: false, mode: 0o700 }); + return decode(path, encoding); +} + +function parseEncoding( + optionsOrCallback?: { encoding: string } | string | mkdtempCallback, +): string | undefined { + let encoding: string | undefined; + if (typeof optionsOrCallback == "function") encoding = undefined; + else if (optionsOrCallback instanceof Object) { + encoding = optionsOrCallback?.encoding; + } else encoding = optionsOrCallback; + + if (encoding) { + try { + new TextDecoder(encoding); + } catch { + throw new ERR_INVALID_OPT_VALUE_ENCODING(encoding); + } + } + + return encoding; +} + +function decode(str: string, encoding?: string): string { + if (!encoding) return str; + else { + const decoder = new TextDecoder(encoding); + const encoder = new TextEncoder(); + return decoder.decode(encoder.encode(str)); + } +} + +const CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; +function randomName(): string { + return [...Array(6)].map(() => + CHARS[Math.floor(Math.random() * CHARS.length)] + ).join(""); +} + +function tempDirPath(prefix: string): string { + let path: string; + do { + path = prefix + randomName(); + } while (existsSync(path)); + + return path; +} diff --git a/ext/node/polyfills/_fs/_fs_open.ts b/ext/node/polyfills/_fs/_fs_open.ts new file mode 100644 index 000000000..e703da56f --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_open.ts @@ -0,0 +1,198 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + O_APPEND, + O_CREAT, + O_EXCL, + O_RDWR, + O_TRUNC, + O_WRONLY, +} from "internal:deno_node/polyfills/_fs/_fs_constants.ts"; +import { getOpenOptions } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; +import { parseFileMode } from "internal:deno_node/polyfills/internal/validators.mjs"; +import { ERR_INVALID_ARG_TYPE } from "internal:deno_node/polyfills/internal/errors.ts"; +import { getValidatedPath } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import type { Buffer } from "internal:deno_node/polyfills/buffer.ts"; + +function existsSync(filePath: string | URL): boolean { + try { + Deno.lstatSync(filePath); + return true; + } catch (error) { + if (error instanceof Deno.errors.NotFound) { + return false; + } + throw error; + } +} + +const FLAGS_AX = O_APPEND | O_CREAT | O_WRONLY | O_EXCL; +const FLAGS_AX_PLUS = O_APPEND | O_CREAT | O_RDWR | O_EXCL; +const FLAGS_WX = O_TRUNC | O_CREAT | O_WRONLY | O_EXCL; +const FLAGS_WX_PLUS = O_TRUNC | O_CREAT | O_RDWR | O_EXCL; + +export type openFlags = + | "a" + | "ax" + | "a+" + | "ax+" + | "as" + | "as+" + | "r" + | "r+" + | "rs+" + | "w" + | "wx" + | "w+" + | "wx+" + | number; + +type openCallback = (err: Error | null, fd: number) => void; + +function convertFlagAndModeToOptions( + flag?: openFlags, + mode?: number, +): Deno.OpenOptions | undefined { + if (!flag && !mode) return undefined; + if (!flag && mode) return { mode }; + return { ...getOpenOptions(flag), mode }; +} + +export function open(path: string | Buffer | URL, callback: openCallback): void; +export function open( + path: string | Buffer | URL, + flags: openFlags, + callback: openCallback, +): void; +export function open( + path: string | Buffer | URL, + flags: openFlags, + mode: number, + callback: openCallback, +): void; +export function open( + path: string | Buffer | URL, + flags: openCallback | openFlags, + mode?: openCallback | number, + callback?: openCallback, +) { + if (flags === undefined) { + throw new ERR_INVALID_ARG_TYPE( + "flags or callback", + ["string", "function"], + flags, + ); + } + path = getValidatedPath(path); + if (arguments.length < 3) { + // deno-lint-ignore no-explicit-any + callback = flags as any; + flags = "r"; + mode = 0o666; + } else if (typeof mode === "function") { + callback = mode; + mode = 0o666; + } else { + mode = parseFileMode(mode, "mode", 0o666); + } + + if (typeof callback !== "function") { + throw new ERR_INVALID_ARG_TYPE( + "callback", + "function", + callback, + ); + } + + if (flags === undefined) { + flags = "r"; + } + + if ( + existenceCheckRequired(flags as openFlags) && + existsSync(path as string) + ) { + const err = new Error(`EEXIST: file already exists, open '${path}'`); + (callback as (err: Error) => void)(err); + } else { + if (flags === "as" || flags === "as+") { + let err: Error | null = null, res: number; + try { + res = openSync(path, flags, mode); + } catch (error) { + err = error instanceof Error ? error : new Error("[non-error thrown]"); + } + if (err) { + (callback as (err: Error) => void)(err); + } else { + callback(null, res!); + } + return; + } + Deno.open( + path as string, + convertFlagAndModeToOptions(flags as openFlags, mode), + ).then( + (file) => callback!(null, file.rid), + (err) => (callback as (err: Error) => void)(err), + ); + } +} + +export const openPromise = promisify(open) as ( + & ((path: string | Buffer | URL) => Promise<number>) + & ((path: string | Buffer | URL, flags: openFlags) => Promise<number>) + & ((path: string | Buffer | URL, mode?: number) => Promise<number>) + & (( + path: string | Buffer | URL, + flags?: openFlags, + mode?: number, + ) => Promise<number>) +); + +export function openSync(path: string | Buffer | URL): number; +export function openSync( + path: string | Buffer | URL, + flags?: openFlags, +): number; +export function openSync(path: string | Buffer | URL, mode?: number): number; +export function openSync( + path: string | Buffer | URL, + flags?: openFlags, + mode?: number, +): number; +export function openSync( + path: string | Buffer | URL, + flags?: openFlags, + maybeMode?: number, +) { + const mode = parseFileMode(maybeMode, "mode", 0o666); + path = getValidatedPath(path); + + if (flags === undefined) { + flags = "r"; + } + + if ( + existenceCheckRequired(flags) && + existsSync(path as string) + ) { + throw new Error(`EEXIST: file already exists, open '${path}'`); + } + + return Deno.openSync(path as string, convertFlagAndModeToOptions(flags, mode)) + .rid; +} + +function existenceCheckRequired(flags: openFlags | number) { + return ( + (typeof flags === "string" && + ["ax", "ax+", "wx", "wx+"].includes(flags)) || + (typeof flags === "number" && ( + ((flags & FLAGS_AX) === FLAGS_AX) || + ((flags & FLAGS_AX_PLUS) === FLAGS_AX_PLUS) || + ((flags & FLAGS_WX) === FLAGS_WX) || + ((flags & FLAGS_WX_PLUS) === FLAGS_WX_PLUS) + )) + ); +} diff --git a/ext/node/polyfills/_fs/_fs_opendir.ts b/ext/node/polyfills/_fs/_fs_opendir.ts new file mode 100644 index 000000000..5ee13f951 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_opendir.ts @@ -0,0 +1,89 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import Dir from "internal:deno_node/polyfills/_fs/_fs_dir.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { + getOptions, + getValidatedPath, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { denoErrorToNodeError } from "internal:deno_node/polyfills/internal/errors.ts"; +import { + validateFunction, + validateInteger, +} from "internal:deno_node/polyfills/internal/validators.mjs"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +/** These options aren't funcitonally used right now, as `Dir` doesn't yet support them. + * However, these values are still validated. + */ +type Options = { + encoding?: string; + bufferSize?: number; +}; +type Callback = (err?: Error | null, dir?: Dir) => void; + +function _validateFunction(callback: unknown): asserts callback is Callback { + validateFunction(callback, "callback"); +} + +/** @link https://nodejs.org/api/fs.html#fsopendirsyncpath-options */ +export function opendir( + path: string | Buffer | URL, + options: Options | Callback, + callback?: Callback, +) { + callback = typeof options === "function" ? options : callback; + _validateFunction(callback); + + path = getValidatedPath(path).toString(); + + let err, dir; + try { + const { bufferSize } = getOptions(options, { + encoding: "utf8", + bufferSize: 32, + }); + validateInteger(bufferSize, "options.bufferSize", 1, 4294967295); + + /** Throws if path is invalid */ + Deno.readDirSync(path); + + dir = new Dir(path); + } catch (error) { + err = denoErrorToNodeError(error as Error, { syscall: "opendir" }); + } + if (err) { + callback(err); + } else { + callback(null, dir); + } +} + +/** @link https://nodejs.org/api/fs.html#fspromisesopendirpath-options */ +export const opendirPromise = promisify(opendir) as ( + path: string | Buffer | URL, + options?: Options, +) => Promise<Dir>; + +export function opendirSync( + path: string | Buffer | URL, + options?: Options, +): Dir { + path = getValidatedPath(path).toString(); + + const { bufferSize } = getOptions(options, { + encoding: "utf8", + bufferSize: 32, + }); + + validateInteger(bufferSize, "options.bufferSize", 1, 4294967295); + + try { + /** Throws if path is invalid */ + Deno.readDirSync(path); + + return new Dir(path); + } catch (err) { + throw denoErrorToNodeError(err as Error, { syscall: "opendir" }); + } +} diff --git a/ext/node/polyfills/_fs/_fs_read.ts b/ext/node/polyfills/_fs/_fs_read.ts new file mode 100644 index 000000000..d74445829 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_read.ts @@ -0,0 +1,197 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { ERR_INVALID_ARG_TYPE } from "internal:deno_node/polyfills/internal/errors.ts"; +import { + validateOffsetLengthRead, + validatePosition, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { + validateBuffer, + validateInteger, +} from "internal:deno_node/polyfills/internal/validators.mjs"; + +type readOptions = { + buffer: Buffer | Uint8Array; + offset: number; + length: number; + position: number | null; +}; + +type readSyncOptions = { + offset: number; + length: number; + position: number | null; +}; + +type BinaryCallback = ( + err: Error | null, + bytesRead: number | null, + data?: Buffer, +) => void; +type Callback = BinaryCallback; + +export function read(fd: number, callback: Callback): void; +export function read( + fd: number, + options: readOptions, + callback: Callback, +): void; +export function read( + fd: number, + buffer: Buffer | Uint8Array, + offset: number, + length: number, + position: number | null, + callback: Callback, +): void; +export function read( + fd: number, + optOrBufferOrCb?: Buffer | Uint8Array | readOptions | Callback, + offsetOrCallback?: number | Callback, + length?: number, + position?: number | null, + callback?: Callback, +) { + let cb: Callback | undefined; + let offset = 0, + buffer: Buffer | Uint8Array; + + if (typeof fd !== "number") { + throw new ERR_INVALID_ARG_TYPE("fd", "number", fd); + } + + if (length == null) { + length = 0; + } + + if (typeof offsetOrCallback === "function") { + cb = offsetOrCallback; + } else if (typeof optOrBufferOrCb === "function") { + cb = optOrBufferOrCb; + } else { + offset = offsetOrCallback as number; + validateInteger(offset, "offset", 0); + cb = callback; + } + + if ( + optOrBufferOrCb instanceof Buffer || optOrBufferOrCb instanceof Uint8Array + ) { + buffer = optOrBufferOrCb; + } else if (typeof optOrBufferOrCb === "function") { + offset = 0; + buffer = Buffer.alloc(16384); + length = buffer.byteLength; + position = null; + } else { + const opt = optOrBufferOrCb as readOptions; + if ( + !(opt.buffer instanceof Buffer) && !(opt.buffer instanceof Uint8Array) + ) { + if (opt.buffer === null) { + // @ts-ignore: Intentionally create TypeError for passing test-fs-read.js#L87 + length = opt.buffer.byteLength; + } + throw new ERR_INVALID_ARG_TYPE("buffer", [ + "Buffer", + "TypedArray", + "DataView", + ], optOrBufferOrCb); + } + offset = opt.offset ?? 0; + buffer = opt.buffer ?? Buffer.alloc(16384); + length = opt.length ?? buffer.byteLength; + position = opt.position ?? null; + } + + if (position == null) { + position = -1; + } + + validatePosition(position); + validateOffsetLengthRead(offset, length, buffer.byteLength); + + if (!cb) throw new ERR_INVALID_ARG_TYPE("cb", "Callback", cb); + + (async () => { + try { + let nread: number | null; + if (typeof position === "number" && position >= 0) { + const currentPosition = await Deno.seek(fd, 0, Deno.SeekMode.Current); + // We use sync calls below to avoid being affected by others during + // these calls. + Deno.seekSync(fd, position, Deno.SeekMode.Start); + nread = Deno.readSync(fd, buffer); + Deno.seekSync(fd, currentPosition, Deno.SeekMode.Start); + } else { + nread = await Deno.read(fd, buffer); + } + cb(null, nread ?? 0, Buffer.from(buffer.buffer, offset, length)); + } catch (error) { + cb(error as Error, null); + } + })(); +} + +export function readSync( + fd: number, + buffer: Buffer | Uint8Array, + offset: number, + length: number, + position: number | null, +): number; +export function readSync( + fd: number, + buffer: Buffer | Uint8Array, + opt: readSyncOptions, +): number; +export function readSync( + fd: number, + buffer: Buffer | Uint8Array, + offsetOrOpt?: number | readSyncOptions, + length?: number, + position?: number | null, +): number { + let offset = 0; + + if (typeof fd !== "number") { + throw new ERR_INVALID_ARG_TYPE("fd", "number", fd); + } + + validateBuffer(buffer); + + if (length == null) { + length = 0; + } + + if (typeof offsetOrOpt === "number") { + offset = offsetOrOpt; + validateInteger(offset, "offset", 0); + } else { + const opt = offsetOrOpt as readSyncOptions; + offset = opt.offset ?? 0; + length = opt.length ?? buffer.byteLength; + position = opt.position ?? null; + } + + if (position == null) { + position = -1; + } + + validatePosition(position); + validateOffsetLengthRead(offset, length, buffer.byteLength); + + let currentPosition = 0; + if (typeof position === "number" && position >= 0) { + currentPosition = Deno.seekSync(fd, 0, Deno.SeekMode.Current); + Deno.seekSync(fd, position, Deno.SeekMode.Start); + } + + const numberOfBytesRead = Deno.readSync(fd, buffer); + + if (typeof position === "number" && position >= 0) { + Deno.seekSync(fd, currentPosition, Deno.SeekMode.Start); + } + + return numberOfBytesRead ?? 0; +} diff --git a/ext/node/polyfills/_fs/_fs_readFile.ts b/ext/node/polyfills/_fs/_fs_readFile.ts new file mode 100644 index 000000000..6c5e9fb8b --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_readFile.ts @@ -0,0 +1,108 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + BinaryOptionsArgument, + FileOptionsArgument, + getEncoding, + TextOptionsArgument, +} from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { + BinaryEncodings, + Encodings, + TextEncodings, +} from "internal:deno_node/polyfills/_utils.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +function maybeDecode(data: Uint8Array, encoding: TextEncodings): string; +function maybeDecode( + data: Uint8Array, + encoding: BinaryEncodings | null, +): Buffer; +function maybeDecode( + data: Uint8Array, + encoding: Encodings | null, +): string | Buffer { + const buffer = Buffer.from(data.buffer, data.byteOffset, data.byteLength); + if (encoding && encoding !== "binary") return buffer.toString(encoding); + return buffer; +} + +type TextCallback = (err: Error | null, data?: string) => void; +type BinaryCallback = (err: Error | null, data?: Buffer) => void; +type GenericCallback = (err: Error | null, data?: string | Buffer) => void; +type Callback = TextCallback | BinaryCallback | GenericCallback; + +export function readFile( + path: string | URL, + options: TextOptionsArgument, + callback: TextCallback, +): void; +export function readFile( + path: string | URL, + options: BinaryOptionsArgument, + callback: BinaryCallback, +): void; +export function readFile( + path: string | URL, + options: null | undefined | FileOptionsArgument, + callback: BinaryCallback, +): void; +export function readFile(path: string | URL, callback: BinaryCallback): void; +export function readFile( + path: string | URL, + optOrCallback?: FileOptionsArgument | Callback | null | undefined, + callback?: Callback, +) { + path = path instanceof URL ? fromFileUrl(path) : path; + let cb: Callback | undefined; + if (typeof optOrCallback === "function") { + cb = optOrCallback; + } else { + cb = callback; + } + + const encoding = getEncoding(optOrCallback); + + const p = Deno.readFile(path); + + if (cb) { + p.then((data: Uint8Array) => { + if (encoding && encoding !== "binary") { + const text = maybeDecode(data, encoding); + return (cb as TextCallback)(null, text); + } + const buffer = maybeDecode(data, encoding); + (cb as BinaryCallback)(null, buffer); + }, (err) => cb && cb(err)); + } +} + +export const readFilePromise = promisify(readFile) as ( + & ((path: string | URL, opt: TextOptionsArgument) => Promise<string>) + & ((path: string | URL, opt?: BinaryOptionsArgument) => Promise<Buffer>) + & ((path: string | URL, opt?: FileOptionsArgument) => Promise<Buffer>) +); + +export function readFileSync( + path: string | URL, + opt: TextOptionsArgument, +): string; +export function readFileSync( + path: string | URL, + opt?: BinaryOptionsArgument, +): Buffer; +export function readFileSync( + path: string | URL, + opt?: FileOptionsArgument, +): string | Buffer { + path = path instanceof URL ? fromFileUrl(path) : path; + const data = Deno.readFileSync(path); + const encoding = getEncoding(opt); + if (encoding && encoding !== "binary") { + const text = maybeDecode(data, encoding); + return text; + } + const buffer = maybeDecode(data, encoding); + return buffer; +} diff --git a/ext/node/polyfills/_fs/_fs_readdir.ts b/ext/node/polyfills/_fs/_fs_readdir.ts new file mode 100644 index 000000000..f6cfae4f7 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_readdir.ts @@ -0,0 +1,142 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { + TextDecoder, + TextEncoder, +} from "internal:deno_web/08_text_encoding.js"; +import { asyncIterableToCallback } from "internal:deno_node/polyfills/_fs/_fs_watch.ts"; +import Dirent from "internal:deno_node/polyfills/_fs/_fs_dirent.ts"; +import { denoErrorToNodeError } from "internal:deno_node/polyfills/internal/errors.ts"; +import { getValidatedPath } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +function toDirent(val: Deno.DirEntry): Dirent { + return new Dirent(val); +} + +type readDirOptions = { + encoding?: string; + withFileTypes?: boolean; +}; + +type readDirCallback = (err: Error | null, files: string[]) => void; + +type readDirCallbackDirent = (err: Error | null, files: Dirent[]) => void; + +type readDirBoth = ( + ...args: [Error] | [null, string[] | Dirent[] | Array<string | Dirent>] +) => void; + +export function readdir( + path: string | Buffer | URL, + options: { withFileTypes?: false; encoding?: string }, + callback: readDirCallback, +): void; +export function readdir( + path: string | Buffer | URL, + options: { withFileTypes: true; encoding?: string }, + callback: readDirCallbackDirent, +): void; +export function readdir(path: string | URL, callback: readDirCallback): void; +export function readdir( + path: string | Buffer | URL, + optionsOrCallback: readDirOptions | readDirCallback | readDirCallbackDirent, + maybeCallback?: readDirCallback | readDirCallbackDirent, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as readDirBoth | undefined; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : null; + const result: Array<string | Dirent> = []; + path = getValidatedPath(path); + + if (!callback) throw new Error("No callback function supplied"); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + try { + asyncIterableToCallback(Deno.readDir(path.toString()), (val, done) => { + if (typeof path !== "string") return; + if (done) { + callback(null, result); + return; + } + if (options?.withFileTypes) { + result.push(toDirent(val)); + } else result.push(decode(val.name)); + }, (e) => { + callback(denoErrorToNodeError(e as Error, { syscall: "readdir" })); + }); + } catch (e) { + callback(denoErrorToNodeError(e as Error, { syscall: "readdir" })); + } +} + +function decode(str: string, encoding?: string): string { + if (!encoding) return str; + else { + const decoder = new TextDecoder(encoding); + const encoder = new TextEncoder(); + return decoder.decode(encoder.encode(str)); + } +} + +export const readdirPromise = promisify(readdir) as ( + & ((path: string | Buffer | URL, options: { + withFileTypes: true; + encoding?: string; + }) => Promise<Dirent[]>) + & ((path: string | Buffer | URL, options?: { + withFileTypes?: false; + encoding?: string; + }) => Promise<string[]>) +); + +export function readdirSync( + path: string | Buffer | URL, + options: { withFileTypes: true; encoding?: string }, +): Dirent[]; +export function readdirSync( + path: string | Buffer | URL, + options?: { withFileTypes?: false; encoding?: string }, +): string[]; +export function readdirSync( + path: string | Buffer | URL, + options?: readDirOptions, +): Array<string | Dirent> { + const result = []; + path = getValidatedPath(path); + + if (options?.encoding) { + try { + new TextDecoder(options.encoding); + } catch { + throw new Error( + `TypeError [ERR_INVALID_OPT_VALUE_ENCODING]: The value "${options.encoding}" is invalid for option "encoding"`, + ); + } + } + + try { + for (const file of Deno.readDirSync(path.toString())) { + if (options?.withFileTypes) { + result.push(toDirent(file)); + } else result.push(decode(file.name)); + } + } catch (e) { + throw denoErrorToNodeError(e as Error, { syscall: "readdir" }); + } + return result; +} diff --git a/ext/node/polyfills/_fs/_fs_readlink.ts b/ext/node/polyfills/_fs/_fs_readlink.ts new file mode 100644 index 000000000..07d1b6f6f --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_readlink.ts @@ -0,0 +1,89 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import { TextEncoder } from "internal:deno_web/08_text_encoding.js"; +import { + intoCallbackAPIWithIntercept, + MaybeEmpty, + notImplemented, +} from "internal:deno_node/polyfills/_utils.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +type ReadlinkCallback = ( + err: MaybeEmpty<Error>, + linkString: MaybeEmpty<string | Uint8Array>, +) => void; + +interface ReadlinkOptions { + encoding?: string | null; +} + +function maybeEncode( + data: string, + encoding: string | null, +): string | Uint8Array { + if (encoding === "buffer") { + return new TextEncoder().encode(data); + } + return data; +} + +function getEncoding( + optOrCallback?: ReadlinkOptions | ReadlinkCallback, +): string | null { + if (!optOrCallback || typeof optOrCallback === "function") { + return null; + } else { + if (optOrCallback.encoding) { + if ( + optOrCallback.encoding === "utf8" || + optOrCallback.encoding === "utf-8" + ) { + return "utf8"; + } else if (optOrCallback.encoding === "buffer") { + return "buffer"; + } else { + notImplemented(`fs.readlink encoding=${optOrCallback.encoding}`); + } + } + return null; + } +} + +export function readlink( + path: string | URL, + optOrCallback: ReadlinkCallback | ReadlinkOptions, + callback?: ReadlinkCallback, +) { + path = path instanceof URL ? fromFileUrl(path) : path; + + let cb: ReadlinkCallback | undefined; + if (typeof optOrCallback === "function") { + cb = optOrCallback; + } else { + cb = callback; + } + + const encoding = getEncoding(optOrCallback); + + intoCallbackAPIWithIntercept<string, Uint8Array | string>( + Deno.readLink, + (data: string): string | Uint8Array => maybeEncode(data, encoding), + cb, + path, + ); +} + +export const readlinkPromise = promisify(readlink) as ( + path: string | URL, + opt?: ReadlinkOptions, +) => Promise<string | Uint8Array>; + +export function readlinkSync( + path: string | URL, + opt?: ReadlinkOptions, +): string | Uint8Array { + path = path instanceof URL ? fromFileUrl(path) : path; + + return maybeEncode(Deno.readLinkSync(path), getEncoding(opt)); +} diff --git a/ext/node/polyfills/_fs/_fs_realpath.ts b/ext/node/polyfills/_fs/_fs_realpath.ts new file mode 100644 index 000000000..5892b2c0f --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_realpath.ts @@ -0,0 +1,35 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +type Options = { encoding: string }; +type Callback = (err: Error | null, path?: string) => void; + +export function realpath( + path: string, + options?: Options | Callback, + callback?: Callback, +) { + if (typeof options === "function") { + callback = options; + } + if (!callback) { + throw new Error("No callback function supplied"); + } + Deno.realPath(path).then( + (path) => callback!(null, path), + (err) => callback!(err), + ); +} + +realpath.native = realpath; + +export const realpathPromise = promisify(realpath) as ( + path: string, + options?: Options, +) => Promise<string>; + +export function realpathSync(path: string): string { + return Deno.realPathSync(path); +} + +realpathSync.native = realpathSync; diff --git a/ext/node/polyfills/_fs/_fs_rename.ts b/ext/node/polyfills/_fs/_fs_rename.ts new file mode 100644 index 000000000..3f8b5bd7e --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_rename.ts @@ -0,0 +1,28 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function rename( + oldPath: string | URL, + newPath: string | URL, + callback: (err?: Error) => void, +) { + oldPath = oldPath instanceof URL ? fromFileUrl(oldPath) : oldPath; + newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.rename(oldPath, newPath).then((_) => callback(), callback); +} + +export const renamePromise = promisify(rename) as ( + oldPath: string | URL, + newPath: string | URL, +) => Promise<void>; + +export function renameSync(oldPath: string | URL, newPath: string | URL) { + oldPath = oldPath instanceof URL ? fromFileUrl(oldPath) : oldPath; + newPath = newPath instanceof URL ? fromFileUrl(newPath) : newPath; + + Deno.renameSync(oldPath, newPath); +} diff --git a/ext/node/polyfills/_fs/_fs_rm.ts b/ext/node/polyfills/_fs/_fs_rm.ts new file mode 100644 index 000000000..80ba0b5f8 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_rm.ts @@ -0,0 +1,81 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + validateRmOptions, + validateRmOptionsSync, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { denoErrorToNodeError } from "internal:deno_node/polyfills/internal/errors.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +type rmOptions = { + force?: boolean; + maxRetries?: number; + recursive?: boolean; + retryDelay?: number; +}; + +type rmCallback = (err: Error | null) => void; + +export function rm(path: string | URL, callback: rmCallback): void; +export function rm( + path: string | URL, + options: rmOptions, + callback: rmCallback, +): void; +export function rm( + path: string | URL, + optionsOrCallback: rmOptions | rmCallback, + maybeCallback?: rmCallback, +) { + const callback = typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : undefined; + + if (!callback) throw new Error("No callback function supplied"); + + validateRmOptions( + path, + options, + false, + (err: Error | null, options: rmOptions) => { + if (err) { + return callback(err); + } + Deno.remove(path, { recursive: options?.recursive }) + .then((_) => callback(null), (err: unknown) => { + if (options?.force && err instanceof Deno.errors.NotFound) { + callback(null); + } else { + callback( + err instanceof Error + ? denoErrorToNodeError(err, { syscall: "rm" }) + : err, + ); + } + }); + }, + ); +} + +export const rmPromise = promisify(rm) as ( + path: string | URL, + options?: rmOptions, +) => Promise<void>; + +export function rmSync(path: string | URL, options?: rmOptions) { + options = validateRmOptionsSync(path, options, false); + try { + Deno.removeSync(path, { recursive: options?.recursive }); + } catch (err: unknown) { + if (options?.force && err instanceof Deno.errors.NotFound) { + return; + } + if (err instanceof Error) { + throw denoErrorToNodeError(err, { syscall: "stat" }); + } else { + throw err; + } + } +} diff --git a/ext/node/polyfills/_fs/_fs_rmdir.ts b/ext/node/polyfills/_fs/_fs_rmdir.ts new file mode 100644 index 000000000..ba753a743 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_rmdir.ts @@ -0,0 +1,108 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { + emitRecursiveRmdirWarning, + getValidatedPath, + validateRmdirOptions, + validateRmOptions, + validateRmOptionsSync, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { toNamespacedPath } from "internal:deno_node/polyfills/path.ts"; +import { + denoErrorToNodeError, + ERR_FS_RMDIR_ENOTDIR, +} from "internal:deno_node/polyfills/internal/errors.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +type rmdirOptions = { + maxRetries?: number; + recursive?: boolean; + retryDelay?: number; +}; + +type rmdirCallback = (err?: Error) => void; + +export function rmdir(path: string | URL, callback: rmdirCallback): void; +export function rmdir( + path: string | URL, + options: rmdirOptions, + callback: rmdirCallback, +): void; +export function rmdir( + path: string | URL, + optionsOrCallback: rmdirOptions | rmdirCallback, + maybeCallback?: rmdirCallback, +) { + path = toNamespacedPath(getValidatedPath(path) as string); + + const callback = typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : undefined; + + if (!callback) throw new Error("No callback function supplied"); + + if (options?.recursive) { + emitRecursiveRmdirWarning(); + validateRmOptions( + path, + { ...options, force: false }, + true, + (err: Error | null | false, options: rmdirOptions) => { + if (err === false) { + return callback(new ERR_FS_RMDIR_ENOTDIR(path.toString())); + } + if (err) { + return callback(err); + } + + Deno.remove(path, { recursive: options?.recursive }) + .then((_) => callback(), callback); + }, + ); + } else { + validateRmdirOptions(options); + Deno.remove(path, { recursive: options?.recursive }) + .then((_) => callback(), (err: unknown) => { + callback( + err instanceof Error + ? denoErrorToNodeError(err, { syscall: "rmdir" }) + : err, + ); + }); + } +} + +export const rmdirPromise = promisify(rmdir) as ( + path: string | Buffer | URL, + options?: rmdirOptions, +) => Promise<void>; + +export function rmdirSync(path: string | Buffer | URL, options?: rmdirOptions) { + path = getValidatedPath(path); + if (options?.recursive) { + emitRecursiveRmdirWarning(); + const optionsOrFalse: rmdirOptions | false = validateRmOptionsSync(path, { + ...options, + force: false, + }, true); + if (optionsOrFalse === false) { + throw new ERR_FS_RMDIR_ENOTDIR(path.toString()); + } + options = optionsOrFalse; + } else { + validateRmdirOptions(options); + } + + try { + Deno.removeSync(toNamespacedPath(path as string), { + recursive: options?.recursive, + }); + } catch (err: unknown) { + throw (err instanceof Error + ? denoErrorToNodeError(err, { syscall: "rmdir" }) + : err); + } +} diff --git a/ext/node/polyfills/_fs/_fs_stat.ts b/ext/node/polyfills/_fs/_fs_stat.ts new file mode 100644 index 000000000..3a006084d --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_stat.ts @@ -0,0 +1,314 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { denoErrorToNodeError } from "internal:deno_node/polyfills/internal/errors.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export type statOptions = { + bigint: boolean; + throwIfNoEntry?: boolean; +}; + +export type Stats = { + /** ID of the device containing the file. + * + * _Linux/Mac OS only._ */ + dev: number | null; + /** Inode number. + * + * _Linux/Mac OS only._ */ + ino: number | null; + /** **UNSTABLE**: Match behavior with Go on Windows for `mode`. + * + * The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. */ + mode: number | null; + /** Number of hard links pointing to this file. + * + * _Linux/Mac OS only._ */ + nlink: number | null; + /** User ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + uid: number | null; + /** Group ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + gid: number | null; + /** Device ID of this file. + * + * _Linux/Mac OS only._ */ + rdev: number | null; + /** The size of the file, in bytes. */ + size: number; + /** Blocksize for filesystem I/O. + * + * _Linux/Mac OS only._ */ + blksize: number | null; + /** Number of blocks allocated to the file, in 512-byte units. + * + * _Linux/Mac OS only._ */ + blocks: number | null; + /** The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ + mtime: Date | null; + /** The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ + atime: Date | null; + /** The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; + /** change time */ + ctime: Date | null; + /** atime in milliseconds */ + atimeMs: number | null; + /** atime in milliseconds */ + mtimeMs: number | null; + /** atime in milliseconds */ + ctimeMs: number | null; + /** atime in milliseconds */ + birthtimeMs: number | null; + isBlockDevice: () => boolean; + isCharacterDevice: () => boolean; + isDirectory: () => boolean; + isFIFO: () => boolean; + isFile: () => boolean; + isSocket: () => boolean; + isSymbolicLink: () => boolean; +}; + +export type BigIntStats = { + /** ID of the device containing the file. + * + * _Linux/Mac OS only._ */ + dev: bigint | null; + /** Inode number. + * + * _Linux/Mac OS only._ */ + ino: bigint | null; + /** **UNSTABLE**: Match behavior with Go on Windows for `mode`. + * + * The underlying raw `st_mode` bits that contain the standard Unix + * permissions for this file/directory. */ + mode: bigint | null; + /** Number of hard links pointing to this file. + * + * _Linux/Mac OS only._ */ + nlink: bigint | null; + /** User ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + uid: bigint | null; + /** Group ID of the owner of this file. + * + * _Linux/Mac OS only._ */ + gid: bigint | null; + /** Device ID of this file. + * + * _Linux/Mac OS only._ */ + rdev: bigint | null; + /** The size of the file, in bytes. */ + size: bigint; + /** Blocksize for filesystem I/O. + * + * _Linux/Mac OS only._ */ + blksize: bigint | null; + /** Number of blocks allocated to the file, in 512-byte units. + * + * _Linux/Mac OS only._ */ + blocks: bigint | null; + /** The last modification time of the file. This corresponds to the `mtime` + * field from `stat` on Linux/Mac OS and `ftLastWriteTime` on Windows. This + * may not be available on all platforms. */ + mtime: Date | null; + /** The last access time of the file. This corresponds to the `atime` + * field from `stat` on Unix and `ftLastAccessTime` on Windows. This may not + * be available on all platforms. */ + atime: Date | null; + /** The creation time of the file. This corresponds to the `birthtime` + * field from `stat` on Mac/BSD and `ftCreationTime` on Windows. This may + * not be available on all platforms. */ + birthtime: Date | null; + /** change time */ + ctime: Date | null; + /** atime in milliseconds */ + atimeMs: bigint | null; + /** atime in milliseconds */ + mtimeMs: bigint | null; + /** atime in milliseconds */ + ctimeMs: bigint | null; + /** atime in nanoseconds */ + birthtimeMs: bigint | null; + /** atime in nanoseconds */ + atimeNs: bigint | null; + /** atime in nanoseconds */ + mtimeNs: bigint | null; + /** atime in nanoseconds */ + ctimeNs: bigint | null; + /** atime in nanoseconds */ + birthtimeNs: bigint | null; + isBlockDevice: () => boolean; + isCharacterDevice: () => boolean; + isDirectory: () => boolean; + isFIFO: () => boolean; + isFile: () => boolean; + isSocket: () => boolean; + isSymbolicLink: () => boolean; +}; + +export function convertFileInfoToStats(origin: Deno.FileInfo): Stats { + return { + dev: origin.dev, + ino: origin.ino, + mode: origin.mode, + nlink: origin.nlink, + uid: origin.uid, + gid: origin.gid, + rdev: origin.rdev, + size: origin.size, + blksize: origin.blksize, + blocks: origin.blocks, + mtime: origin.mtime, + atime: origin.atime, + birthtime: origin.birthtime, + mtimeMs: origin.mtime?.getTime() || null, + atimeMs: origin.atime?.getTime() || null, + birthtimeMs: origin.birthtime?.getTime() || null, + isFile: () => origin.isFile, + isDirectory: () => origin.isDirectory, + isSymbolicLink: () => origin.isSymlink, + // not sure about those + isBlockDevice: () => false, + isFIFO: () => false, + isCharacterDevice: () => false, + isSocket: () => false, + ctime: origin.mtime, + ctimeMs: origin.mtime?.getTime() || null, + }; +} + +function toBigInt(number?: number | null) { + if (number === null || number === undefined) return null; + return BigInt(number); +} + +export function convertFileInfoToBigIntStats( + origin: Deno.FileInfo, +): BigIntStats { + return { + dev: toBigInt(origin.dev), + ino: toBigInt(origin.ino), + mode: toBigInt(origin.mode), + nlink: toBigInt(origin.nlink), + uid: toBigInt(origin.uid), + gid: toBigInt(origin.gid), + rdev: toBigInt(origin.rdev), + size: toBigInt(origin.size) || 0n, + blksize: toBigInt(origin.blksize), + blocks: toBigInt(origin.blocks), + mtime: origin.mtime, + atime: origin.atime, + birthtime: origin.birthtime, + mtimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null, + atimeMs: origin.atime ? BigInt(origin.atime.getTime()) : null, + birthtimeMs: origin.birthtime ? BigInt(origin.birthtime.getTime()) : null, + mtimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null, + atimeNs: origin.atime ? BigInt(origin.atime.getTime()) * 1000000n : null, + birthtimeNs: origin.birthtime + ? BigInt(origin.birthtime.getTime()) * 1000000n + : null, + isFile: () => origin.isFile, + isDirectory: () => origin.isDirectory, + isSymbolicLink: () => origin.isSymlink, + // not sure about those + isBlockDevice: () => false, + isFIFO: () => false, + isCharacterDevice: () => false, + isSocket: () => false, + ctime: origin.mtime, + ctimeMs: origin.mtime ? BigInt(origin.mtime.getTime()) : null, + ctimeNs: origin.mtime ? BigInt(origin.mtime.getTime()) * 1000000n : null, + }; +} + +// shortcut for Convert File Info to Stats or BigIntStats +export function CFISBIS(fileInfo: Deno.FileInfo, bigInt: boolean) { + if (bigInt) return convertFileInfoToBigIntStats(fileInfo); + return convertFileInfoToStats(fileInfo); +} + +export type statCallbackBigInt = (err: Error | null, stat: BigIntStats) => void; + +export type statCallback = (err: Error | null, stat: Stats) => void; + +export function stat(path: string | URL, callback: statCallback): void; +export function stat( + path: string | URL, + options: { bigint: false }, + callback: statCallback, +): void; +export function stat( + path: string | URL, + options: { bigint: true }, + callback: statCallbackBigInt, +): void; +export function stat( + path: string | URL, + optionsOrCallback: statCallback | statCallbackBigInt | statOptions, + maybeCallback?: statCallback | statCallbackBigInt, +) { + const callback = + (typeof optionsOrCallback === "function" + ? optionsOrCallback + : maybeCallback) as ( + ...args: [Error] | [null, BigIntStats | Stats] + ) => void; + const options = typeof optionsOrCallback === "object" + ? optionsOrCallback + : { bigint: false }; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.stat(path).then( + (stat) => callback(null, CFISBIS(stat, options.bigint)), + (err) => callback(denoErrorToNodeError(err, { syscall: "stat" })), + ); +} + +export const statPromise = promisify(stat) as ( + & ((path: string | URL) => Promise<Stats>) + & ((path: string | URL, options: { bigint: false }) => Promise<Stats>) + & ((path: string | URL, options: { bigint: true }) => Promise<BigIntStats>) +); + +export function statSync(path: string | URL): Stats; +export function statSync( + path: string | URL, + options: { bigint: false; throwIfNoEntry?: boolean }, +): Stats; +export function statSync( + path: string | URL, + options: { bigint: true; throwIfNoEntry?: boolean }, +): BigIntStats; +export function statSync( + path: string | URL, + options: statOptions = { bigint: false, throwIfNoEntry: true }, +): Stats | BigIntStats | undefined { + try { + const origin = Deno.statSync(path); + return CFISBIS(origin, options.bigint); + } catch (err) { + if ( + options?.throwIfNoEntry === false && + err instanceof Deno.errors.NotFound + ) { + return; + } + if (err instanceof Error) { + throw denoErrorToNodeError(err, { syscall: "stat" }); + } else { + throw err; + } + } +} diff --git a/ext/node/polyfills/_fs/_fs_symlink.ts b/ext/node/polyfills/_fs/_fs_symlink.ts new file mode 100644 index 000000000..c8652885f --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_symlink.ts @@ -0,0 +1,46 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +type SymlinkType = "file" | "dir"; + +export function symlink( + target: string | URL, + path: string | URL, + typeOrCallback: SymlinkType | CallbackWithError, + maybeCallback?: CallbackWithError, +) { + target = target instanceof URL ? fromFileUrl(target) : target; + path = path instanceof URL ? fromFileUrl(path) : path; + + const type: SymlinkType = typeof typeOrCallback === "string" + ? typeOrCallback + : "file"; + + const callback: CallbackWithError = typeof typeOrCallback === "function" + ? typeOrCallback + : (maybeCallback as CallbackWithError); + + if (!callback) throw new Error("No callback function supplied"); + + Deno.symlink(target, path, { type }).then(() => callback(null), callback); +} + +export const symlinkPromise = promisify(symlink) as ( + target: string | URL, + path: string | URL, + type?: SymlinkType, +) => Promise<void>; + +export function symlinkSync( + target: string | URL, + path: string | URL, + type?: SymlinkType, +) { + target = target instanceof URL ? fromFileUrl(target) : target; + path = path instanceof URL ? fromFileUrl(path) : path; + type = type || "file"; + + Deno.symlinkSync(target, path, { type }); +} diff --git a/ext/node/polyfills/_fs/_fs_truncate.ts b/ext/node/polyfills/_fs/_fs_truncate.ts new file mode 100644 index 000000000..105555abc --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_truncate.ts @@ -0,0 +1,33 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function truncate( + path: string | URL, + lenOrCallback: number | CallbackWithError, + maybeCallback?: CallbackWithError, +) { + path = path instanceof URL ? fromFileUrl(path) : path; + const len: number | undefined = typeof lenOrCallback === "number" + ? lenOrCallback + : undefined; + const callback: CallbackWithError = typeof lenOrCallback === "function" + ? lenOrCallback + : maybeCallback as CallbackWithError; + + if (!callback) throw new Error("No callback function supplied"); + + Deno.truncate(path, len).then(() => callback(null), callback); +} + +export const truncatePromise = promisify(truncate) as ( + path: string | URL, + len?: number, +) => Promise<void>; + +export function truncateSync(path: string | URL, len?: number) { + path = path instanceof URL ? fromFileUrl(path) : path; + + Deno.truncateSync(path, len); +} diff --git a/ext/node/polyfills/_fs/_fs_unlink.ts b/ext/node/polyfills/_fs/_fs_unlink.ts new file mode 100644 index 000000000..ed43bb1b3 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_unlink.ts @@ -0,0 +1,15 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +export function unlink(path: string | URL, callback: (err?: Error) => void) { + if (!callback) throw new Error("No callback function supplied"); + Deno.remove(path).then((_) => callback(), callback); +} + +export const unlinkPromise = promisify(unlink) as ( + path: string | URL, +) => Promise<void>; + +export function unlinkSync(path: string | URL) { + Deno.removeSync(path); +} diff --git a/ext/node/polyfills/_fs/_fs_utimes.ts b/ext/node/polyfills/_fs/_fs_utimes.ts new file mode 100644 index 000000000..7423a1060 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_utimes.ts @@ -0,0 +1,61 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +import type { CallbackWithError } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +function getValidTime( + time: number | string | Date, + name: string, +): number | Date { + if (typeof time === "string") { + time = Number(time); + } + + if ( + typeof time === "number" && + (Number.isNaN(time) || !Number.isFinite(time)) + ) { + throw new Deno.errors.InvalidData( + `invalid ${name}, must not be infinity or NaN`, + ); + } + + return time; +} + +export function utimes( + path: string | URL, + atime: number | string | Date, + mtime: number | string | Date, + callback: CallbackWithError, +) { + path = path instanceof URL ? fromFileUrl(path) : path; + + if (!callback) { + throw new Deno.errors.InvalidData("No callback function supplied"); + } + + atime = getValidTime(atime, "atime"); + mtime = getValidTime(mtime, "mtime"); + + Deno.utime(path, atime, mtime).then(() => callback(null), callback); +} + +export const utimesPromise = promisify(utimes) as ( + path: string | URL, + atime: number | string | Date, + mtime: number | string | Date, +) => Promise<void>; + +export function utimesSync( + path: string | URL, + atime: number | string | Date, + mtime: number | string | Date, +) { + path = path instanceof URL ? fromFileUrl(path) : path; + atime = getValidTime(atime, "atime"); + mtime = getValidTime(mtime, "mtime"); + + Deno.utimeSync(path, atime, mtime); +} diff --git a/ext/node/polyfills/_fs/_fs_watch.ts b/ext/node/polyfills/_fs/_fs_watch.ts new file mode 100644 index 000000000..79f226126 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_watch.ts @@ -0,0 +1,346 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { basename } from "internal:deno_node/polyfills/path.ts"; +import { EventEmitter } from "internal:deno_node/polyfills/events.ts"; +import { notImplemented } from "internal:deno_node/polyfills/_utils.ts"; +import { promisify } from "internal:deno_node/polyfills/util.ts"; +import { getValidatedPath } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { validateFunction } from "internal:deno_node/polyfills/internal/validators.mjs"; +import { stat, Stats } from "internal:deno_node/polyfills/_fs/_fs_stat.ts"; +import { Stats as StatsClass } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { delay } from "internal:deno_node/polyfills/_util/async.ts"; + +const statPromisified = promisify(stat); +const statAsync = async (filename: string): Promise<Stats | null> => { + try { + return await statPromisified(filename); + } catch { + return emptyStats; + } +}; +const emptyStats = new StatsClass( + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + Date.UTC(1970, 0, 1, 0, 0, 0), + Date.UTC(1970, 0, 1, 0, 0, 0), + Date.UTC(1970, 0, 1, 0, 0, 0), + Date.UTC(1970, 0, 1, 0, 0, 0), +) as unknown as Stats; + +export function asyncIterableIteratorToCallback<T>( + iterator: AsyncIterableIterator<T>, + callback: (val: T, done?: boolean) => void, +) { + function next() { + iterator.next().then((obj) => { + if (obj.done) { + callback(obj.value, true); + return; + } + callback(obj.value); + next(); + }); + } + next(); +} + +export function asyncIterableToCallback<T>( + iter: AsyncIterable<T>, + callback: (val: T, done?: boolean) => void, + errCallback: (e: unknown) => void, +) { + const iterator = iter[Symbol.asyncIterator](); + function next() { + iterator.next().then((obj) => { + if (obj.done) { + callback(obj.value, true); + return; + } + callback(obj.value); + next(); + }, errCallback); + } + next(); +} + +type watchOptions = { + persistent?: boolean; + recursive?: boolean; + encoding?: string; +}; + +type watchListener = (eventType: string, filename: string) => void; + +export function watch( + filename: string | URL, + options: watchOptions, + listener: watchListener, +): FSWatcher; +export function watch( + filename: string | URL, + listener: watchListener, +): FSWatcher; +export function watch( + filename: string | URL, + options: watchOptions, +): FSWatcher; +export function watch(filename: string | URL): FSWatcher; +export function watch( + filename: string | URL, + optionsOrListener?: watchOptions | watchListener, + optionsOrListener2?: watchOptions | watchListener, +) { + const listener = typeof optionsOrListener === "function" + ? optionsOrListener + : typeof optionsOrListener2 === "function" + ? optionsOrListener2 + : undefined; + const options = typeof optionsOrListener === "object" + ? optionsOrListener + : typeof optionsOrListener2 === "object" + ? optionsOrListener2 + : undefined; + + const watchPath = getValidatedPath(filename).toString(); + + let iterator: Deno.FsWatcher; + // Start the actual watcher a few msec later to avoid race condition + // error in test case in compat test case + // (parallel/test-fs-watch.js, parallel/test-fs-watchfile.js) + const timer = setTimeout(() => { + iterator = Deno.watchFs(watchPath, { + recursive: options?.recursive || false, + }); + + asyncIterableToCallback<Deno.FsEvent>(iterator, (val, done) => { + if (done) return; + fsWatcher.emit( + "change", + convertDenoFsEventToNodeFsEvent(val.kind), + basename(val.paths[0]), + ); + }, (e) => { + fsWatcher.emit("error", e); + }); + }, 5); + + const fsWatcher = new FSWatcher(() => { + clearTimeout(timer); + try { + iterator?.close(); + } catch (e) { + if (e instanceof Deno.errors.BadResource) { + // already closed + return; + } + throw e; + } + }); + + if (listener) { + fsWatcher.on("change", listener.bind({ _handle: fsWatcher })); + } + + return fsWatcher; +} + +export const watchPromise = promisify(watch) as ( + & (( + filename: string | URL, + options: watchOptions, + listener: watchListener, + ) => Promise<FSWatcher>) + & (( + filename: string | URL, + listener: watchListener, + ) => Promise<FSWatcher>) + & (( + filename: string | URL, + options: watchOptions, + ) => Promise<FSWatcher>) + & ((filename: string | URL) => Promise<FSWatcher>) +); + +type WatchFileListener = (curr: Stats, prev: Stats) => void; +type WatchFileOptions = { + bigint?: boolean; + persistent?: boolean; + interval?: number; +}; + +export function watchFile( + filename: string | Buffer | URL, + listener: WatchFileListener, +): StatWatcher; +export function watchFile( + filename: string | Buffer | URL, + options: WatchFileOptions, + listener: WatchFileListener, +): StatWatcher; +export function watchFile( + filename: string | Buffer | URL, + listenerOrOptions: WatchFileListener | WatchFileOptions, + listener?: WatchFileListener, +): StatWatcher { + const watchPath = getValidatedPath(filename).toString(); + const handler = typeof listenerOrOptions === "function" + ? listenerOrOptions + : listener!; + validateFunction(handler, "listener"); + const { + bigint = false, + persistent = true, + interval = 5007, + } = typeof listenerOrOptions === "object" ? listenerOrOptions : {}; + + let stat = statWatchers.get(watchPath); + if (stat === undefined) { + stat = new StatWatcher(bigint); + stat[kFSStatWatcherStart](watchPath, persistent, interval); + statWatchers.set(watchPath, stat); + } + + stat.addListener("change", listener!); + return stat; +} + +export function unwatchFile( + filename: string | Buffer | URL, + listener?: WatchFileListener, +) { + const watchPath = getValidatedPath(filename).toString(); + const stat = statWatchers.get(watchPath); + + if (!stat) { + return; + } + + if (typeof listener === "function") { + const beforeListenerCount = stat.listenerCount("change"); + stat.removeListener("change", listener); + if (stat.listenerCount("change") < beforeListenerCount) { + stat[kFSStatWatcherAddOrCleanRef]("clean"); + } + } else { + stat.removeAllListeners("change"); + stat[kFSStatWatcherAddOrCleanRef]("cleanAll"); + } + + if (stat.listenerCount("change") === 0) { + stat.stop(); + statWatchers.delete(watchPath); + } +} + +const statWatchers = new Map<string, StatWatcher>(); + +const kFSStatWatcherStart = Symbol("kFSStatWatcherStart"); +const kFSStatWatcherAddOrCleanRef = Symbol("kFSStatWatcherAddOrCleanRef"); + +class StatWatcher extends EventEmitter { + #bigint: boolean; + #refCount = 0; + #abortController = new AbortController(); + constructor(bigint: boolean) { + super(); + this.#bigint = bigint; + } + [kFSStatWatcherStart]( + filename: string, + persistent: boolean, + interval: number, + ) { + if (persistent) { + this.#refCount++; + } + + (async () => { + let prev = await statAsync(filename); + + if (prev === emptyStats) { + this.emit("change", prev, prev); + } + + try { + while (true) { + await delay(interval, { signal: this.#abortController.signal }); + const curr = await statAsync(filename); + if (curr?.mtime !== prev?.mtime) { + this.emit("change", curr, prev); + prev = curr; + } + } + } catch (e) { + if (e instanceof DOMException && e.name === "AbortError") { + return; + } + this.emit("error", e); + } + })(); + } + [kFSStatWatcherAddOrCleanRef](addOrClean: "add" | "clean" | "cleanAll") { + if (addOrClean === "add") { + this.#refCount++; + } else if (addOrClean === "clean") { + this.#refCount--; + } else { + this.#refCount = 0; + } + } + stop() { + if (this.#abortController.signal.aborted) { + return; + } + this.#abortController.abort(); + this.emit("stop"); + } + ref() { + notImplemented("FSWatcher.ref() is not implemented"); + } + unref() { + notImplemented("FSWatcher.unref() is not implemented"); + } +} + +class FSWatcher extends EventEmitter { + #closer: () => void; + #closed = false; + constructor(closer: () => void) { + super(); + this.#closer = closer; + } + close() { + if (this.#closed) { + return; + } + this.#closed = true; + this.emit("close"); + this.#closer(); + } + ref() { + notImplemented("FSWatcher.ref() is not implemented"); + } + unref() { + notImplemented("FSWatcher.unref() is not implemented"); + } +} + +type NodeFsEventType = "rename" | "change"; + +function convertDenoFsEventToNodeFsEvent( + kind: Deno.FsEvent["kind"], +): NodeFsEventType { + if (kind === "create" || kind === "remove") { + return "rename"; + } else { + return "change"; + } +} diff --git a/ext/node/polyfills/_fs/_fs_write.d.ts b/ext/node/polyfills/_fs/_fs_write.d.ts new file mode 100644 index 000000000..eb6dbcc95 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_write.d.ts @@ -0,0 +1,207 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Forked from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d9df51e34526f48bef4e2546a006157b391ad96c/types/node/fs.d.ts + +import { + BufferEncoding, + ErrnoException, +} from "internal:deno_node/polyfills/_global.d.ts"; + +/** + * Write `buffer` to the file specified by `fd`. If `buffer` is a normal object, it + * must have an own `toString` function property. + * + * `offset` determines the part of the buffer to be written, and `length` is + * an integer specifying the number of bytes to write. + * + * `position` refers to the offset from the beginning of the file where this data + * should be written. If `typeof position !== 'number'`, the data will be written + * at the current position. See [`pwrite(2)`](http://man7.org/linux/man-pages/man2/pwrite.2.html). + * + * The callback will be given three arguments `(err, bytesWritten, buffer)` where`bytesWritten` specifies how many _bytes_ were written from `buffer`. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a promise for an `Object` with `bytesWritten` and `buffer` properties. + * + * It is unsafe to use `fs.write()` multiple times on the same file without waiting + * for the callback. For this scenario, {@link createWriteStream} is + * recommended. + * + * On Linux, positional writes don't work when the file is opened in append mode. + * The kernel ignores the position argument and always appends the data to + * the end of the file. + * @since v0.0.2 + */ +export function write<TBuffer extends ArrayBufferView>( + fd: number, + buffer: TBuffer, + offset: number | undefined | null, + length: number | undefined | null, + position: number | undefined | null, + callback: ( + err: ErrnoException | null, + written: number, + buffer: TBuffer, + ) => void, +): void; +/** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param offset The part of the buffer to be written. If not supplied, defaults to `0`. + * @param length The number of bytes to write. If not supplied, defaults to `buffer.length - offset`. + */ +export function write<TBuffer extends ArrayBufferView>( + fd: number, + buffer: TBuffer, + offset: number | undefined | null, + length: number | undefined | null, + callback: ( + err: ErrnoException | null, + written: number, + buffer: TBuffer, + ) => void, +): void; +/** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param offset The part of the buffer to be written. If not supplied, defaults to `0`. + */ +export function write<TBuffer extends ArrayBufferView>( + fd: number, + buffer: TBuffer, + offset: number | undefined | null, + callback: ( + err: ErrnoException | null, + written: number, + buffer: TBuffer, + ) => void, +): void; +/** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + */ +export function write<TBuffer extends ArrayBufferView>( + fd: number, + buffer: TBuffer, + callback: ( + err: ErrnoException | null, + written: number, + buffer: TBuffer, + ) => void, +): void; +/** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + * @param encoding The expected string encoding. + */ +export function write( + fd: number, + string: string, + position: number | undefined | null, + encoding: BufferEncoding | undefined | null, + callback: (err: ErrnoException | null, written: number, str: string) => void, +): void; +/** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + */ +export function write( + fd: number, + string: string, + position: number | undefined | null, + callback: (err: ErrnoException | null, written: number, str: string) => void, +): void; +/** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + */ +export function write( + fd: number, + string: string, + callback: (err: ErrnoException | null, written: number, str: string) => void, +): void; +export namespace write { + /** + * Asynchronously writes `buffer` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param offset The part of the buffer to be written. If not supplied, defaults to `0`. + * @param length The number of bytes to write. If not supplied, defaults to `buffer.length - offset`. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + */ + function __promisify__<TBuffer extends ArrayBufferView>( + fd: number, + buffer?: TBuffer, + offset?: number, + length?: number, + position?: number | null, + ): Promise<{ + bytesWritten: number; + buffer: TBuffer; + }>; + /** + * Asynchronously writes `string` to the file referenced by the supplied file descriptor. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + * @param encoding The expected string encoding. + */ + function __promisify__( + fd: number, + string: string, + position?: number | null, + encoding?: BufferEncoding | null, + ): Promise<{ + bytesWritten: number; + buffer: string; + }>; +} +/** + * If `buffer` is a plain object, it must have an own (not inherited) `toString`function property. + * + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link write}. + * @since v0.1.21 + * @return The number of bytes written. + */ +export function writeSync( + fd: number, + buffer: ArrayBufferView, + offset?: number | null, + length?: number | null, + position?: number | null, +): number; +/** + * Synchronously writes `string` to the file referenced by the supplied file descriptor, returning the number of bytes written. + * @param fd A file descriptor. + * @param string A string to write. + * @param position The offset from the beginning of the file where this data should be written. If not supplied, defaults to the current position. + * @param encoding The expected string encoding. + */ +export function writeSync( + fd: number, + string: string, + position?: number | null, + encoding?: BufferEncoding | null, +): number; +export type ReadPosition = number | bigint; +/** + * Read data from the file specified by `fd`. + * + * The callback is given the three arguments, `(err, bytesRead, buffer)`. + * + * If the file is not modified concurrently, the end-of-file is reached when the + * number of bytes read is zero. + * + * If this method is invoked as its `util.promisify()` ed version, it returns + * a promise for an `Object` with `bytesRead` and `buffer` properties. + * @since v0.0.2 + * @param buffer The buffer that the data will be written to. + * @param offset The position in `buffer` to write the data to. + * @param length The number of bytes to read. + * @param position Specifies where to begin reading from in the file. If `position` is `null` or `-1 `, data will be read from the current file position, and the file position will be updated. If + * `position` is an integer, the file position will be unchanged. + */ diff --git a/ext/node/polyfills/_fs/_fs_write.mjs b/ext/node/polyfills/_fs/_fs_write.mjs new file mode 100644 index 000000000..d44a72921 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_write.mjs @@ -0,0 +1,132 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { validateEncoding, validateInteger } from "internal:deno_node/polyfills/internal/validators.mjs"; +import { + getValidatedFd, + showStringCoercionDeprecation, + validateOffsetLengthWrite, + validateStringAfterArrayBufferView, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { isArrayBufferView } from "internal:deno_node/polyfills/internal/util/types.ts"; +import { maybeCallback } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; + +export function writeSync(fd, buffer, offset, length, position) { + fd = getValidatedFd(fd); + + const innerWriteSync = (fd, buffer, offset, length, position) => { + if (buffer instanceof DataView) { + buffer = new Uint8Array(buffer.buffer); + } + if (typeof position === "number") { + Deno.seekSync(fd, position, Deno.SeekMode.Start); + } + let currentOffset = offset; + const end = offset + length; + while (currentOffset - offset < length) { + currentOffset += Deno.writeSync(fd, buffer.subarray(currentOffset, end)); + } + return currentOffset - offset; + }; + + if (isArrayBufferView(buffer)) { + if (position === undefined) { + position = null; + } + if (offset == null) { + offset = 0; + } else { + validateInteger(offset, "offset", 0); + } + if (typeof length !== "number") { + length = buffer.byteLength - offset; + } + validateOffsetLengthWrite(offset, length, buffer.byteLength); + return innerWriteSync(fd, buffer, offset, length, position); + } + validateStringAfterArrayBufferView(buffer, "buffer"); + validateEncoding(buffer, length); + if (offset === undefined) { + offset = null; + } + buffer = Buffer.from(buffer, length); + return innerWriteSync(fd, buffer, 0, buffer.length, position); +} + +/** Writes the buffer to the file of the given descriptor. + * https://nodejs.org/api/fs.html#fswritefd-buffer-offset-length-position-callback + * https://github.com/nodejs/node/blob/42ad4137aadda69c51e1df48eee9bc2e5cebca5c/lib/fs.js#L797 + */ +export function write(fd, buffer, offset, length, position, callback) { + fd = getValidatedFd(fd); + + const innerWrite = async (fd, buffer, offset, length, position) => { + if (buffer instanceof DataView) { + buffer = new Uint8Array(buffer.buffer); + } + if (typeof position === "number") { + await Deno.seek(fd, position, Deno.SeekMode.Start); + } + let currentOffset = offset; + const end = offset + length; + while (currentOffset - offset < length) { + currentOffset += await Deno.write( + fd, + buffer.subarray(currentOffset, end), + ); + } + return currentOffset - offset; + }; + + if (isArrayBufferView(buffer)) { + callback = maybeCallback(callback || position || length || offset); + if (offset == null || typeof offset === "function") { + offset = 0; + } else { + validateInteger(offset, "offset", 0); + } + if (typeof length !== "number") { + length = buffer.byteLength - offset; + } + if (typeof position !== "number") { + position = null; + } + validateOffsetLengthWrite(offset, length, buffer.byteLength); + innerWrite(fd, buffer, offset, length, position).then( + (nwritten) => { + callback(null, nwritten, buffer); + }, + (err) => callback(err), + ); + return; + } + + // Here the call signature is + // `fs.write(fd, string[, position[, encoding]], callback)` + + validateStringAfterArrayBufferView(buffer, "buffer"); + if (typeof buffer !== "string") { + showStringCoercionDeprecation(); + } + + if (typeof position !== "function") { + if (typeof offset === "function") { + position = offset; + offset = null; + } else { + position = length; + } + length = "utf-8"; + } + + const str = String(buffer); + validateEncoding(str, length); + callback = maybeCallback(position); + buffer = Buffer.from(str, length); + innerWrite(fd, buffer, 0, buffer.length, offset, callback).then( + (nwritten) => { + callback(null, nwritten, buffer); + }, + (err) => callback(err), + ); +} diff --git a/ext/node/polyfills/_fs/_fs_writeFile.ts b/ext/node/polyfills/_fs/_fs_writeFile.ts new file mode 100644 index 000000000..3cad5f947 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_writeFile.ts @@ -0,0 +1,193 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +import { Encodings } from "internal:deno_node/polyfills/_utils.ts"; +import { fromFileUrl } from "internal:deno_node/polyfills/path.ts"; +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { + CallbackWithError, + checkEncoding, + getEncoding, + getOpenOptions, + isFileOptions, + WriteFileOptions, +} from "internal:deno_node/polyfills/_fs/_fs_common.ts"; +import { isWindows } from "internal:deno_node/polyfills/_util/os.ts"; +import { + AbortError, + denoErrorToNodeError, +} from "internal:deno_node/polyfills/internal/errors.ts"; +import { + showStringCoercionDeprecation, + validateStringAfterArrayBufferView, +} from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { promisify } from "internal:deno_node/polyfills/internal/util.mjs"; + +interface Writer { + write(p: Uint8Array): Promise<number>; +} + +export function writeFile( + pathOrRid: string | number | URL, + // deno-lint-ignore ban-types + data: string | Uint8Array | Object, + optOrCallback: Encodings | CallbackWithError | WriteFileOptions | undefined, + callback?: CallbackWithError, +) { + const callbackFn: CallbackWithError | undefined = + optOrCallback instanceof Function ? optOrCallback : callback; + const options: Encodings | WriteFileOptions | undefined = + optOrCallback instanceof Function ? undefined : optOrCallback; + + if (!callbackFn) { + throw new TypeError("Callback must be a function."); + } + + pathOrRid = pathOrRid instanceof URL ? fromFileUrl(pathOrRid) : pathOrRid; + + const flag: string | undefined = isFileOptions(options) + ? options.flag + : undefined; + + const mode: number | undefined = isFileOptions(options) + ? options.mode + : undefined; + + const encoding = checkEncoding(getEncoding(options)) || "utf8"; + const openOptions = getOpenOptions(flag || "w"); + + if (!ArrayBuffer.isView(data)) { + validateStringAfterArrayBufferView(data, "data"); + if (typeof data !== "string") { + showStringCoercionDeprecation(); + } + data = Buffer.from(String(data), encoding); + } + + const isRid = typeof pathOrRid === "number"; + let file; + + let error: Error | null = null; + (async () => { + try { + file = isRid + ? new Deno.FsFile(pathOrRid as number) + : await Deno.open(pathOrRid as string, openOptions); + + // ignore mode because it's not supported on windows + // TODO(@bartlomieju): remove `!isWindows` when `Deno.chmod` is supported + if (!isRid && mode && !isWindows) { + await Deno.chmod(pathOrRid as string, mode); + } + + const signal: AbortSignal | undefined = isFileOptions(options) + ? options.signal + : undefined; + await writeAll(file, data as Uint8Array, { signal }); + } catch (e) { + error = e instanceof Error + ? denoErrorToNodeError(e, { syscall: "write" }) + : new Error("[non-error thrown]"); + } finally { + // Make sure to close resource + if (!isRid && file) file.close(); + callbackFn(error); + } + })(); +} + +export const writeFilePromise = promisify(writeFile) as ( + pathOrRid: string | number | URL, + // deno-lint-ignore ban-types + data: string | Uint8Array | Object, + options?: Encodings | WriteFileOptions, +) => Promise<void>; + +export function writeFileSync( + pathOrRid: string | number | URL, + // deno-lint-ignore ban-types + data: string | Uint8Array | Object, + options?: Encodings | WriteFileOptions, +) { + pathOrRid = pathOrRid instanceof URL ? fromFileUrl(pathOrRid) : pathOrRid; + + const flag: string | undefined = isFileOptions(options) + ? options.flag + : undefined; + + const mode: number | undefined = isFileOptions(options) + ? options.mode + : undefined; + + const encoding = checkEncoding(getEncoding(options)) || "utf8"; + const openOptions = getOpenOptions(flag || "w"); + + if (!ArrayBuffer.isView(data)) { + validateStringAfterArrayBufferView(data, "data"); + if (typeof data !== "string") { + showStringCoercionDeprecation(); + } + data = Buffer.from(String(data), encoding); + } + + const isRid = typeof pathOrRid === "number"; + let file; + + let error: Error | null = null; + try { + file = isRid + ? new Deno.FsFile(pathOrRid as number) + : Deno.openSync(pathOrRid as string, openOptions); + + // ignore mode because it's not supported on windows + // TODO(@bartlomieju): remove `!isWindows` when `Deno.chmod` is supported + if (!isRid && mode && !isWindows) { + Deno.chmodSync(pathOrRid as string, mode); + } + + // TODO(crowlKats): duplicate from runtime/js/13_buffer.js + let nwritten = 0; + while (nwritten < (data as Uint8Array).length) { + nwritten += file.writeSync((data as Uint8Array).subarray(nwritten)); + } + } catch (e) { + error = e instanceof Error + ? denoErrorToNodeError(e, { syscall: "write" }) + : new Error("[non-error thrown]"); + } finally { + // Make sure to close resource + if (!isRid && file) file.close(); + } + + if (error) throw error; +} + +interface WriteAllOptions { + offset?: number; + length?: number; + signal?: AbortSignal; +} +async function writeAll( + w: Writer, + arr: Uint8Array, + options: WriteAllOptions = {}, +) { + const { offset = 0, length = arr.byteLength, signal } = options; + checkAborted(signal); + + const written = await w.write(arr.subarray(offset, offset + length)); + + if (written === length) { + return; + } + + await writeAll(w, arr, { + offset: offset + written, + length: length - written, + signal, + }); +} + +function checkAborted(signal?: AbortSignal) { + if (signal?.aborted) { + throw new AbortError(); + } +} diff --git a/ext/node/polyfills/_fs/_fs_writev.d.ts b/ext/node/polyfills/_fs/_fs_writev.d.ts new file mode 100644 index 000000000..d828bf677 --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_writev.d.ts @@ -0,0 +1,65 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Forked from https://github.com/DefinitelyTyped/DefinitelyTyped/blob/d9df51e34526f48bef4e2546a006157b391ad96c/types/node/fs.d.ts + +import { ErrnoException } from "internal:deno_node/polyfills/_global.d.ts"; + +/** + * Write an array of `ArrayBufferView`s to the file specified by `fd` using`writev()`. + * + * `position` is the offset from the beginning of the file where this data + * should be written. If `typeof position !== 'number'`, the data will be written + * at the current position. + * + * The callback will be given three arguments: `err`, `bytesWritten`, and`buffers`. `bytesWritten` is how many bytes were written from `buffers`. + * + * If this method is `util.promisify()` ed, it returns a promise for an`Object` with `bytesWritten` and `buffers` properties. + * + * It is unsafe to use `fs.writev()` multiple times on the same file without + * waiting for the callback. For this scenario, use {@link createWriteStream}. + * + * On Linux, positional writes don't work when the file is opened in append mode. + * The kernel ignores the position argument and always appends the data to + * the end of the file. + * @since v12.9.0 + */ +export function writev( + fd: number, + buffers: ReadonlyArray<ArrayBufferView>, + cb: ( + err: ErrnoException | null, + bytesWritten: number, + buffers: ArrayBufferView[], + ) => void, +): void; +export function writev( + fd: number, + buffers: ReadonlyArray<ArrayBufferView>, + position: number | null, + cb: ( + err: ErrnoException | null, + bytesWritten: number, + buffers: ArrayBufferView[], + ) => void, +): void; +export interface WriteVResult { + bytesWritten: number; + buffers: ArrayBufferView[]; +} +export namespace writev { + function __promisify__( + fd: number, + buffers: ReadonlyArray<ArrayBufferView>, + position?: number, + ): Promise<WriteVResult>; +} +/** + * For detailed information, see the documentation of the asynchronous version of + * this API: {@link writev}. + * @since v12.9.0 + * @return The number of bytes written. + */ +export function writevSync( + fd: number, + buffers: ReadonlyArray<ArrayBufferView>, + position?: number, +): number; diff --git a/ext/node/polyfills/_fs/_fs_writev.mjs b/ext/node/polyfills/_fs/_fs_writev.mjs new file mode 100644 index 000000000..ffc67c81a --- /dev/null +++ b/ext/node/polyfills/_fs/_fs_writev.mjs @@ -0,0 +1,81 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Copyright Joyent, Inc. and Node.js contributors. All rights reserved. MIT license. +import { Buffer } from "internal:deno_node/polyfills/buffer.ts"; +import { validateBufferArray } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { getValidatedFd } from "internal:deno_node/polyfills/internal/fs/utils.mjs"; +import { maybeCallback } from "internal:deno_node/polyfills/_fs/_fs_common.ts"; + +export function writev(fd, buffers, position, callback) { + const innerWritev = async (fd, buffers, position) => { + const chunks = []; + const offset = 0; + for (let i = 0; i < buffers.length; i++) { + if (Buffer.isBuffer(buffers[i])) { + chunks.push(buffers[i]); + } else { + chunks.push(Buffer.from(buffers[i])); + } + } + if (typeof position === "number") { + await Deno.seekSync(fd, position, Deno.SeekMode.Start); + } + const buffer = Buffer.concat(chunks); + let currentOffset = 0; + while (currentOffset < buffer.byteLength) { + currentOffset += await Deno.writeSync(fd, buffer.subarray(currentOffset)); + } + return currentOffset - offset; + }; + + fd = getValidatedFd(fd); + validateBufferArray(buffers); + callback = maybeCallback(callback || position); + + if (buffers.length === 0) { + process.nextTick(callback, null, 0, buffers); + return; + } + + if (typeof position !== "number") position = null; + + innerWritev(fd, buffers, position).then( + (nwritten) => { + callback(null, nwritten, buffers); + }, + (err) => callback(err), + ); +} + +export function writevSync(fd, buffers, position) { + const innerWritev = (fd, buffers, position) => { + const chunks = []; + const offset = 0; + for (let i = 0; i < buffers.length; i++) { + if (Buffer.isBuffer(buffers[i])) { + chunks.push(buffers[i]); + } else { + chunks.push(Buffer.from(buffers[i])); + } + } + if (typeof position === "number") { + Deno.seekSync(fd, position, Deno.SeekMode.Start); + } + const buffer = Buffer.concat(chunks); + let currentOffset = 0; + while (currentOffset < buffer.byteLength) { + currentOffset += Deno.writeSync(fd, buffer.subarray(currentOffset)); + } + return currentOffset - offset; + }; + + fd = getValidatedFd(fd); + validateBufferArray(buffers); + + if (buffers.length === 0) { + return 0; + } + + if (typeof position !== "number") position = null; + + return innerWritev(fd, buffers, position); +} diff --git a/ext/node/polyfills/_fs/testdata/hello.txt b/ext/node/polyfills/_fs/testdata/hello.txt new file mode 100644 index 000000000..95d09f2b1 --- /dev/null +++ b/ext/node/polyfills/_fs/testdata/hello.txt @@ -0,0 +1 @@ +hello world
\ No newline at end of file |