From 50e24927b4e66199c6a66080fbd832d3a12f5a75 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 23:19:11 +0000 Subject: [PATCH 1/5] test harness: keep scanning the Flags line after handling --expose-gc The --expose-gc and --expose-externalize-string branches ended the flag scan with break, so a later flag on the same line was never processed. For '// Flags: --expose-gc --expose-internals' the expose-internals require interceptor was silently skipped and the test failed with "Cannot find module 'internal/...'". Continue like the other handled flags (the --no-warnings comment already promises this). --- test/js/node/test/common/index.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/test/js/node/test/common/index.js b/test/js/node/test/common/index.js index 9ac5822e5755..a5f9b2b3070e 100644 --- a/test/js/node/test/common/index.js +++ b/test/js/node/test/common/index.js @@ -173,7 +173,9 @@ if (process.argv.length === 2 && const { onGCSweepSync } = require('./gc'); const { releaseWeakRefs } = require('bun:jsc'); globalThis.gc ??= () => { Bun.gc(true); onGCSweepSync(releaseWeakRefs, Bun.gc); }; - break; + // Keep scanning: a later --expose-internals on the same Flags line + // (e.g. `--expose-gc --expose-internals`) still needs its shim. + continue; } if ((flag === "--expose-externalize-string" || flag === "--expose_externalize_string") && process.versions.bun) { // V8's externalized-string test helpers. JavaScriptCore has no string @@ -189,7 +191,8 @@ if (process.argv.length === 2 && } return true; }; - break; + // Keep scanning for the same reason as --expose-gc above. + continue; } if ((flag === "--experimental-sqlite" || flag === "--no-experimental-sqlite") && process.versions.bun) { // node:sqlite is always available in Bun; the Node experimental gate From 60fb9f1bd83162d1e40c1e6e97fd52a710a525b0 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 23:19:11 +0000 Subject: [PATCH 2/5] node tests: vendor internal/errors.js for the errors-family suite (+7 tests) Vendor node v26.3.0 lib/internal/errors.js byte-verbatim under common/nodeinternals/ and register it, so tests that require E/SystemError/codes/AbortError from 'internal/errors' run against node's real implementation. Adds the AggregateError primordial to the emulator and gives internalBinding('util') the privateSymbols object (arrow_message_private_symbol) the module destructures at load. Passing: test-errors-aborterror, test-errors-systemerror-frozen-intrinsics, and the five test-errors-systemerror-stackTraceLimit-* variants. --- src/js/internal/test/binding.ts | 7 +- test/js/node/test/common/nodeinternals.js | 3 +- .../common/nodeinternals/internal/errors.js | 1959 +++++++++++++++++ .../test/parallel/test-errors-aborterror.js | 28 + ...st-errors-systemerror-frozen-intrinsics.js | 24 + ...stemerror-stackTraceLimit-custom-setter.js | 30 + ...tackTraceLimit-deleted-and-Error-sealed.js | 27 + ...ors-systemerror-stackTraceLimit-deleted.js | 26 + ...error-stackTraceLimit-has-only-a-getter.js | 26 + ...ystemerror-stackTraceLimit-not-writable.js | 29 + 10 files changed, 2157 insertions(+), 2 deletions(-) create mode 100644 test/js/node/test/common/nodeinternals/internal/errors.js create mode 100644 test/js/node/test/parallel/test-errors-aborterror.js create mode 100644 test/js/node/test/parallel/test-errors-systemerror-frozen-intrinsics.js create mode 100644 test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-custom-setter.js create mode 100644 test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js create mode 100644 test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted.js create mode 100644 test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-has-only-a-getter.js create mode 100644 test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-not-writable.js diff --git a/src/js/internal/test/binding.ts b/src/js/internal/test/binding.ts index 0707a1a901af..fc1e2b05dd76 100644 --- a/src/js/internal/test/binding.ts +++ b/src/js/internal/test/binding.ts @@ -91,7 +91,12 @@ function internalBinding(name: string) { case "tcp_wrap": return { TCP: TestTCPWrap, constants: { SOCKET: 0, SERVER: 1 } }; case "util": - return { isInsideNodeModules }; + return { + isInsideNodeModules, + // node's util binding exposes engine-private symbols; vendored + // internal/errors.js stores its arrow message under this one. + privateSymbols: { arrow_message_private_symbol: Symbol("node:arrowMessage") }, + }; // The icu-era binding node exposed until nodejs/node#55156; vendored // tests like test-icu-punycode still consume it. case "icu": { diff --git a/test/js/node/test/common/nodeinternals.js b/test/js/node/test/common/nodeinternals.js index 7228cd9ece06..cce220fbaf3f 100644 --- a/test/js/node/test/common/nodeinternals.js +++ b/test/js/node/test/common/nodeinternals.js @@ -8,6 +8,7 @@ const path = require('path'); const util = require('util'); const VENDORED = new Set([ + 'internal/errors', 'internal/webidl', 'internal/socket_list', 'internal/fs/utils', @@ -18,7 +19,7 @@ const VENDORED = new Set([ // ---------------- primordials emulator ---------------- const globalsMap = { - Array, ArrayBuffer, BigInt, Boolean, DataView, Date, Error, EvalError, + AggregateError, Array, ArrayBuffer, BigInt, Boolean, DataView, Date, Error, EvalError, FinalizationRegistry, Function, JSON, Map, Math, Number, Object, Promise, Proxy, RangeError, ReferenceError, Reflect, RegExp, Set, String, Symbol, SyntaxError, TypeError, URIError, WeakMap, WeakRef, WeakSet, diff --git a/test/js/node/test/common/nodeinternals/internal/errors.js b/test/js/node/test/common/nodeinternals/internal/errors.js new file mode 100644 index 000000000000..ba632359fbc1 --- /dev/null +++ b/test/js/node/test/common/nodeinternals/internal/errors.js @@ -0,0 +1,1959 @@ +/* eslint node-core/documented-errors: "error" */ +/* eslint node-core/alphabetize-errors: ["error", {checkErrorDeclarations: true}] */ +/* eslint node-core/prefer-util-format-errors: "error" */ + +'use strict'; + +// The whole point behind this internal module is to allow Node.js to no +// longer be forced to treat every error message change as a semver-major +// change. The NodeError classes here all expose a `code` property whose +// value statically and permanently identifies the error. While the error +// message may change, the code should not. + +const { + AggregateError, + ArrayIsArray, + ArrayPrototypeFilter, + ArrayPrototypeIncludes, + ArrayPrototypeIndexOf, + ArrayPrototypeJoin, + ArrayPrototypeMap, + ArrayPrototypePush, + ArrayPrototypeSlice, + ArrayPrototypeSplice, + ArrayPrototypeUnshift, + Error, + ErrorCaptureStackTrace, + ErrorPrototypeToString, + JSONStringify, + MapPrototypeGet, + MathAbs, + MathMax, + Number, + NumberIsInteger, + ObjectAssign, + ObjectDefineProperties, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectIsExtensible, + ObjectKeys, + ObjectPrototypeHasOwnProperty, + RangeError, + ReflectApply, + RegExpPrototypeExec, + SafeArrayIterator, + SafeMap, + SafeWeakMap, + String, + StringPrototypeEndsWith, + StringPrototypeIncludes, + StringPrototypeIndexOf, + StringPrototypeSlice, + StringPrototypeSplit, + StringPrototypeStartsWith, + StringPrototypeToLowerCase, + Symbol, + SymbolFor, + SyntaxError, + TypeError, + URIError, +} = primordials; + +const kIsNodeError = Symbol('kIsNodeError'); + +const isWindows = process.platform === 'win32'; + +const messages = new SafeMap(); +const codes = {}; + +const classRegExp = /^[A-Z][a-zA-Z0-9]*$/; + +// Sorted by a rough estimate on most frequently used entries. +const kTypes = [ + 'string', + 'function', + 'number', + 'object', + // Accept 'Function' and 'Object' as alternative to the lower cased version. + 'Function', + 'Object', + 'boolean', + 'bigint', + 'symbol', +]; + +const MainContextError = Error; +const overrideStackTrace = new SafeWeakMap(); +let internalPrepareStackTrace = defaultPrepareStackTrace; + +/** + * The default implementation of `Error.prepareStackTrace` with simple + * concatenation of stack frames. + * Read more about `Error.prepareStackTrace` at https://v8.dev/docs/stack-trace-api#customizing-stack-traces. + * @returns {string} + */ +function defaultPrepareStackTrace(error, trace) { + // Normal error formatting: + // + // Error: Message + // at function (file) + // at file + let errorString; + if (kIsNodeError in error) { + errorString = `${error.name} [${error.code}]: ${error.message}`; + } else { + errorString = ErrorPrototypeToString(error); + } + if (trace.length === 0) { + return errorString; + } + return `${errorString}\n at ${ArrayPrototypeJoin(trace, '\n at ')}`; +} + +function setInternalPrepareStackTrace(callback) { + internalPrepareStackTrace = callback; +} + +function isPermissionModelError(err) { + return typeof err !== 'number' && err.code && err.code === 'ERR_ACCESS_DENIED'; +} + +/** + * Every realm has its own prepareStackTraceCallback. When `error.stack` is + * accessed, if the error is created in a shadow realm, the shadow realm's + * prepareStackTraceCallback is invoked. Otherwise, the principal realm's + * prepareStackTraceCallback is invoked. Note that accessing `error.stack` + * of error objects created in a VM Context will always invoke the + * prepareStackTraceCallback of the principal realm. + * @param {object} globalThis The global object of the realm that the error was + * created in. When the error object is created in a VM Context, this is the + * global object of that VM Context. + * @param {object} error The error object. + * @param {CallSite[]} trace An array of CallSite objects, read more at https://v8.dev/docs/stack-trace-api#customizing-stack-traces. + * @returns {string} + */ +function prepareStackTraceCallback(globalThis, error, trace) { + // API for node internals to override error stack formatting + // without interfering with userland code. + if (overrideStackTrace.has(error)) { + const f = overrideStackTrace.get(error); + overrideStackTrace.delete(error); + return f(error, trace); + } + + // Polyfill of V8's Error.prepareStackTrace API. + // https://crbug.com/v8/7848 + // `globalThis` is the global that contains the constructor which + // created `error`. + if (typeof globalThis.Error?.prepareStackTrace === 'function') { + return globalThis.Error.prepareStackTrace(error, trace); + } + // We still have legacy usage that depends on the main context's `Error` + // being used, even when the error is from a different context. + // TODO(devsnek): evaluate if this can be eventually deprecated/removed. + if (typeof MainContextError.prepareStackTrace === 'function') { + return MainContextError.prepareStackTrace(error, trace); + } + + // If the Error.prepareStackTrace was not a function, fallback to the + // internal implementation. + return internalPrepareStackTrace(error, trace); +} + +/** + * The default Error.prepareStackTrace implementation. + * @returns {string} + */ +function ErrorPrepareStackTrace(error, trace) { + return internalPrepareStackTrace(error, trace); +} + +const aggregateTwoErrors = (innerError, outerError) => { + if (innerError && outerError && innerError !== outerError) { + if (ArrayIsArray(outerError.errors)) { + // If `outerError` is already an `AggregateError`. + ArrayPrototypePush(outerError.errors, innerError); + return outerError; + } + let err; + if (isErrorStackTraceLimitWritable()) { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + // eslint-disable-next-line no-restricted-syntax + err = new AggregateError(new SafeArrayIterator([ + outerError, + innerError, + ]), outerError.message); + Error.stackTraceLimit = limit; + ErrorCaptureStackTrace(err, aggregateTwoErrors); + } else { + // eslint-disable-next-line no-restricted-syntax + err = new AggregateError(new SafeArrayIterator([ + outerError, + innerError, + ]), outerError.message); + } + err.code = outerError.code; + return err; + } + return innerError || outerError; +}; + +class NodeAggregateError extends AggregateError { + constructor(errors, message) { + super(new SafeArrayIterator(errors), message); + this.code = errors[0]?.code; + } + + get [kIsNodeError]() { + return true; + } + + get ['constructor']() { + return AggregateError; + } +} + +const assert = require('internal/assert'); + +// Lazily loaded +let util; + +let internalUtil = null; +function lazyInternalUtil() { + internalUtil ??= require('internal/util'); + return internalUtil; +} + +let internalUtilInspect = null; +function lazyInternalUtilInspect() { + internalUtilInspect ??= require('internal/util/inspect'); + return internalUtilInspect; +} + +let utilColors; +function lazyUtilColors() { + utilColors ??= require('internal/util/colors'); + return utilColors; +} + +let buffer; +function lazyBuffer() { + buffer ??= require('buffer').Buffer; + return buffer; +} + +function isErrorStackTraceLimitWritable() { + // Do no touch Error.stackTraceLimit as V8 would attempt to install + // it again during deserialization. + if (require('internal/v8/startup_snapshot').namespace.isBuildingSnapshot()) { + return false; + } + + const desc = ObjectGetOwnPropertyDescriptor(Error, 'stackTraceLimit'); + if (desc === undefined) { + return ObjectIsExtensible(Error); + } + + return ObjectPrototypeHasOwnProperty(desc, 'writable') ? + desc.writable : + desc.set !== undefined; +} + +function inspectWithNoCustomRetry(obj, options) { + const utilInspect = lazyInternalUtilInspect(); + + try { + return utilInspect.inspect(obj, options); + } catch { + return utilInspect.inspect(obj, { ...options, customInspect: false }); + } +} + +// A specialized Error that includes an additional info property with +// additional information about the error condition. +// It has the properties present in a UVException but with a custom error +// message followed by the uv error code and uv error message. +// It also has its own error code with the original uv error context put into +// `err.info`. +// The context passed into this error must have .code, .syscall and .message, +// and may have .path and .dest. +class SystemError extends Error { + constructor(key, context) { + super(); + const prefix = getMessage(key, [], this); + let message = `${prefix}: ${context.syscall} returned ` + + `${context.code} (${context.message})`; + + if (context.path !== undefined) + message += ` ${context.path}`; + if (context.dest !== undefined) + message += ` => ${context.dest}`; + + this.code = key; + + ObjectDefineProperties(this, { + [kIsNodeError]: { + __proto__: null, + value: true, + enumerable: false, + writable: false, + configurable: true, + }, + name: { + __proto__: null, + value: 'SystemError', + enumerable: false, + writable: true, + configurable: true, + }, + message: { + __proto__: null, + value: message, + enumerable: false, + writable: true, + configurable: true, + }, + info: { + __proto__: null, + value: context, + enumerable: true, + configurable: true, + writable: false, + }, + errno: { + __proto__: null, + get() { + return context.errno; + }, + set: (value) => { + context.errno = value; + }, + enumerable: true, + configurable: true, + }, + syscall: { + __proto__: null, + get() { + return context.syscall; + }, + set: (value) => { + context.syscall = value; + }, + enumerable: true, + configurable: true, + }, + }); + + if (context.path !== undefined) { + // TODO(BridgeAR): Investigate why and when the `.toString()` was + // introduced. The `path` and `dest` properties in the context seem to + // always be of type string. We should probably just remove the + // `.toString()` and `Buffer.from()` operations and set the value on the + // context as the user did. + ObjectDefineProperty(this, 'path', { + __proto__: null, + get() { + return context.path != null ? + context.path.toString() : context.path; + }, + set: (value) => { + context.path = value ? + lazyBuffer().from(value.toString()) : undefined; + }, + enumerable: true, + configurable: true, + }); + } + + if (context.dest !== undefined) { + ObjectDefineProperty(this, 'dest', { + __proto__: null, + get() { + return context.dest != null ? + context.dest.toString() : context.dest; + }, + set: (value) => { + context.dest = value ? + lazyBuffer().from(value.toString()) : undefined; + }, + enumerable: true, + configurable: true, + }); + } + } + + toString() { + return `${this.name} [${this.code}]: ${this.message}`; + } + + [SymbolFor('nodejs.util.inspect.custom')](recurseTimes, ctx) { + return lazyInternalUtilInspect().inspect(this, { + ...ctx, + getters: true, + customInspect: false, + }); + } +} + +function makeSystemErrorWithCode(key) { + return class NodeError extends SystemError { + constructor(ctx) { + super(key, ctx); + } + }; +} + +// This is a special error type that is only used for the E function. +class HideStackFramesError extends Error { +} + +function makeNodeErrorForHideStackFrame(Base, clazz) { + class HideStackFramesError extends Base { + constructor(...args) { + if (isErrorStackTraceLimitWritable()) { + const limit = Error.stackTraceLimit; + Error.stackTraceLimit = 0; + super(...args); + Error.stackTraceLimit = limit; + } else { + super(...args); + } + } + + // This is a workaround for wpt tests that expect that the error + // constructor has a `name` property of the base class. + get ['constructor']() { + return clazz; + } + } + + return HideStackFramesError; +} + +function makeNodeErrorWithCode(Base, key) { + const msg = messages.get(key); + const expectedLength = typeof msg !== 'string' ? -1 : getExpectedArgumentLength(msg); + + switch (expectedLength) { + case 0: { + class NodeError extends Base { + code = key; + + constructor(...args) { + assert( + args.length === 0, + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${expectedLength}).`, + ); + super(msg); + } + + // This is a workaround for wpt tests that expect that the error + // constructor has a `name` property of the base class. + get ['constructor']() { + return Base; + } + + get [kIsNodeError]() { + return true; + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + } + return NodeError; + } + case -1: { + class NodeError extends Base { + code = key; + + constructor(...args) { + super(); + ObjectDefineProperty(this, 'message', { + __proto__: null, + value: getMessage(key, args, this), + enumerable: false, + writable: true, + configurable: true, + }); + } + + // This is a workaround for wpt tests that expect that the error + // constructor has a `name` property of the base class. + get ['constructor']() { + return Base; + } + + get [kIsNodeError]() { + return true; + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + } + return NodeError; + } + default: { + + class NodeError extends Base { + code = key; + + constructor(...args) { + assert( + args.length === expectedLength, + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${expectedLength}).`, + ); + + ArrayPrototypeUnshift(args, msg); + super(ReflectApply(lazyInternalUtilInspect().format, null, args)); + } + + // This is a workaround for wpt tests that expect that the error + // constructor has a `name` property of the base class. + get ['constructor']() { + return Base; + } + + get [kIsNodeError]() { + return true; + } + + toString() { + return `${this.name} [${key}]: ${this.message}`; + } + } + return NodeError; + } + } +} + +/** + * This function removes unnecessary frames from Node.js core errors. + * @template {(...args: unknown[]) => unknown} T + * @param {T} fn + * @returns {T} + */ +function hideStackFrames(fn) { + function wrappedFn(...args) { + try { + return ReflectApply(fn, this, args); + } catch (error) { + Error.stackTraceLimit && ErrorCaptureStackTrace(error, wrappedFn); + throw error; + } + } + wrappedFn.withoutStackTrace = fn; + return wrappedFn; +} + +// Utility function for registering the error codes. Only used here. Exported +// *only* to allow for testing. +function E(sym, val, def, ...otherClasses) { + // Special case for SystemError that formats the error message differently + // The SystemErrors only have SystemError as their base classes. + messages.set(sym, val); + + const ErrClass = def === SystemError ? + makeSystemErrorWithCode(sym) : + makeNodeErrorWithCode(def, sym); + + if (otherClasses.length !== 0) { + if (otherClasses.includes(HideStackFramesError)) { + if (otherClasses.length !== 1) { + otherClasses.forEach((clazz) => { + if (clazz !== HideStackFramesError) { + ErrClass[clazz.name] = makeNodeErrorWithCode(clazz, sym); + ErrClass[clazz.name].HideStackFramesError = makeNodeErrorForHideStackFrame(ErrClass[clazz.name], clazz); + } + }); + } + } else { + otherClasses.forEach((clazz) => { + ErrClass[clazz.name] = makeNodeErrorWithCode(clazz, sym); + }); + } + } + + if (otherClasses.includes(HideStackFramesError)) { + ErrClass.HideStackFramesError = makeNodeErrorForHideStackFrame(ErrClass, def); + } + + codes[sym] = ErrClass; +} + +function getExpectedArgumentLength(msg) { + let expectedLength = 0; + const regex = /%[dfijoOs]/g; + while (RegExpPrototypeExec(regex, msg) !== null) expectedLength++; + return expectedLength; +} + +function getMessage(key, args, self) { + const msg = messages.get(key); + + if (typeof msg === 'function') { + assert( + msg.length <= args.length, // Default options do not count. + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${msg.length}).`, + ); + return ReflectApply(msg, self, args); + } + + const expectedLength = getExpectedArgumentLength(msg); + assert( + expectedLength === args.length, + `Code: ${key}; The provided arguments length (${args.length}) does not ` + + `match the required ones (${expectedLength}).`, + ); + if (args.length === 0) + return msg; + + ArrayPrototypeUnshift(args, msg); + return ReflectApply(lazyInternalUtilInspect().format, null, args); +} + +let uvBinding; + +function lazyUv() { + uvBinding ??= internalBinding('uv'); + return uvBinding; +} + +const uvUnmappedError = ['UNKNOWN', 'unknown error']; + +function uvErrmapGet(name) { + uvBinding = lazyUv(); + uvBinding.errmap ??= uvBinding.getErrorMap(); + return MapPrototypeGet(uvBinding.errmap, name); +} + +/** + * This creates an error compatible with errors produced in the C++ + * function UVException using a context object with data assembled in C++. + * The goal is to migrate them to ERR_* errors later when compatibility is + * not a concern. + */ +class UVException extends Error { + /** + * @param {object} ctx + */ + constructor(ctx) { + const { 0: code, 1: uvmsg } = uvErrmapGet(ctx.errno) || uvUnmappedError; + let message = `${code}: ${ctx.message || uvmsg}, ${ctx.syscall}`; + + let path; + let dest; + if (ctx.path) { + path = ctx.path.toString(); + message += ` '${path}'`; + } + if (ctx.dest) { + dest = ctx.dest.toString(); + message += ` -> '${dest}'`; + } + + super(message); + + for (const prop of ObjectKeys(ctx)) { + if (prop === 'message' || prop === 'path' || prop === 'dest') { + continue; + } + this[prop] = ctx[prop]; + } + + this.code = code; + if (path) { + this.path = path; + } + if (dest) { + this.dest = dest; + } + } + + get ['constructor']() { + return Error; + } +} + +/** + * This creates an error compatible with errors produced in the C++ + * This function should replace the deprecated + * `exceptionWithHostPort()` function. + */ +class UVExceptionWithHostPort extends Error { + /** + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} address + * @param {number} [port] + */ + constructor(err, syscall, address, port) { + const { 0: code, 1: uvmsg } = uvErrmapGet(err) || uvUnmappedError; + const message = `${syscall} ${code}: ${uvmsg}`; + let details = ''; + + if (port && port > 0) { + details = ` ${address}:${port}`; + } else if (address) { + details = ` ${address}`; + } + + super(`${message}${details}`); + + this.code = code; + this.errno = err; + this.syscall = syscall; + this.address = address; + if (port) { + this.port = port; + } + } + + get ['constructor']() { + return Error; + } +} + +/** + * This used to be util._errnoException(). + */ +class ErrnoException extends Error { + /** + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} [original] err + */ + constructor(err, syscall, original) { + // TODO(joyeecheung): We have to use the type-checked + // getSystemErrorName(err) to guard against invalid arguments from users. + // This can be replaced with [ code ] = errmap.get(err) when this method + // is no longer exposed to user land. + util ??= require('util'); + const code = util.getSystemErrorName(err); + const message = original ? + `${syscall} ${code} ${original}` : `${syscall} ${code}`; + + super(message); + + this.errno = err; + this.code = code; + this.syscall = syscall; + } + + get ['constructor']() { + return Error; + } +} + +/** + * Deprecated, new Error is `UVExceptionWithHostPort()` + * New function added the error description directly + * from C++. this method for backwards compatibility + * @param {number} err - A libuv error number + * @param {string} syscall + * @param {string} address + * @param {number} [port] + * @param {string} [additional] + * @returns {Error} + */ +class ExceptionWithHostPort extends Error { + constructor(err, syscall, address, port, additional) { + // TODO(joyeecheung): We have to use the type-checked + // getSystemErrorName(err) to guard against invalid arguments from users. + // This can be replaced with [ code ] = errmap.get(err) when this method + // is no longer exposed to user land. + util ??= require('util'); + let code; + let details = ''; + // True when permission model is enabled + if (isPermissionModelError(err)) { + code = err.code; + details = ` ${err.message}`; + } else { + code = util.getSystemErrorName(err); + if (port && port > 0) { + details = ` ${address}:${port}`; + } else if (address) { + details = ` ${address}`; + } + if (additional) { + details += ` - Local (${additional})`; + } + } + super(`${syscall} ${code}${details}`); + + this.errno = err; + this.code = code; + this.syscall = syscall; + this.address = address; + if (port) { + this.port = port; + } + } + + get ['constructor']() { + return Error; + } +} + +class DNSException extends Error { + /** + * @param {number|string} code - A libuv error number or a c-ares error code + * @param {string} syscall + * @param {string} [hostname] + */ + constructor(code, syscall, hostname) { + let errno; + // If `code` is of type number, it is a libuv error number, else it is a + // c-ares/permission model error code. + // TODO(joyeecheung): translate c-ares error codes into numeric ones and + // make them available in a property that's not error.errno (since they + // can be in conflict with libuv error codes). Also make sure + // util.getSystemErrorName() can understand them when an being informed that + // the number is a c-ares error code. + if (typeof code === 'number') { + errno = code; + // ENOTFOUND is not a proper POSIX error, but this error has been in place + // long enough that it's not practical to remove it. + if (code === lazyUv().UV_EAI_NODATA || code === lazyUv().UV_EAI_NONAME) { + code = 'ENOTFOUND'; // Fabricated error name. + } else { + code = lazyInternalUtil().getSystemErrorName(code); + } + } else if (isPermissionModelError(code)) { + // Expects a ERR_ACCESS_DENIED object + code = code.code; + } + super(`${syscall} ${code}${hostname ? ` ${hostname}` : ''}`); + this.errno = errno; + this.code = code; + this.syscall = syscall; + if (hostname) { + this.hostname = hostname; + } + } + + get ['constructor']() { + return Error; + } +} + +class ConnResetException extends Error { + constructor(msg) { + super(msg); + this.code = 'ECONNRESET'; + } + + get ['constructor']() { + return Error; + } +} + +let maxStack_ErrorName; +let maxStack_ErrorMessage; + +/** + * Returns true if `err.name` and `err.message` are equal to engine-specific + * values indicating max call stack size has been exceeded. + * "Maximum call stack size exceeded" in V8. + * @param {Error} err + * @returns {boolean} + */ +function isStackOverflowError(err) { + if (maxStack_ErrorMessage === undefined) { + try { + function overflowStack() { overflowStack(); } + overflowStack(); + } catch (err) { + maxStack_ErrorMessage = err.message; + maxStack_ErrorName = err.name; + } + } + + return err && err.name === maxStack_ErrorName && + err.message === maxStack_ErrorMessage; +} + +// Only use this for integers! Decimal numbers do not work with this function. +function addNumericalSeparator(val) { + let res = ''; + let i = val.length; + const start = val[0] === '-' ? 1 : 0; + for (; i >= start + 4; i -= 3) { + res = `_${StringPrototypeSlice(val, i - 3, i)}${res}`; + } + return `${StringPrototypeSlice(val, 0, i)}${res}`; +} + +// Used to enhance the stack that will be picked up by the inspector +const kEnhanceStackBeforeInspector = Symbol('kEnhanceStackBeforeInspector'); + +// These are supposed to be called only on fatal exceptions before +// the process exits. +const fatalExceptionStackEnhancers = { + beforeInspector(error) { + if (typeof error[kEnhanceStackBeforeInspector] !== 'function') { + return error.stack; + } + + try { + // Set the error.stack here so it gets picked up by the + // inspector. + error.stack = error[kEnhanceStackBeforeInspector](); + } catch { + // We are just enhancing the error. If it fails, ignore it. + } + return error.stack; + }, + afterInspector(error) { + const originalStack = error.stack; + let useColors = true; + // Some consoles do not convert ANSI escape sequences to colors, + // rather display them directly to the stdout. On those consoles, + // libuv emulates colors by intercepting stdout stream and calling + // corresponding Windows API functions for setting console colors. + // However, fatal error are handled differently and we cannot easily + // highlight them. On Windows, detecting whether a console supports + // ANSI escape sequences is not reliable. + if (isWindows) { + const info = internalBinding('os').getOSInformation(); + const ver = ArrayPrototypeMap(StringPrototypeSplit(info[2], '.', 3), + Number); + if (ver[0] !== 10 || ver[2] < 14393) { + useColors = false; + } + } + const { + inspect, + inspectDefaultOptions: { + colors: defaultColors, + }, + } = lazyInternalUtilInspect(); + const colors = useColors && (lazyUtilColors().shouldColorize(process.stderr) || defaultColors); + try { + return inspect(error, { + colors, + customInspect: false, + depth: MathMax(inspect.defaultOptions.depth, 5), + }); + } catch { + return originalStack; + } + }, +}; + +const { + privateSymbols: { + arrow_message_private_symbol, + }, +} = internalBinding('util'); +// Ensures the printed error line is from user code. +function setArrowMessage(err, arrowMessage) { + err[arrow_message_private_symbol] = arrowMessage; +} + +// Hide stack lines before the first user code line. +function hideInternalStackFrames(error) { + overrideStackTrace.set(error, (error, stackFrames) => { + let frames = stackFrames; + if (typeof stackFrames === 'object') { + frames = ArrayPrototypeFilter( + stackFrames, + (frm) => !StringPrototypeStartsWith(frm.getFileName() || '', + 'node:internal'), + ); + } + ArrayPrototypeUnshift(frames, error); + return ArrayPrototypeJoin(frames, '\n at '); + }); +} + +// Node uses an AbortError that isn't exactly the same as the DOMException +// to make usage of the error in userland and readable-stream easier. +// It is a regular error with `.code` and `.name`. +class AbortError extends Error { + constructor(message = 'The operation was aborted', options = undefined) { + if (options !== undefined && typeof options !== 'object') { + throw new codes.ERR_INVALID_ARG_TYPE('options', 'Object', options); + } + super(message, options); + this.code = 'ABORT_ERR'; + this.name = 'AbortError'; + } +} + +/** + * This creates a generic Node.js error. + * @param {string} message The error message. + * @param {object} errorProperties Object with additional properties to be added to the error. + * @returns {Error} + */ +const genericNodeError = hideStackFrames(function genericNodeError(message, errorProperties) { + // eslint-disable-next-line no-restricted-syntax + const err = new Error(message); + ObjectAssign(err, errorProperties); + return err; +}); + +/** + * Determine the specific type of a value for type-mismatch errors. + * @param {*} value + * @returns {string} + */ +function determineSpecificType(value) { + if (value === null) { + return 'null'; + } else if (value === undefined) { + return 'undefined'; + } + + const type = typeof value; + + switch (type) { + case 'bigint': + return `type bigint (${value}n)`; + case 'number': + if (value === 0) { + return 1 / value === -Infinity ? 'type number (-0)' : 'type number (0)'; + } else if (value !== value) { // eslint-disable-line no-self-compare + return 'type number (NaN)'; + } else if (value === Infinity) { + return 'type number (Infinity)'; + } else if (value === -Infinity) { + return 'type number (-Infinity)'; + } + return `type number (${value})`; + case 'boolean': + return value ? 'type boolean (true)' : 'type boolean (false)'; + case 'symbol': + return `type symbol (${String(value)})`; + case 'function': + return `function ${value.name}`; + case 'object': + if (value.constructor && 'name' in value.constructor) { + return `an instance of ${value.constructor.name}`; + } + return `${lazyInternalUtilInspect().inspect(value, { depth: -1 })}`; + case 'string': + value.length > 28 && (value = `${StringPrototypeSlice(value, 0, 25)}...`); + if (StringPrototypeIndexOf(value, "'") === -1) { + return `type string ('${value}')`; + } + return `type string (${JSONStringify(value)})`; + default: + value = lazyInternalUtilInspect().inspect(value, { colors: false }); + if (value.length > 28) { + value = `${StringPrototypeSlice(value, 0, 25)}...`; + } + + return `type ${type} (${value})`; + } +} + +/** + * Create a list string in the form like 'A and B' or 'A, B, ..., and Z'. + * We cannot use Intl.ListFormat because it's not available in + * --without-intl builds. + * @param {string[]} array An array of strings. + * @param {string} [type] The list type to be inserted before the last element. + * @returns {string} + */ +function formatList(array, type = 'and') { + switch (array.length) { + case 0: return ''; + case 1: return `${array[0]}`; + case 2: return `${array[0]} ${type} ${array[1]}`; + case 3: return `${array[0]}, ${array[1]}, ${type} ${array[2]}`; + default: + return `${ArrayPrototypeJoin(ArrayPrototypeSlice(array, 0, -1), ', ')}, ${type} ${array[array.length - 1]}`; + } +} + +module.exports = { + AbortError, + aggregateTwoErrors, + NodeAggregateError, + codes, + ConnResetException, + DNSException, + // This is exported only to facilitate testing. + determineSpecificType, + E, + ErrnoException, + ExceptionWithHostPort, + fatalExceptionStackEnhancers, + formatList, + genericNodeError, + getMessage, + hideInternalStackFrames, + hideStackFrames, + inspectWithNoCustomRetry, + isErrorStackTraceLimitWritable, + isStackOverflowError, + kEnhanceStackBeforeInspector, + kIsNodeError, + defaultPrepareStackTrace, + setInternalPrepareStackTrace, + overrideStackTrace, + prepareStackTraceCallback, + ErrorPrepareStackTrace, + setArrowMessage, + SystemError, + uvErrmapGet, + UVException, + UVExceptionWithHostPort, +}; + +// To declare an error message, use the E(sym, val, def) function above. The sym +// must be an upper case string. The val can be either a function or a string. +// The def must be an error class. +// The return value of the function must be a string. +// Examples: +// E('EXAMPLE_KEY1', 'This is the error value', Error); +// E('EXAMPLE_KEY2', (a, b) => return `${a} ${b}`, RangeError); +// +// Once an error code has been assigned, the code itself MUST NOT change and +// any given error code must never be reused to identify a different error. +// +// Any error code added here should also be added to the documentation +// +// Note: Please try to keep these in alphabetical order +// +// Note: Node.js specific errors must begin with the prefix ERR_ + +E('ERR_ACCESS_DENIED', + function(msg, permission = '', resource = '') { + this.permission = permission; + this.resource = resource; + return msg; + }, + Error); +E('ERR_AMBIGUOUS_ARGUMENT', 'The "%s" argument is ambiguous. %s', TypeError); +E('ERR_ARG_NOT_ITERABLE', '%s must be iterable', TypeError); +E('ERR_ASSERTION', '%s', Error); +E('ERR_ASYNC_CALLBACK', '%s must be a function', TypeError); +E('ERR_ASYNC_LOADER_REQUEST_NEVER_SETTLED', + 'Async loader request never settled', Error); +E('ERR_ASYNC_TYPE', 'Invalid name for async "type": %s', TypeError); +E('ERR_BROTLI_INVALID_PARAM', '%s is not a valid Brotli parameter', RangeError); +E('ERR_BUFFER_OUT_OF_BOUNDS', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (name = undefined) => { + if (name) { + return `"${name}" is outside of buffer bounds`; + } + return 'Attempt to access memory outside buffer bounds'; + }, RangeError); +E('ERR_BUFFER_TOO_LARGE', + 'Cannot create a Buffer larger than %s bytes', + RangeError); +E('ERR_CANNOT_WATCH_SIGINT', 'Cannot watch for SIGINT signals', Error); +E('ERR_CHILD_CLOSED_BEFORE_REPLY', + 'Child closed before reply received', Error); +E('ERR_CHILD_PROCESS_IPC_REQUIRED', + "Forked processes must have an IPC channel, missing value 'ipc' in %s", + Error); +E('ERR_CHILD_PROCESS_STDIO_MAXBUFFER', '%s maxBuffer length exceeded', + RangeError); +E('ERR_CONSOLE_WRITABLE_STREAM', + 'Console expects a writable stream instance for %s', TypeError); +E('ERR_CONSTRUCT_CALL_REQUIRED', 'Class constructor %s cannot be invoked without `new`', TypeError); +E('ERR_CONTEXT_NOT_INITIALIZED', 'context used is not initialized', Error); +E('ERR_CRYPTO_ARGON2_NOT_SUPPORTED', 'Argon2 algorithm not supported', Error); +E('ERR_CRYPTO_CUSTOM_ENGINE_NOT_SUPPORTED', + 'Custom engines not supported by this OpenSSL', Error); +E('ERR_CRYPTO_ECDH_INVALID_FORMAT', 'Invalid ECDH format: %s', TypeError); +E('ERR_CRYPTO_ECDH_INVALID_PUBLIC_KEY', + 'Public key is not valid for specified curve', Error); +E('ERR_CRYPTO_ENGINE_UNKNOWN', 'Engine "%s" was not found', Error); +E('ERR_CRYPTO_FIPS_FORCED', + 'Cannot set FIPS mode, it was forced with --force-fips at startup.', Error); +E('ERR_CRYPTO_FIPS_UNAVAILABLE', 'Cannot set FIPS mode in a non-FIPS build.', + Error); +E('ERR_CRYPTO_HASH_FINALIZED', 'Digest already called', Error); +E('ERR_CRYPTO_HASH_UPDATE_FAILED', 'Hash update failed', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY', 'Incompatible %s: %s', Error); +E('ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS', 'The selected key encoding %s %s.', + Error); +E('ERR_CRYPTO_INVALID_DIGEST', 'Invalid digest: %s', TypeError); +E('ERR_CRYPTO_INVALID_JWK', 'Invalid JWK data', TypeError); +E('ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE', + 'Invalid key object type %s, expected %s.', TypeError); +E('ERR_CRYPTO_INVALID_STATE', 'Invalid state for operation %s', Error); +E('ERR_CRYPTO_KEM_NOT_SUPPORTED', 'KEM is not supported', Error); +E('ERR_CRYPTO_PBKDF2_ERROR', 'PBKDF2 error', Error); +E('ERR_CRYPTO_SCRYPT_NOT_SUPPORTED', 'Scrypt algorithm not supported', Error); +// Switch to TypeError. The current implementation does not seem right. +E('ERR_CRYPTO_SIGN_KEY_REQUIRED', 'No key provided to sign', Error); +E('ERR_DEBUGGER_ERROR', '%s', Error); +E('ERR_DEBUGGER_STARTUP_ERROR', function(message, details = undefined) { + if (details !== undefined) { + ObjectAssign(this, details); + } + return message; +}, Error); +E('ERR_DIR_CLOSED', 'Directory handle was closed', Error); +E('ERR_DIR_CONCURRENT_OPERATION', + 'Cannot do synchronous work on directory handle with concurrent ' + + 'asynchronous operations', Error); +E('ERR_DNS_SET_SERVERS_FAILED', 'c-ares failed to set servers: "%s" [%s]', + Error); +E('ERR_DOMAIN_CALLBACK_NOT_AVAILABLE', + 'A callback was registered through ' + + 'process.setUncaughtExceptionCaptureCallback(), which is mutually ' + + 'exclusive with using the `domain` module', + Error); +E('ERR_DOMAIN_CANNOT_SET_UNCAUGHT_EXCEPTION_CAPTURE', + 'The `domain` module is in use, which is mutually exclusive with calling ' + + 'process.setUncaughtExceptionCaptureCallback()', + Error); +E('ERR_DUPLICATE_STARTUP_SNAPSHOT_MAIN_FUNCTION', + 'Deserialize main function is already configured.', Error); +E('ERR_ENCODING_INVALID_ENCODED_DATA', function(encoding, ret) { + this.errno = ret; + return `The encoded data was not valid for encoding ${encoding}`; +}, TypeError); +E('ERR_ENCODING_NOT_SUPPORTED', 'The "%s" encoding is not supported', + RangeError); +E('ERR_EVAL_ESM_CANNOT_PRINT', '--print cannot be used with ESM input', Error); +E('ERR_EVENT_RECURSION', 'The event "%s" is already being dispatched', Error); +E('ERR_FALSY_VALUE_REJECTION', function(reason) { + this.reason = reason; + return 'Promise was rejected with falsy value'; +}, Error, HideStackFramesError); +E('ERR_FEATURE_UNAVAILABLE_ON_PLATFORM', + 'The feature %s is unavailable on the current platform' + + ', which is being used to run Node.js', + TypeError); +E('ERR_FS_CP_DIR_TO_NON_DIR', + 'Cannot overwrite non-directory with directory', SystemError); +E('ERR_FS_CP_EEXIST', 'Target already exists', SystemError); +E('ERR_FS_CP_EINVAL', 'Invalid src or dest', SystemError); +E('ERR_FS_CP_FIFO_PIPE', 'Cannot copy a FIFO pipe', SystemError); +E('ERR_FS_CP_NON_DIR_TO_DIR', + 'Cannot overwrite directory with non-directory', SystemError); +E('ERR_FS_CP_SOCKET', 'Cannot copy a socket file', SystemError); +E('ERR_FS_CP_SYMLINK_TO_SUBDIRECTORY', + 'Cannot overwrite symlink in subdirectory of self', SystemError); +E('ERR_FS_CP_UNKNOWN', 'Cannot copy an unknown file type', SystemError); +E('ERR_FS_EISDIR', 'Path is a directory', SystemError, HideStackFramesError); +E('ERR_FS_FILE_TOO_LARGE', 'File size (%s) is greater than 2 GiB', RangeError); +E('ERR_FS_WATCH_QUEUE_OVERFLOW', 'fs.watch() queued more than %d events', Error); +E('ERR_HTTP2_ALTSVC_INVALID_ORIGIN', + 'HTTP/2 ALTSVC frames require a valid origin', TypeError); +E('ERR_HTTP2_ALTSVC_LENGTH', + 'HTTP/2 ALTSVC frames are limited to 16382 bytes', TypeError); +E('ERR_HTTP2_CONNECT_AUTHORITY', + ':authority header is required for CONNECT requests', Error); +E('ERR_HTTP2_CONNECT_PATH', + 'The :path header is forbidden for CONNECT requests', Error); +E('ERR_HTTP2_CONNECT_SCHEME', + 'The :scheme header is forbidden for CONNECT requests', Error); +E('ERR_HTTP2_GOAWAY_SESSION', + 'New streams cannot be created after receiving a GOAWAY', Error); +E('ERR_HTTP2_HEADERS_AFTER_RESPOND', + 'Cannot specify additional headers after response initiated', Error); +E('ERR_HTTP2_HEADERS_SENT', 'Response has already been initiated.', Error); +E('ERR_HTTP2_HEADER_SINGLE_VALUE', + 'Header field "%s" must only have a single value', TypeError); +E('ERR_HTTP2_INFO_STATUS_NOT_ALLOWED', + 'Informational status codes cannot be used', RangeError); +E('ERR_HTTP2_INVALID_CONNECTION_HEADERS', + 'HTTP/1 Connection specific headers are forbidden: "%s"', TypeError); +E('ERR_HTTP2_INVALID_HEADER_VALUE', + 'Invalid value "%s" for header "%s"', TypeError, HideStackFramesError); +E('ERR_HTTP2_INVALID_INFO_STATUS', + 'Invalid informational status code: %s', RangeError); +E('ERR_HTTP2_INVALID_ORIGIN', + 'HTTP/2 ORIGIN frames require a valid origin', TypeError); +E('ERR_HTTP2_INVALID_PACKED_SETTINGS_LENGTH', + 'Packed settings length must be a multiple of six', RangeError); +E('ERR_HTTP2_INVALID_PSEUDOHEADER', + '"%s" is an invalid pseudoheader or is used incorrectly', TypeError, HideStackFramesError); +E('ERR_HTTP2_INVALID_SESSION', 'The session has been destroyed', Error); +E('ERR_HTTP2_INVALID_SETTING_VALUE', + // Using default arguments here is important so the arguments are not counted + // towards `Function#length`. + function(name, actual, min = undefined, max = undefined) { + this.actual = actual; + if (min !== undefined) { + this.min = min; + this.max = max; + } + return `Invalid value for setting "${name}": ${actual}`; + }, TypeError, RangeError, HideStackFramesError); +E('ERR_HTTP2_INVALID_STREAM', 'The stream has been destroyed', Error); +E('ERR_HTTP2_MAX_PENDING_SETTINGS_ACK', + 'Maximum number of pending settings acknowledgements', Error); +E('ERR_HTTP2_NESTED_PUSH', + 'A push stream cannot initiate another push stream.', Error); +E('ERR_HTTP2_NO_MEM', 'Out of memory', Error); +E('ERR_HTTP2_NO_SOCKET_MANIPULATION', + 'HTTP/2 sockets should not be directly manipulated (e.g. read and written)', + Error); +E('ERR_HTTP2_ORIGIN_LENGTH', + 'HTTP/2 ORIGIN frames are limited to 16382 bytes', TypeError); +E('ERR_HTTP2_OUT_OF_STREAMS', + 'No stream ID is available because maximum stream ID has been reached', + Error); +E('ERR_HTTP2_PAYLOAD_FORBIDDEN', + 'Responses with %s status must not have a payload', Error); +E('ERR_HTTP2_PING_CANCEL', 'HTTP2 ping cancelled', Error); +E('ERR_HTTP2_PING_LENGTH', 'HTTP2 ping payload must be 8 bytes', RangeError); +E('ERR_HTTP2_PSEUDOHEADER_NOT_ALLOWED', + 'Cannot set HTTP/2 pseudo-headers', TypeError, HideStackFramesError); +E('ERR_HTTP2_PUSH_DISABLED', 'HTTP/2 client has disabled push streams', Error); +E('ERR_HTTP2_SEND_FILE', 'Directories cannot be sent', Error); +E('ERR_HTTP2_SEND_FILE_NOSEEK', + 'Offset or length can only be specified for regular files', Error); +E('ERR_HTTP2_SESSION_ERROR', 'Session closed with error code %s', Error); +E('ERR_HTTP2_SETTINGS_CANCEL', 'HTTP2 session settings canceled', Error); +E('ERR_HTTP2_SOCKET_BOUND', + 'The socket is already bound to an Http2Session', Error); +E('ERR_HTTP2_SOCKET_UNBOUND', + 'The socket has been disconnected from the Http2Session', Error); +E('ERR_HTTP2_STATUS_101', + 'HTTP status code 101 (Switching Protocols) is forbidden in HTTP/2', Error); +E('ERR_HTTP2_STATUS_INVALID', 'Invalid status code: %s', RangeError); +E('ERR_HTTP2_STREAM_CANCEL', function(error) { + let msg = 'The pending stream has been canceled'; + if (error) { + this.cause = error; + if (typeof error.message === 'string') + msg += ` (caused by: ${error.message})`; + } + return msg; +}, Error); +E('ERR_HTTP2_STREAM_ERROR', 'Stream closed with error code %s', Error); +E('ERR_HTTP2_STREAM_SELF_DEPENDENCY', + 'A stream cannot depend on itself', Error); +E('ERR_HTTP2_TOO_MANY_CUSTOM_SETTINGS', + 'Number of custom settings exceeds MAX_ADDITIONAL_SETTINGS', Error); +E('ERR_HTTP2_TOO_MANY_INVALID_FRAMES', 'Too many invalid HTTP/2 frames', Error); +E('ERR_HTTP2_TRAILERS_ALREADY_SENT', + 'Trailing headers have already been sent', Error); +E('ERR_HTTP2_TRAILERS_NOT_READY', + 'Trailing headers cannot be sent until after the wantTrailers event is ' + + 'emitted', Error); +E('ERR_HTTP2_UNSUPPORTED_PROTOCOL', 'protocol "%s" is unsupported.', Error); +E('ERR_HTTP_BODY_NOT_ALLOWED', + 'Adding content for this request method or response status is not allowed.', Error); +E('ERR_HTTP_CONTENT_LENGTH_MISMATCH', + 'Response body\'s content-length of %s byte(s) does not match the content-length of %s byte(s) set in header', Error); +E('ERR_HTTP_HEADERS_SENT', + 'Cannot %s headers after they are sent to the client', Error); +E('ERR_HTTP_INVALID_HEADER_VALUE', + 'Invalid value "%s" for header "%s"', TypeError, HideStackFramesError); +E('ERR_HTTP_INVALID_STATUS_CODE', 'Invalid status code: %s', RangeError); +E('ERR_HTTP_REQUEST_TIMEOUT', 'Request timeout', Error); +E('ERR_HTTP_SOCKET_ASSIGNED', + 'ServerResponse has an already assigned socket', Error); +E('ERR_HTTP_SOCKET_ENCODING', + 'Changing the socket encoding is not allowed per RFC7230 Section 3.', Error); +E('ERR_HTTP_TRAILER_INVALID', + 'Trailers are invalid with this transfer encoding', Error); +E('ERR_ILLEGAL_CONSTRUCTOR', 'Illegal constructor', TypeError); +E('ERR_IMPORT_ATTRIBUTE_MISSING', + 'Module "%s" needs an import attribute of "%s: %s"', TypeError); +E('ERR_IMPORT_ATTRIBUTE_TYPE_INCOMPATIBLE', + 'Module "%s" is not of type "%s"', TypeError); +E('ERR_IMPORT_ATTRIBUTE_UNSUPPORTED', + function error(attribute, value, url = undefined) { + if (url === undefined) { + return `Import attribute "${attribute}" with value "${value}" is not supported`; + } + return `Import attribute "${attribute}" with value "${value}" is not supported in ${url}`; + }, TypeError); +E('ERR_INCOMPATIBLE_OPTION_PAIR', + 'Option "%s" cannot be used in combination with option "%s"', TypeError, HideStackFramesError); +E('ERR_INPUT_TYPE_NOT_ALLOWED', '--input-type can only be used with string ' + + 'input via --eval, --print, or STDIN', Error); +E('ERR_INSPECTOR_ALREADY_ACTIVATED', + 'Inspector is already activated. Close it with inspector.close() ' + + 'before activating it again.', + Error); +E('ERR_INSPECTOR_ALREADY_CONNECTED', '%s is already connected', Error); +E('ERR_INSPECTOR_CLOSED', 'Session was closed', Error); +E('ERR_INSPECTOR_COMMAND', 'Inspector error %d: %s', Error); +E('ERR_INSPECTOR_NOT_ACTIVE', 'Inspector is not active', Error); +E('ERR_INSPECTOR_NOT_AVAILABLE', 'Inspector is not available', Error); +E('ERR_INSPECTOR_NOT_CONNECTED', 'Session is not connected', Error); +E('ERR_INSPECTOR_NOT_WORKER', 'Current thread is not a worker', Error); +E('ERR_INTERNAL_ASSERTION', (message) => { + const suffix = 'This is caused by either a bug in Node.js ' + + 'or incorrect usage of Node.js internals.\n' + + 'Please open an issue with this stack trace at ' + + 'https://github.com/nodejs/node/issues\n'; + return message === undefined ? suffix : `${message}\n${suffix}`; +}, Error); +E('ERR_INVALID_ADDRESS_FAMILY', function(addressType, host, port) { + this.host = host; + this.port = port; + return `Invalid address family: ${addressType} ${host}:${port}`; +}, RangeError); +E('ERR_INVALID_ARG_TYPE', + (name, expected, actual) => { + assert(typeof name === 'string', "'name' must be a string"); + if (!ArrayIsArray(expected)) { + expected = [expected]; + } + + let msg = 'The '; + if (StringPrototypeEndsWith(name, ' argument')) { + // For cases like 'first argument' + msg += `${name} `; + } else { + const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument'; + msg += `"${name}" ${type} `; + } + msg += 'must be '; + + const types = []; + const instances = []; + const other = []; + + for (const value of expected) { + assert(typeof value === 'string', + 'All expected entries have to be of type string'); + if (ArrayPrototypeIncludes(kTypes, value)) { + ArrayPrototypePush(types, StringPrototypeToLowerCase(value)); + } else if (RegExpPrototypeExec(classRegExp, value) !== null) { + ArrayPrototypePush(instances, value); + } else { + assert(value !== 'object', + 'The value "object" should be written as "Object"'); + ArrayPrototypePush(other, value); + } + } + + // Special handle `object` in case other instances are allowed to outline + // the differences between each other. + if (instances.length > 0) { + const pos = ArrayPrototypeIndexOf(types, 'object'); + if (pos !== -1) { + ArrayPrototypeSplice(types, pos, 1); + ArrayPrototypePush(instances, 'Object'); + } + } + + if (types.length > 0) { + msg += `${types.length > 1 ? 'one of type' : 'of type'} ${formatList(types, 'or')}`; + if (instances.length > 0 || other.length > 0) + msg += ' or '; + } + + if (instances.length > 0) { + msg += `an instance of ${formatList(instances, 'or')}`; + if (other.length > 0) + msg += ' or '; + } + + if (other.length > 0) { + if (other.length > 1) { + msg += `one of ${formatList(other, 'or')}`; + } else { + if (StringPrototypeToLowerCase(other[0]) !== other[0]) + msg += 'an '; + msg += `${other[0]}`; + } + } + + msg += `. Received ${determineSpecificType(actual)}`; + + return msg; + }, TypeError, HideStackFramesError); +E('ERR_INVALID_ARG_VALUE', (name, value, reason = 'is invalid') => { + let inspected = lazyInternalUtilInspect().inspect(value); + if (inspected.length > 128) { + inspected = `${StringPrototypeSlice(inspected, 0, 128)}...`; + } + const type = StringPrototypeIncludes(name, '.') ? 'property' : 'argument'; + return `The ${type} '${name}' ${reason}. Received ${inspected}`; +}, TypeError, RangeError, HideStackFramesError); +E('ERR_INVALID_ASYNC_ID', 'Invalid %s value: %s', RangeError); +E('ERR_INVALID_BUFFER_SIZE', + 'Buffer size must be a multiple of %s', RangeError); +E('ERR_INVALID_CHAR', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (name, field = undefined) => { + let msg = `Invalid character in ${name}`; + if (field !== undefined) { + msg += ` ["${field}"]`; + } + return msg; + }, TypeError, HideStackFramesError); +E('ERR_INVALID_CURSOR_POS', + 'Cannot set cursor row without setting its column', TypeError); +E('ERR_INVALID_FD', + '"fd" must be a positive integer: %s', RangeError); +E('ERR_INVALID_FD_TYPE', 'Unsupported fd type: %s', TypeError); +E('ERR_INVALID_FILE_URL_HOST', + 'File URL host must be "localhost" or empty on %s', TypeError); +E('ERR_INVALID_FILE_URL_PATH', function(reason, input) { + this.input = input; + return `File URL path ${reason}`; +}, TypeError); +E('ERR_INVALID_HANDLE_TYPE', 'This handle type cannot be sent', TypeError); +E('ERR_INVALID_HTTP_TOKEN', '%s must be a valid HTTP token ["%s"]', TypeError, HideStackFramesError); +E('ERR_INVALID_IP_ADDRESS', 'Invalid IP address: %s', TypeError); +E('ERR_INVALID_MIME_SYNTAX', (production, str, invalidIndex) => { + const msg = invalidIndex !== -1 ? ` at ${invalidIndex}` : ''; + return `The MIME syntax for a ${production} in "${str}" is invalid` + msg; +}, TypeError); +E('ERR_INVALID_MODULE_SPECIFIER', (request, reason, base = undefined) => { + return `Invalid module "${request}" ${reason}${base ? + ` imported from ${base}` : ''}`; +}, TypeError); +E('ERR_INVALID_PACKAGE_CONFIG', (path, base, message) => { + return `Invalid package config ${path}${base ? ` while importing ${base}` : + ''}${message ? `. ${message}` : ''}`; +}, Error); +E('ERR_INVALID_PACKAGE_TARGET', + (pkgPath, key, target, isImport = false, base = undefined) => { + const relError = typeof target === 'string' && !isImport && + target.length && !StringPrototypeStartsWith(target, './'); + if (key === '.') { + assert(isImport === false); + return `Invalid "exports" main target ${JSONStringify(target)} defined ` + + `in the package config ${pkgPath}package.json${base ? + ` imported from ${base}` : ''}${relError ? + '; targets must start with "./"' : ''}`; + } + return `Invalid "${isImport ? 'imports' : 'exports'}" target ${ + JSONStringify(target)} defined for '${key}' in the package config ${ + pkgPath}package.json${base ? ` imported from ${base}` : ''}${relError ? + '; targets must start with "./"' : ''}`; + }, Error); +E('ERR_INVALID_PROTOCOL', + 'Protocol "%s" not supported. Expected "%s"', + TypeError); +E('ERR_INVALID_REPL_EVAL_CONFIG', + 'Cannot specify both "breakEvalOnSigint" and "eval" for REPL', TypeError); +E('ERR_INVALID_REPL_INPUT', '%s', TypeError); +E('ERR_INVALID_RETURN_PROPERTY', (input, name, prop, value) => { + return `Expected a valid ${input} to be returned for the "${prop}" from the` + + ` "${name}" hook but got ${determineSpecificType(value)}.`; +}, TypeError); +E('ERR_INVALID_RETURN_PROPERTY_VALUE', (input, name, prop, value) => { + return `Expected ${input} to be returned for the "${prop}" from the` + + ` "${name}" hook but got ${determineSpecificType(value)}.`; +}, TypeError); +E('ERR_INVALID_RETURN_VALUE', (input, name, value) => { + const type = determineSpecificType(value); + + return `Expected ${input} to be returned from the "${name}"` + + ` function but got ${type}.`; +}, TypeError, RangeError); +E('ERR_INVALID_STATE', 'Invalid state: %s', Error, TypeError, RangeError); +E('ERR_INVALID_SYNC_FORK_INPUT', + 'Asynchronous forks do not support ' + + 'Buffer, TypedArray, DataView or string input: %s', + TypeError); +E('ERR_INVALID_THIS', 'Value of "this" must be of type %s', TypeError, HideStackFramesError); +E('ERR_INVALID_TUPLE', '%s must be an iterable %s tuple', TypeError); +E('ERR_INVALID_TYPESCRIPT_SYNTAX', '%s', SyntaxError); +E('ERR_INVALID_URI', 'URI malformed', URIError); +E('ERR_INVALID_URL', function(input, base = null) { + this.input = input; + + if (base != null) { + this.base = base; + } + + // Don't include URL in message. + // (See https://github.com/nodejs/node/pull/38614) + return 'Invalid URL'; +}, TypeError); +E('ERR_INVALID_URL_SCHEME', + (expected) => { + if (typeof expected === 'string') + expected = [expected]; + assert(expected.length <= 2); + const res = expected.length === 2 ? + `one of scheme ${expected[0]} or ${expected[1]}` : + `of scheme ${expected[0]}`; + return `The URL must be ${res}`; + }, TypeError); +E('ERR_IPC_CHANNEL_CLOSED', 'Channel closed', Error); +E('ERR_IPC_DISCONNECTED', 'IPC channel is already disconnected', Error); +E('ERR_IPC_ONE_PIPE', 'Child process can have only one IPC pipe', Error); +E('ERR_IPC_SYNC_FORK', 'IPC cannot be used with synchronous forks', Error); +E('ERR_IP_BLOCKED', function(ip) { + return `IP(${ip}) is blocked by net.BlockList`; +}, Error); +E( + 'ERR_LOADER_CHAIN_INCOMPLETE', + '"%s" did not call the next hook in its chain and did not' + + ' explicitly signal a short circuit. If this is intentional, include' + + ' `shortCircuit: true` in the hook\'s return.', + Error, +); +E('ERR_METHOD_NOT_IMPLEMENTED', 'The %s method is not implemented', Error); +E('ERR_MISSING_ARGS', + (...args) => { + assert(args.length > 0, 'At least one arg needs to be specified'); + let msg = 'The '; + const len = args.length; + const wrap = (a) => `"${a}"`; + args = ArrayPrototypeMap( + args, + (a) => (ArrayIsArray(a) ? + ArrayPrototypeJoin(ArrayPrototypeMap(a, wrap), ' or ') : + wrap(a)), + ); + msg += `${formatList(args)} argument${len > 1 ? 's' : ''}`; + return `${msg} must be specified`; + }, TypeError); +E('ERR_MISSING_OPTION', '%s is required', TypeError); +E('ERR_MODULE_LINK_MISMATCH', '%s', TypeError); +E('ERR_MODULE_NOT_FOUND', function(path, base, exactUrl) { + if (exactUrl) { + lazyInternalUtil().setOwnProperty(this, 'url', `${exactUrl}`); + } + return `Cannot find ${ + exactUrl ? 'module' : 'package'} '${path}' imported from ${base}`; +}, Error); +E('ERR_MULTIPLE_CALLBACK', 'Callback called multiple times', Error); +E('ERR_NAPI_CONS_FUNCTION', 'Constructor must be a function', TypeError); +E('ERR_NAPI_INVALID_DATAVIEW_ARGS', + 'byte_offset + byte_length should be less than or equal to the size in ' + + 'bytes of the array passed in', + RangeError); +E('ERR_NAPI_INVALID_TYPEDARRAY_ALIGNMENT', + 'start offset of %s should be a multiple of %s', RangeError); +E('ERR_NAPI_INVALID_TYPEDARRAY_LENGTH', + 'Invalid typed array length', RangeError); +E('ERR_NOT_BUILDING_SNAPSHOT', + 'Operation cannot be invoked when not building startup snapshot', Error); +E('ERR_NOT_IN_SINGLE_EXECUTABLE_APPLICATION', + 'Operation cannot be invoked when not in a single-executable application', Error); +E('ERR_NOT_SUPPORTED_IN_SNAPSHOT', '%s is not supported in startup snapshot', Error); +E('ERR_NO_CRYPTO', + 'Node.js is not compiled with OpenSSL crypto support', Error); +E('ERR_NO_ICU', + '%s is not supported on Node.js compiled without ICU', TypeError); +E('ERR_NO_TEMPORAL', + 'Temporal is not supported in this environment', Error); +E('ERR_NO_TYPESCRIPT', + 'Node.js is not compiled with TypeScript support', Error); +E('ERR_OPERATION_FAILED', 'Operation failed: %s', Error, TypeError); +E('ERR_OUT_OF_RANGE', + (str, range, input, replaceDefaultBoolean = false) => { + assert(range, 'Missing "range" argument'); + let msg = replaceDefaultBoolean ? str : + `The value of "${str}" is out of range.`; + let received; + if (NumberIsInteger(input) && MathAbs(input) > 2 ** 32) { + received = addNumericalSeparator(String(input)); + } else if (typeof input === 'bigint') { + received = String(input); + if (input > 2n ** 32n || input < -(2n ** 32n)) { + received = addNumericalSeparator(received); + } + received += 'n'; + } else { + received = lazyInternalUtilInspect().inspect(input); + } + msg += ` It must be ${range}. Received ${received}`; + return msg; + }, RangeError, HideStackFramesError); +E('ERR_PACKAGE_IMPORT_NOT_DEFINED', (specifier, packagePath, base) => { + return `Package import specifier "${specifier}" is not defined${packagePath ? + ` in package ${packagePath}package.json` : ''} imported from ${base}`; +}, TypeError); +E('ERR_PACKAGE_PATH_NOT_EXPORTED', (pkgPath, subpath, base = undefined) => { + if (subpath === '.') + return `No "exports" main defined in ${pkgPath}package.json${base ? + ` imported from ${base}` : ''}`; + return `Package subpath '${subpath}' is not defined by "exports" in ${ + pkgPath}package.json${base ? ` imported from ${base}` : ''}`; +}, Error); +E('ERR_PARSE_ARGS_INVALID_OPTION_VALUE', '%s', TypeError); +E('ERR_PARSE_ARGS_UNEXPECTED_POSITIONAL', "Unexpected argument '%s'. This " + + 'command does not take positional arguments', TypeError); +E('ERR_PARSE_ARGS_UNKNOWN_OPTION', (option, allowPositionals) => { + const suggestDashDash = allowPositionals ? '. To specify a positional ' + + "argument starting with a '-', place it at the end of the command after " + + `'--', as in '-- ${JSONStringify(option)}` : ''; + return `Unknown option '${option}'${suggestDashDash}`; +}, TypeError); +E('ERR_PERFORMANCE_INVALID_TIMESTAMP', + '%d is not a valid timestamp', TypeError); +E('ERR_PERFORMANCE_MEASURE_INVALID_OPTIONS', '%s', TypeError); +E('ERR_PROXY_INVALID_CONFIG', '%s', Error); +E('ERR_PROXY_TUNNEL', '%s', Error); +E('ERR_QUIC_CONNECTION_FAILED', 'QUIC connection failed', Error); +E('ERR_QUIC_ENDPOINT_CLOSED', 'QUIC endpoint closed: %s (%d)', Error); +E('ERR_QUIC_OPEN_STREAM_FAILED', 'Failed to open QUIC stream', Error); +E('ERR_QUIC_STREAM_ABORTED', '%s', Error); +E('ERR_QUIC_STREAM_RESET', + 'The QUIC stream was reset by the peer with error code %d', Error); +E('ERR_QUIC_VERSION_NEGOTIATION_ERROR', 'The QUIC session requires version negotiation', Error); +E('ERR_REQUIRE_ASYNC_MODULE', function(filename, parentFilename) { + let message = 'require() cannot be used on an ESM ' + + 'graph with top-level await. Use import() instead. To see where the' + + ' top-level await comes from, use --experimental-print-required-tla.'; + if (parentFilename) { + message += `\n From ${parentFilename} `; + } + if (filename) { + message += `\n Requiring ${filename} `; + } + return message; +}, Error); +E('ERR_REQUIRE_CYCLE_MODULE', '%s', Error); +E('ERR_REQUIRE_ESM', + function(filename, hasEsmSyntax, parentPath = null, packageJsonPath = null) { + hideInternalStackFrames(this); + let msg = `require() of ES Module ${filename}${parentPath ? ` from ${ + parentPath}` : ''} not supported.`; + if (!packageJsonPath) { + if (StringPrototypeEndsWith(filename, '.mjs')) + msg += `\nInstead change the require of ${filename} to a dynamic ` + + 'import() which is available in all CommonJS modules.'; + return msg; + } + const path = require('path'); + const basename = parentPath && path.basename(filename) === + path.basename(parentPath) ? filename : path.basename(filename); + if (hasEsmSyntax) { + msg += `\nInstead change the require of ${basename} in ${parentPath} to` + + ' a dynamic import() which is available in all CommonJS modules.'; + return msg; + } + msg += `\n${basename} is treated as an ES module file as it is a .js ` + + 'file whose nearest parent package.json contains "type": "module" ' + + 'which declares all .js files in that package scope as ES modules.' + + `\nInstead either rename ${basename} to end in .cjs, change the requiring ` + + 'code to use dynamic import() which is available in all CommonJS ' + + 'modules, or change "type": "module" to "type": "commonjs" in ' + + `${packageJsonPath} to treat all .js files as CommonJS (using .mjs for ` + + 'all ES modules instead).\n'; + return msg; + }, Error); +E('ERR_REQUIRE_ESM_RACE_CONDITION', (filename, parentFilename, isForAsyncLoaderHookWorker) => { + let raceMessage = `Cannot require() ES Module ${filename} because it is not yet fully loaded.\n`; + raceMessage += 'This may be caused by a race condition if the module is simultaneously dynamically '; + raceMessage += 'import()-ed via Promise.all().\n'; + raceMessage += 'Try await-ing the import() sequentially in a loop instead.\n'; + raceMessage += ` (From ${parentFilename ? `${parentFilename} in ` : ' '}`; + raceMessage += `${isForAsyncLoaderHookWorker ? 'loader hook worker thread' : 'non-loader-hook thread'})`; + return raceMessage; +}, Error); +E('ERR_SCRIPT_EXECUTION_INTERRUPTED', + 'Script execution was interrupted by `SIGINT`', Error); +E('ERR_SERVER_ALREADY_LISTEN', + 'Listen method has been called more than once without closing.', Error); +E('ERR_SERVER_NOT_RUNNING', 'Server is not running.', Error); +E('ERR_SINGLE_EXECUTABLE_APPLICATION_ASSET_NOT_FOUND', + 'Cannot find asset %s for the single executable application', Error); +E('ERR_SOCKET_ALREADY_BOUND', 'Socket is already bound', Error); +E('ERR_SOCKET_BAD_BUFFER_SIZE', + 'Buffer size must be a positive integer', TypeError); +E('ERR_SOCKET_BAD_PORT', (name, port, allowZero = true) => { + assert(typeof allowZero === 'boolean', + "The 'allowZero' argument must be of type boolean."); + const operator = allowZero ? '>=' : '>'; + return `${name} should be ${operator} 0 and < 65536. Received ${determineSpecificType(port)}.`; +}, RangeError, HideStackFramesError); +E('ERR_SOCKET_BAD_TYPE', + 'Bad socket type specified. Valid types are: udp4, udp6', TypeError); +E('ERR_SOCKET_BUFFER_SIZE', + 'Could not get or set buffer size', + SystemError); +E('ERR_SOCKET_CLOSED', 'Socket is closed', Error); +E('ERR_SOCKET_CLOSED_BEFORE_CONNECTION', + 'Socket closed before the connection was established', + Error); +E('ERR_SOCKET_CONNECTION_TIMEOUT', + 'Socket connection timeout', Error); +E('ERR_SOCKET_DGRAM_IS_CONNECTED', 'Already connected', Error); +E('ERR_SOCKET_DGRAM_NOT_CONNECTED', 'Not connected', Error); +E('ERR_SOCKET_DGRAM_NOT_RUNNING', 'Not running', Error); +E('ERR_SOURCE_MAP_CORRUPT', `The source map for '%s' does not exist or is corrupt.`, Error); +E('ERR_SOURCE_MAP_MISSING_SOURCE', `Cannot find '%s' imported from the source map for '%s'`, Error); +E('ERR_SRI_PARSE', + 'Subresource Integrity string %j had an unexpected %j at position %d', + SyntaxError); +E('ERR_STREAM_ALREADY_FINISHED', + 'Cannot call %s after a stream was finished', + Error); +E('ERR_STREAM_CANNOT_PIPE', 'Cannot pipe, not readable', Error); +E('ERR_STREAM_DESTROYED', 'Cannot call %s after a stream was destroyed', Error); +E('ERR_STREAM_ITER_MISSING_FLAG', + 'The stream/iter API requires the --experimental-stream-iter flag', TypeError); +E('ERR_STREAM_NULL_VALUES', 'May not write null values to stream', TypeError); +E('ERR_STREAM_PREMATURE_CLOSE', 'Premature close', Error); +E('ERR_STREAM_PUSH_AFTER_EOF', 'stream.push() after EOF', Error); +E('ERR_STREAM_UNABLE_TO_PIPE', 'Cannot pipe to a closed or destroyed stream', Error); +E('ERR_STREAM_UNSHIFT_AFTER_END_EVENT', + 'stream.unshift() after end event', Error); +E('ERR_STREAM_WRAP', 'Stream has StringDecoder set or is in objectMode', Error); +E('ERR_STREAM_WRITE_AFTER_END', 'write after end', Error); +E('ERR_SYNTHETIC', 'JavaScript Callstack', Error); +E('ERR_SYSTEM_ERROR', 'A system error occurred', SystemError, HideStackFramesError); +E('ERR_TEST_FAILURE', function(error, failureType) { + hideInternalStackFrames(this); + assert(typeof failureType === 'string' || typeof failureType === 'symbol', + "The 'failureType' argument must be of type string or symbol."); + + let msg = error?.message ?? error; + + if (typeof msg !== 'string') { + msg = inspectWithNoCustomRetry(msg); + } + + this.failureType = failureType; + this.cause = error; + return msg; +}, Error); +E('ERR_TLS_ALPN_CALLBACK_INVALID_RESULT', (value, protocols) => { + return `ALPN callback returned a value (${ + value + }) that did not match any of the client's offered protocols (${ + protocols.join(', ') + })`; +}, TypeError); +E('ERR_TLS_ALPN_CALLBACK_WITH_PROTOCOLS', + 'The ALPNCallback and ALPNProtocols TLS options are mutually exclusive', + TypeError); +E('ERR_TLS_CERT_ALTNAME_FORMAT', 'Invalid subject alternative name string', + SyntaxError); +E('ERR_TLS_CERT_ALTNAME_INVALID', function(reason, host, cert) { + this.reason = reason; + this.host = host; + this.cert = cert; + return `Hostname/IP does not match certificate's altnames: ${reason}`; +}, Error); +E('ERR_TLS_DH_PARAM_SIZE', 'DH parameter size %s is less than 2048', Error); +E('ERR_TLS_HANDSHAKE_TIMEOUT', 'TLS handshake timeout', Error); +E('ERR_TLS_INVALID_CONTEXT', '%s must be a SecureContext', TypeError); +E('ERR_TLS_INVALID_PROTOCOL_VERSION', + '%j is not a valid %s TLS protocol version', TypeError); +E('ERR_TLS_INVALID_STATE', 'TLS socket connection must be securely established', + Error); +E('ERR_TLS_PROTOCOL_VERSION_CONFLICT', + 'TLS protocol version %j conflicts with secureProtocol %j', TypeError); +E('ERR_TLS_RENEGOTIATION_DISABLED', + 'TLS session renegotiation disabled for this socket', Error); +E('ERR_TLS_RENEGOTIATION_UNSUPPORTED', + 'TLS session renegotiation is unsupported by this TLS implementation', Error); + +// This should probably be a `TypeError`. +E('ERR_TLS_REQUIRED_SERVER_NAME', + '"servername" is required parameter for Server.addContext', Error); +E('ERR_TLS_SESSION_ATTACK', 'TLS session renegotiation attack detected', Error); +E('ERR_TLS_SNI_FROM_SERVER', + 'Cannot issue SNI from a TLS server-side socket', Error); +E('ERR_TRACE_EVENTS_CATEGORY_REQUIRED', + 'At least one category is required', TypeError); +E('ERR_TRACE_EVENTS_UNAVAILABLE', 'Trace events are unavailable', Error); + +E('ERR_TRAILING_JUNK_AFTER_STREAM_END', 'Trailing junk found after the end of the compressed stream', TypeError); + +// This should probably be a `RangeError`. +E('ERR_TTY_INIT_FAILED', 'TTY initialization failed', SystemError); +E('ERR_UNAVAILABLE_DURING_EXIT', 'Cannot call function in process exit ' + + 'handler', Error); +E('ERR_UNCAUGHT_EXCEPTION_CAPTURE_ALREADY_SET', + '`process.setupUncaughtExceptionCapture()` was called while a capture ' + + 'callback was already active', + Error); +E('ERR_UNESCAPED_CHARACTERS', '%s contains unescaped characters', TypeError); +E('ERR_UNHANDLED_ERROR', + // Using a default argument here is important so the argument is not counted + // towards `Function#length`. + (err = undefined) => { + const msg = 'Unhandled error.'; + if (err === undefined) return msg; + return `${msg} (${err})`; + }, Error); +E('ERR_UNKNOWN_BUILTIN_MODULE', 'No such built-in module: %s', Error); +E('ERR_UNKNOWN_CREDENTIAL', '%s identifier does not exist: %s', Error); +E('ERR_UNKNOWN_ENCODING', 'Unknown encoding: %s', TypeError); +E('ERR_UNKNOWN_FILE_EXTENSION', 'Unknown file extension "%s" for %s', TypeError); +E('ERR_UNKNOWN_MODULE_FORMAT', 'Unknown module format: %s for URL %s', + RangeError); +E('ERR_UNKNOWN_SIGNAL', 'Unknown signal: %s', TypeError, HideStackFramesError); +E('ERR_UNSUPPORTED_DIR_IMPORT', function(path, base, exactUrl) { + lazyInternalUtil().setOwnProperty(this, 'url', exactUrl); + return `Directory import '${path}' is not supported ` + + `resolving ES modules imported from ${base}`; +}, Error); +E('ERR_UNSUPPORTED_ESM_URL_SCHEME', (url, supported) => { + let msg = `Only URLs with a scheme in: ${formatList(supported)} are supported by the default ESM loader`; + if (isWindows && url.protocol.length === 2) { + msg += + '. On Windows, absolute paths must be valid file:// URLs'; + } + msg += `. Received protocol '${url.protocol}'`; + return msg; +}, Error); +E('ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING', + 'Stripping types is currently unsupported for files under node_modules, for "%s"', + Error); +E('ERR_UNSUPPORTED_RESOLVE_REQUEST', + 'Failed to resolve module specifier "%s" from "%s": Invalid relative URL or base scheme is not hierarchical.', + TypeError); +E('ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX', '%s', SyntaxError); +E('ERR_USE_AFTER_CLOSE', '%s was closed', Error); + +// This should probably be a `TypeError`. +E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', + 'At least one valid performance entry type is required', Error); +E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', + 'A dynamic import callback was not specified.', TypeError); +E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING_FLAG', + 'A dynamic import callback was invoked without --experimental-vm-modules', + TypeError); +E('ERR_VM_MODULE_ALREADY_LINKED', 'Module has already been linked', Error); +E('ERR_VM_MODULE_CANNOT_CREATE_CACHED_DATA', + 'Cached data cannot be created for a module which has been evaluated', Error); +E('ERR_VM_MODULE_DIFFERENT_CONTEXT', + 'Linked modules must use the same context', Error); +E('ERR_VM_MODULE_LINK_FAILURE', function(message, cause) { + this.cause = cause; + return message; +}, Error); +E('ERR_VM_MODULE_NOT_MODULE', + 'Provided module is not an instance of Module', Error); +E('ERR_VM_MODULE_STATUS', 'Module status %s', Error); +E('ERR_WASI_ALREADY_STARTED', 'WASI instance has already started', Error); +E('ERR_WEBASSEMBLY_NOT_SUPPORTED', + 'WebAssembly is not supported in this environment, but is required for %s', + Error); +E('ERR_WEBASSEMBLY_RESPONSE', 'WebAssembly response %s', TypeError); +E('ERR_WORKER_INIT_FAILED', 'Worker initialization failure: %s', Error); +E('ERR_WORKER_INVALID_EXEC_ARGV', (errors, msg = 'invalid execArgv flags') => + `Initiated Worker with ${msg}: ${ArrayPrototypeJoin(errors, ', ')}`, + Error); +E('ERR_WORKER_MESSAGING_ERRORED', 'The destination thread threw an error while processing the message', Error); +E('ERR_WORKER_MESSAGING_FAILED', 'Cannot find the destination thread or listener', Error); +E('ERR_WORKER_MESSAGING_SAME_THREAD', 'Cannot sent a message to the same thread', Error); +E('ERR_WORKER_MESSAGING_TIMEOUT', 'Sending a message to another thread timed out', Error); +E('ERR_WORKER_NOT_RUNNING', 'Worker instance not running', Error); +E('ERR_WORKER_OUT_OF_MEMORY', + 'Worker terminated due to reaching memory limit: %s', Error); +E('ERR_WORKER_PATH', (filename) => + 'The worker script or module filename must be an absolute path or a ' + + 'relative path starting with \'./\' or \'../\'.' + + (StringPrototypeStartsWith(filename, 'file://') ? + ' Wrap file:// URLs with `new URL`.' : '' + ) + + (StringPrototypeStartsWith(filename, 'data:text/javascript') ? + ' Wrap data: URLs with `new URL`.' : '' + ) + + ` Received "${filename}"`, + TypeError); +E('ERR_WORKER_UNSERIALIZABLE_ERROR', + 'Serializing an uncaught exception failed', Error); +E('ERR_WORKER_UNSUPPORTED_OPERATION', + '%s is not supported in workers', TypeError); +E('ERR_ZSTD_INVALID_PARAM', '%s is not a valid zstd parameter', RangeError); diff --git a/test/js/node/test/parallel/test-errors-aborterror.js b/test/js/node/test/parallel/test-errors-aborterror.js new file mode 100644 index 000000000000..deabd78999c3 --- /dev/null +++ b/test/js/node/test/parallel/test-errors-aborterror.js @@ -0,0 +1,28 @@ +// Flags: --expose-internals +'use strict'; + +require('../common'); +const assert = require('assert'); +const { AbortError } = require('internal/errors'); + +{ + const err = new AbortError(); + assert.strictEqual(err.message, 'The operation was aborted'); + assert.strictEqual(err.cause, undefined); +} + +{ + const cause = new Error('boom'); + const err = new AbortError('bang', { cause }); + assert.strictEqual(err.message, 'bang'); + assert.strictEqual(err.cause, cause); +} + +{ + assert.throws(() => new AbortError('', false), { + code: 'ERR_INVALID_ARG_TYPE' + }); + assert.throws(() => new AbortError('', ''), { + code: 'ERR_INVALID_ARG_TYPE' + }); +} diff --git a/test/js/node/test/parallel/test-errors-systemerror-frozen-intrinsics.js b/test/js/node/test/parallel/test-errors-systemerror-frozen-intrinsics.js new file mode 100644 index 000000000000..439950083ce1 --- /dev/null +++ b/test/js/node/test/parallel/test-errors-systemerror-frozen-intrinsics.js @@ -0,0 +1,24 @@ +// Flags: --expose-internals --frozen-intrinsics +'use strict'; +require('../common'); +const assert = require('assert'); +const { E, SystemError, codes } = require('internal/errors'); + +E('ERR_TEST', 'custom message', SystemError); +const { ERR_TEST } = codes; + +const ctx = { + code: 'ETEST', + message: 'code message', + syscall: 'syscall_test', + path: '/str', + dest: '/str2' +}; +assert.throws( + () => { throw new ERR_TEST(ctx); }, + { + code: 'ERR_TEST', + name: 'SystemError', + info: ctx, + } +); diff --git a/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-custom-setter.js b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-custom-setter.js new file mode 100644 index 000000000000..d89ec22fc465 --- /dev/null +++ b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-custom-setter.js @@ -0,0 +1,30 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const { E, SystemError, codes } = require('internal/errors'); + +let stackTraceLimit; +Reflect.defineProperty(Error, 'stackTraceLimit', { + get() { return stackTraceLimit; }, + set(value) { stackTraceLimit = value; }, +}); + +E('ERR_TEST', 'custom message', SystemError); +const { ERR_TEST } = codes; + +const ctx = { + code: 'ETEST', + message: 'code message', + syscall: 'syscall_test', + path: '/str', + dest: '/str2' +}; +assert.throws( + () => { throw new ERR_TEST(ctx); }, + { + code: 'ERR_TEST', + name: 'SystemError', + info: ctx, + } +); diff --git a/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js new file mode 100644 index 000000000000..ef6a9d16f481 --- /dev/null +++ b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted-and-Error-sealed.js @@ -0,0 +1,27 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const { E, SystemError, codes } = require('internal/errors'); + +delete Error.stackTraceLimit; +Object.seal(Error); + +E('ERR_TEST', 'custom message', SystemError); +const { ERR_TEST } = codes; + +const ctx = { + code: 'ETEST', + message: 'code message', + syscall: 'syscall_test', + path: '/str', + dest: '/str2' +}; +assert.throws( + () => { throw new ERR_TEST(ctx); }, + { + code: 'ERR_TEST', + name: 'SystemError', + info: ctx, + } +); diff --git a/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted.js b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted.js new file mode 100644 index 000000000000..2967ff84ed7c --- /dev/null +++ b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-deleted.js @@ -0,0 +1,26 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const { E, SystemError, codes } = require('internal/errors'); + +delete Error.stackTraceLimit; + +E('ERR_TEST', 'custom message', SystemError); +const { ERR_TEST } = codes; + +const ctx = { + code: 'ETEST', + message: 'code message', + syscall: 'syscall_test', + path: '/str', + dest: '/str2' +}; +assert.throws( + () => { throw new ERR_TEST(ctx); }, + { + code: 'ERR_TEST', + name: 'SystemError', + info: ctx, + } +); diff --git a/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-has-only-a-getter.js b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-has-only-a-getter.js new file mode 100644 index 000000000000..49c39e157622 --- /dev/null +++ b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-has-only-a-getter.js @@ -0,0 +1,26 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const { E, SystemError, codes } = require('internal/errors'); + +Reflect.defineProperty(Error, 'stackTraceLimit', { get() { return 0; } }); + +E('ERR_TEST', 'custom message', SystemError); +const { ERR_TEST } = codes; + +const ctx = { + code: 'ETEST', + message: 'code message', + syscall: 'syscall_test', + path: '/str', + dest: '/str2' +}; +assert.throws( + () => { throw new ERR_TEST(ctx); }, + { + code: 'ERR_TEST', + name: 'SystemError', + info: ctx, + } +); diff --git a/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-not-writable.js b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-not-writable.js new file mode 100644 index 000000000000..8650c5f88738 --- /dev/null +++ b/test/js/node/test/parallel/test-errors-systemerror-stackTraceLimit-not-writable.js @@ -0,0 +1,29 @@ +// Flags: --expose-internals +'use strict'; +require('../common'); +const assert = require('assert'); +const { E, SystemError, codes } = require('internal/errors'); + +Reflect.defineProperty(Error, 'stackTraceLimit', { + writable: false, + value: Error.stackTraceLimit, +}); + +E('ERR_TEST', 'custom message', SystemError); +const { ERR_TEST } = codes; + +const ctx = { + code: 'ETEST', + message: 'code message', + syscall: 'syscall_test', + path: '/str', + dest: '/str2' +}; +assert.throws( + () => { throw new ERR_TEST(ctx); }, + { + code: 'ERR_TEST', + name: 'SystemError', + info: ctx, + } +); From f55be190e423a495400865fa604f117d46964c0b Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 23:19:15 +0000 Subject: [PATCH 3/5] url: name the custom inspect method [nodejs.util.inspect.custom] (+1 test) URL.prototype[util.inspect.custom] (and the other prototypes wired through installInspectCustom) exposed an anonymous function; node names the method '[nodejs.util.inspect.custom]' and user code can read fn.name. Create the JSFunction with that explicit name instead of relying on the symbol property key. Passing: test-whatwg-url-properties. --- .../streams/WebStreamsInspectCustom.cpp | 6 +- .../parallel/test-whatwg-url-properties.js | 142 ++++++++++++++++++ 2 files changed, 146 insertions(+), 2 deletions(-) create mode 100644 test/js/node/test/parallel/test-whatwg-url-properties.js diff --git a/src/jsc/bindings/webcore/streams/WebStreamsInspectCustom.cpp b/src/jsc/bindings/webcore/streams/WebStreamsInspectCustom.cpp index 1712b7b22025..daa3e5c7b6f1 100644 --- a/src/jsc/bindings/webcore/streams/WebStreamsInspectCustom.cpp +++ b/src/jsc/bindings/webcore/streams/WebStreamsInspectCustom.cpp @@ -108,8 +108,10 @@ WTF::String constructorNameOf(JSGlobalObject* lexicalGlobalObject, JSValue thisV void installInspectCustom(VM& vm, JSObject* prototype, NativeFunction nativeFunction) { auto* globalObject = prototype->globalObject(); - prototype->putDirectNativeFunction(vm, globalObject, WebCore::builtinNames(vm).inspectCustomPublicName(), 2, - nativeFunction, ImplementationVisibility::Public, NoIntrinsic, + // Node names this method "[nodejs.util.inspect.custom]" (V8's symbol-keyed + // method naming); user code and node's own tests read fn.name. + auto* function = JSFunction::create(vm, globalObject, 2, "[nodejs.util.inspect.custom]"_s, nativeFunction, ImplementationVisibility::Public); + prototype->putDirect(vm, WebCore::builtinNames(vm).inspectCustomPublicName(), function, static_cast(JSC::PropertyAttribute::ReadOnly | JSC::PropertyAttribute::DontEnum)); } diff --git a/test/js/node/test/parallel/test-whatwg-url-properties.js b/test/js/node/test/parallel/test-whatwg-url-properties.js new file mode 100644 index 000000000000..b76832a7d309 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-url-properties.js @@ -0,0 +1,142 @@ +'use strict'; +require('../common'); +const assert = require('assert'); +const { URL, URLSearchParams, format } = require('url'); + +[ + { name: 'toString' }, + { name: 'toJSON' }, + { name: Symbol.for('nodejs.util.inspect.custom') }, +].forEach(({ name }) => { + testMethod(URL.prototype, name); +}); + +[ + 'http://www.google.com', + 'https://www.domain.com:443', + 'file:///Users/yagiz/Developer/node', +].forEach((url) => { + const u = new URL(url); + assert.strictEqual(JSON.stringify(u), `"${u.href}"`); + assert.strictEqual(u.toString(), u.href); + assert.strictEqual(format(u), u.href); +}); + +[ + { name: 'href' }, + { name: 'protocol' }, + { name: 'username' }, + { name: 'password' }, + { name: 'host' }, + { name: 'hostname' }, + { name: 'port' }, + { name: 'pathname' }, + { name: 'search' }, + { name: 'hash' }, + { name: 'origin', readonly: true }, + { name: 'searchParams', readonly: true }, +].forEach(({ name, readonly = false }) => { + testAccessor(URL.prototype, name, readonly); +}); + +[ + { name: 'createObjectURL' }, + { name: 'revokeObjectURL' }, +].forEach(({ name }) => { + testStaticAccessor(URL, name); +}); + +[ + { name: 'append' }, + { name: 'delete' }, + { name: 'get' }, + { name: 'getAll' }, + { name: 'has' }, + { name: 'set' }, + { name: 'sort' }, + { name: 'entries' }, + { name: 'forEach' }, + { name: 'keys' }, + { name: 'values' }, + { name: 'toString' }, + { name: Symbol.iterator, methodName: 'entries' }, + { name: Symbol.for('nodejs.util.inspect.custom') }, +].forEach(({ name, methodName }) => { + testMethod(URLSearchParams.prototype, name, methodName); +}); + +{ + const params = new URLSearchParams(); + params.append('a', 'b'); + params.append('a', 'c'); + params.append('b', 'c'); + assert.strictEqual(params.size, 3); +} + +{ + const u = new URL('https://abc.com/?q=old'); + const s = u.searchParams; + u.href = 'http://abc.com/?q=new'; + assert.strictEqual(s.get('q'), 'new'); +} + +function stringifyName(name) { + if (typeof name === 'symbol') { + const { description } = name; + if (description === undefined) { + return ''; + } + return `[${description}]`; + } + + return name; +} + +function testMethod(target, name, methodName = stringifyName(name)) { + const desc = Object.getOwnPropertyDescriptor(target, name); + assert.notStrictEqual(desc, undefined); + assert.strictEqual(desc.enumerable, typeof name === 'string'); + + const { value } = desc; + assert.strictEqual(typeof value, 'function'); + assert.strictEqual(value.name, methodName); + assert.strictEqual( + Object.hasOwn(value, 'prototype'), + false, + ); +} + +function testAccessor(target, name, readonly = false) { + const desc = Object.getOwnPropertyDescriptor(target, name); + assert.notStrictEqual(desc, undefined); + assert.strictEqual(desc.enumerable, typeof name === 'string'); + + const methodName = stringifyName(name); + const { get, set } = desc; + assert.strictEqual(typeof get, 'function'); + assert.strictEqual(get.name, `get ${methodName}`); + assert.strictEqual( + Object.hasOwn(get, 'prototype'), + false, + ); + + if (readonly) { + assert.strictEqual(set, undefined); + } else { + assert.strictEqual(typeof set, 'function'); + assert.strictEqual(set.name, `set ${methodName}`); + assert.strictEqual( + Object.hasOwn(set, 'prototype'), + false, + ); + } +} + +function testStaticAccessor(target, name) { + const desc = Object.getOwnPropertyDescriptor(target, name); + assert.notStrictEqual(desc, undefined); + + assert.strictEqual(desc.configurable, true); + assert.strictEqual(desc.enumerable, true); + assert.strictEqual(desc.writable, true); +} From 101bf93f7e8b0d782bd9eaf4900ed1974b3e0a1d Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 23:36:22 +0000 Subject: [PATCH 4/5] node tests: vendor internal/encoding for the whatwg-encoding suite (+1 test) Vendor node v26.3.0 lib/internal/encoding.js (plus its internal/encoding/single-byte and internal/encoding/util submodules) byte-verbatim under common/nodeinternals/, so test-whatwg-encoding-custom-internals exercises node's real getEncodingFromLabel table. Support pieces: internalBinding('config') ({ hasIntl }) and internalBinding('encoding_binding') (encodeInto/encodeUtf8String/ decodeUTF8 backed by TextEncoder/TextDecoder) in the test binding shim; uncurried %TypedArray% static primordials, a FastBuffer shim, and customInspectSymbol in the nodeinternals emulator. --- src/js/internal/test/binding.ts | 22 + test/js/node/test/common/nodeinternals.js | 23 + .../common/nodeinternals/internal/encoding.js | 621 ++++++++++++++++++ .../internal/encoding/single-byte.js | 155 +++++ .../nodeinternals/internal/encoding/util.js | 72 ++ .../test-whatwg-encoding-custom-internals.js | 287 ++++++++ 6 files changed, 1180 insertions(+) create mode 100644 test/js/node/test/common/nodeinternals/internal/encoding.js create mode 100644 test/js/node/test/common/nodeinternals/internal/encoding/single-byte.js create mode 100644 test/js/node/test/common/nodeinternals/internal/encoding/util.js create mode 100644 test/js/node/test/parallel/test-whatwg-encoding-custom-internals.js diff --git a/src/js/internal/test/binding.ts b/src/js/internal/test/binding.ts index fc1e2b05dd76..89528a945993 100644 --- a/src/js/internal/test/binding.ts +++ b/src/js/internal/test/binding.ts @@ -90,6 +90,28 @@ function internalBinding(name: string) { return { UDP: require("internal/dgram").UDP }; case "tcp_wrap": return { TCP: TestTCPWrap, constants: { SOCKET: 0, SERVER: 1 } }; + // Just what vendored modules destructure at load; Bun always builds with ICU. + case "config": + return { hasIntl: true }; + // node's C++ encoding binding, backed by the runtime's own encoders. + case "encoding_binding": { + const utf8Encoder = new TextEncoder(); + const encodeIntoResults = new Uint32Array(2); + return { + encodeInto(source: string, dest: Uint8Array) { + const { read, written } = utf8Encoder.encodeInto(source, dest); + encodeIntoResults[0] = read; + encodeIntoResults[1] = written; + }, + encodeIntoResults, + encodeUtf8String(source: string) { + return utf8Encoder.encode(source); + }, + decodeUTF8(input: ArrayBufferView, ignoreBOM: boolean, fatal: boolean) { + return new TextDecoder("utf-8", { ignoreBOM, fatal }).decode(input); + }, + }; + } case "util": return { isInsideNodeModules, diff --git a/test/js/node/test/common/nodeinternals.js b/test/js/node/test/common/nodeinternals.js index cce220fbaf3f..e8b50f83c6fe 100644 --- a/test/js/node/test/common/nodeinternals.js +++ b/test/js/node/test/common/nodeinternals.js @@ -8,6 +8,9 @@ const path = require('path'); const util = require('util'); const VENDORED = new Set([ + 'internal/encoding', + 'internal/encoding/single-byte', + 'internal/encoding/util', 'internal/errors', 'internal/webidl', 'internal/socket_list', @@ -113,6 +116,13 @@ function computePrimordial(name) { if (name.startsWith('TypedArrayPrototype')) { return resolveOnProto(TypedArray.prototype, name.slice('TypedArrayPrototype'.length), name); } + if (name.startsWith('TypedArray')) { + // %TypedArray% statics are uncurried over the concrete constructor: + // TypedArrayOf(Uint16Array, 1, 2) === Uint16Array.of(1, 2). + const method = lowerFirst(name.slice('TypedArray'.length)); + if (typeof TypedArray[method] === 'function') return uncurryThis(TypedArray[method]); + throw new Error(`nodeinternals primordials: cannot resolve ${name}`); + } for (const g of Object.keys(globalsMap)) { if (name === g) return globalsMap[g]; @@ -456,6 +466,7 @@ function getOverrides() { return result; }; }, + customInspectSymbol: Symbol.for('nodejs.util.inspect.custom'), isWindows: process.platform === 'win32', deprecate: util.deprecate, lazyDOMException: (message, name) => new DOMException(message, name), @@ -499,6 +510,18 @@ function getOverrides() { UVException, }, 'internal/util': iuExtended, + 'internal/buffer': (() => { + class FastBuffer extends Uint8Array { + constructor(bufferOrLength, byteOffset, length) { + if (bufferOrLength === undefined) super(0); + else if (typeof bufferOrLength === 'number') super(bufferOrLength); + else super(bufferOrLength, byteOffset, length); + } + } + FastBuffer.prototype.constructor = Buffer; + Object.setPrototypeOf(FastBuffer.prototype, Buffer.prototype); + return { FastBuffer }; + })(), 'internal/util/types': require('util/types'), 'internal/util/inspect': X['internal/util/inspect'], 'internal/validators': X['internal/validators'], diff --git a/test/js/node/test/common/nodeinternals/internal/encoding.js b/test/js/node/test/common/nodeinternals/internal/encoding.js new file mode 100644 index 000000000000..7d4747abc23b --- /dev/null +++ b/test/js/node/test/common/nodeinternals/internal/encoding.js @@ -0,0 +1,621 @@ +'use strict'; + +// An implementation of the WHATWG Encoding Standard +// https://encoding.spec.whatwg.org + +const { + Boolean, + ObjectDefineProperties, + ObjectGetOwnPropertyDescriptors, + ObjectSetPrototypeOf, + ObjectValues, + SafeMap, + StringPrototypeSlice, + Symbol, + SymbolToStringTag, +} = primordials; + +const { FastBuffer } = require('internal/buffer'); + +const { + ERR_ENCODING_NOT_SUPPORTED, + ERR_INVALID_ARG_TYPE, + ERR_INVALID_THIS, + ERR_NO_ICU, +} = require('internal/errors').codes; +const kSingleByte = Symbol('single-byte'); +const kHandle = Symbol('handle'); +const kFlags = Symbol('flags'); +const kEncoding = Symbol('encoding'); +const kDecoder = Symbol('decoder'); +const kChunk = Symbol('chunk'); +const kFatal = Symbol('kFatal'); +const kUTF8FastPath = Symbol('kUTF8FastPath'); +const kIgnoreBOM = Symbol('kIgnoreBOM'); + +const { isSinglebyteEncoding, createSinglebyteDecoder } = require('internal/encoding/single-byte'); +const { unfinishedBytesUtf8, mergePrefixUtf8 } = require('internal/encoding/util'); + +const { + getConstructorOf, + customInspectSymbol: inspect, + kEmptyObject, + kEnumerableProperty, +} = require('internal/util'); + +const { + isAnyArrayBuffer, + isArrayBufferView, + isUint8Array, +} = require('internal/util/types'); + +const { + validateString, + validateObject, + kValidateObjectAllowObjectsAndNull, +} = require('internal/validators'); + +const { hasIntl } = internalBinding('config'); +const binding = internalBinding('encoding_binding'); +const { + encodeInto, + encodeIntoResults, + encodeUtf8String, + decodeUTF8, +} = binding; + +function validateDecoder(obj) { + if (obj == null || obj[kDecoder] !== true) + throw new ERR_INVALID_THIS('TextDecoder'); +} + +const CONVERTER_FLAGS_FLUSH = 0x1; +const CONVERTER_FLAGS_FATAL = 0x2; +const CONVERTER_FLAGS_IGNORE_BOM = 0x4; + +const empty = new FastBuffer(); + +const encodings = new SafeMap([ + ['unicode-1-1-utf-8', 'utf-8'], + ['unicode11utf8', 'utf-8'], + ['unicode20utf8', 'utf-8'], + ['utf8', 'utf-8'], + ['utf-8', 'utf-8'], + ['x-unicode20utf8', 'utf-8'], + ['866', 'ibm866'], + ['cp866', 'ibm866'], + ['csibm866', 'ibm866'], + ['ibm866', 'ibm866'], + ['csisolatin2', 'iso-8859-2'], + ['iso-8859-2', 'iso-8859-2'], + ['iso-ir-101', 'iso-8859-2'], + ['iso8859-2', 'iso-8859-2'], + ['iso88592', 'iso-8859-2'], + ['iso_8859-2', 'iso-8859-2'], + ['iso_8859-2:1987', 'iso-8859-2'], + ['l2', 'iso-8859-2'], + ['latin2', 'iso-8859-2'], + ['csisolatin3', 'iso-8859-3'], + ['iso-8859-3', 'iso-8859-3'], + ['iso-ir-109', 'iso-8859-3'], + ['iso8859-3', 'iso-8859-3'], + ['iso88593', 'iso-8859-3'], + ['iso_8859-3', 'iso-8859-3'], + ['iso_8859-3:1988', 'iso-8859-3'], + ['l3', 'iso-8859-3'], + ['latin3', 'iso-8859-3'], + ['csisolatin4', 'iso-8859-4'], + ['iso-8859-4', 'iso-8859-4'], + ['iso-ir-110', 'iso-8859-4'], + ['iso8859-4', 'iso-8859-4'], + ['iso88594', 'iso-8859-4'], + ['iso_8859-4', 'iso-8859-4'], + ['iso_8859-4:1988', 'iso-8859-4'], + ['l4', 'iso-8859-4'], + ['latin4', 'iso-8859-4'], + ['csisolatincyrillic', 'iso-8859-5'], + ['cyrillic', 'iso-8859-5'], + ['iso-8859-5', 'iso-8859-5'], + ['iso-ir-144', 'iso-8859-5'], + ['iso8859-5', 'iso-8859-5'], + ['iso88595', 'iso-8859-5'], + ['iso_8859-5', 'iso-8859-5'], + ['iso_8859-5:1988', 'iso-8859-5'], + ['arabic', 'iso-8859-6'], + ['asmo-708', 'iso-8859-6'], + ['csiso88596e', 'iso-8859-6'], + ['csiso88596i', 'iso-8859-6'], + ['csisolatinarabic', 'iso-8859-6'], + ['ecma-114', 'iso-8859-6'], + ['iso-8859-6', 'iso-8859-6'], + ['iso-8859-6-e', 'iso-8859-6'], + ['iso-8859-6-i', 'iso-8859-6'], + ['iso-ir-127', 'iso-8859-6'], + ['iso8859-6', 'iso-8859-6'], + ['iso88596', 'iso-8859-6'], + ['iso_8859-6', 'iso-8859-6'], + ['iso_8859-6:1987', 'iso-8859-6'], + ['csisolatingreek', 'iso-8859-7'], + ['ecma-118', 'iso-8859-7'], + ['elot_928', 'iso-8859-7'], + ['greek', 'iso-8859-7'], + ['greek8', 'iso-8859-7'], + ['iso-8859-7', 'iso-8859-7'], + ['iso-ir-126', 'iso-8859-7'], + ['iso8859-7', 'iso-8859-7'], + ['iso88597', 'iso-8859-7'], + ['iso_8859-7', 'iso-8859-7'], + ['iso_8859-7:1987', 'iso-8859-7'], + ['sun_eu_greek', 'iso-8859-7'], + ['csiso88598e', 'iso-8859-8'], + ['csisolatinhebrew', 'iso-8859-8'], + ['hebrew', 'iso-8859-8'], + ['iso-8859-8', 'iso-8859-8'], + ['iso-8859-8-e', 'iso-8859-8'], + ['iso-ir-138', 'iso-8859-8'], + ['iso8859-8', 'iso-8859-8'], + ['iso88598', 'iso-8859-8'], + ['iso_8859-8', 'iso-8859-8'], + ['iso_8859-8:1988', 'iso-8859-8'], + ['visual', 'iso-8859-8'], + ['csiso88598i', 'iso-8859-8-i'], + ['iso-8859-8-i', 'iso-8859-8-i'], + ['logical', 'iso-8859-8-i'], + ['csisolatin6', 'iso-8859-10'], + ['iso-8859-10', 'iso-8859-10'], + ['iso-ir-157', 'iso-8859-10'], + ['iso8859-10', 'iso-8859-10'], + ['iso885910', 'iso-8859-10'], + ['l6', 'iso-8859-10'], + ['latin6', 'iso-8859-10'], + ['iso-8859-13', 'iso-8859-13'], + ['iso8859-13', 'iso-8859-13'], + ['iso885913', 'iso-8859-13'], + ['iso-8859-14', 'iso-8859-14'], + ['iso8859-14', 'iso-8859-14'], + ['iso885914', 'iso-8859-14'], + ['csisolatin9', 'iso-8859-15'], + ['iso-8859-15', 'iso-8859-15'], + ['iso8859-15', 'iso-8859-15'], + ['iso885915', 'iso-8859-15'], + ['iso_8859-15', 'iso-8859-15'], + ['l9', 'iso-8859-15'], + ['iso-8859-16', 'iso-8859-16'], + ['cskoi8r', 'koi8-r'], + ['koi', 'koi8-r'], + ['koi8', 'koi8-r'], + ['koi8-r', 'koi8-r'], + ['koi8_r', 'koi8-r'], + ['koi8-ru', 'koi8-u'], + ['koi8-u', 'koi8-u'], + ['csmacintosh', 'macintosh'], + ['mac', 'macintosh'], + ['macintosh', 'macintosh'], + ['x-mac-roman', 'macintosh'], + ['dos-874', 'windows-874'], + ['iso-8859-11', 'windows-874'], + ['iso8859-11', 'windows-874'], + ['iso885911', 'windows-874'], + ['tis-620', 'windows-874'], + ['windows-874', 'windows-874'], + ['cp1250', 'windows-1250'], + ['windows-1250', 'windows-1250'], + ['x-cp1250', 'windows-1250'], + ['cp1251', 'windows-1251'], + ['windows-1251', 'windows-1251'], + ['x-cp1251', 'windows-1251'], + ['ansi_x3.4-1968', 'windows-1252'], + ['ascii', 'windows-1252'], + ['cp1252', 'windows-1252'], + ['cp819', 'windows-1252'], + ['csisolatin1', 'windows-1252'], + ['ibm819', 'windows-1252'], + ['iso-8859-1', 'windows-1252'], + ['iso-ir-100', 'windows-1252'], + ['iso8859-1', 'windows-1252'], + ['iso88591', 'windows-1252'], + ['iso_8859-1', 'windows-1252'], + ['iso_8859-1:1987', 'windows-1252'], + ['l1', 'windows-1252'], + ['latin1', 'windows-1252'], + ['us-ascii', 'windows-1252'], + ['windows-1252', 'windows-1252'], + ['x-cp1252', 'windows-1252'], + ['cp1253', 'windows-1253'], + ['windows-1253', 'windows-1253'], + ['x-cp1253', 'windows-1253'], + ['cp1254', 'windows-1254'], + ['csisolatin5', 'windows-1254'], + ['iso-8859-9', 'windows-1254'], + ['iso-ir-148', 'windows-1254'], + ['iso8859-9', 'windows-1254'], + ['iso88599', 'windows-1254'], + ['iso_8859-9', 'windows-1254'], + ['iso_8859-9:1989', 'windows-1254'], + ['l5', 'windows-1254'], + ['latin5', 'windows-1254'], + ['windows-1254', 'windows-1254'], + ['x-cp1254', 'windows-1254'], + ['cp1255', 'windows-1255'], + ['windows-1255', 'windows-1255'], + ['x-cp1255', 'windows-1255'], + ['cp1256', 'windows-1256'], + ['windows-1256', 'windows-1256'], + ['x-cp1256', 'windows-1256'], + ['cp1257', 'windows-1257'], + ['windows-1257', 'windows-1257'], + ['x-cp1257', 'windows-1257'], + ['cp1258', 'windows-1258'], + ['windows-1258', 'windows-1258'], + ['x-cp1258', 'windows-1258'], + ['x-mac-cyrillic', 'x-mac-cyrillic'], + ['x-mac-ukrainian', 'x-mac-cyrillic'], + ['chinese', 'gbk'], + ['csgb2312', 'gbk'], + ['csiso58gb231280', 'gbk'], + ['gb2312', 'gbk'], + ['gb_2312', 'gbk'], + ['gb_2312-80', 'gbk'], + ['gbk', 'gbk'], + ['iso-ir-58', 'gbk'], + ['x-gbk', 'gbk'], + ['gb18030', 'gb18030'], + ['big5', 'big5'], + ['big5-hkscs', 'big5'], + ['cn-big5', 'big5'], + ['csbig5', 'big5'], + ['x-x-big5', 'big5'], + ['cseucpkdfmtjapanese', 'euc-jp'], + ['euc-jp', 'euc-jp'], + ['x-euc-jp', 'euc-jp'], + ['csiso2022jp', 'iso-2022-jp'], + ['iso-2022-jp', 'iso-2022-jp'], + ['csshiftjis', 'shift_jis'], + ['ms932', 'shift_jis'], + ['ms_kanji', 'shift_jis'], + ['shift-jis', 'shift_jis'], + ['shift_jis', 'shift_jis'], + ['sjis', 'shift_jis'], + ['windows-31j', 'shift_jis'], + ['x-sjis', 'shift_jis'], + ['cseuckr', 'euc-kr'], + ['csksc56011987', 'euc-kr'], + ['euc-kr', 'euc-kr'], + ['iso-ir-149', 'euc-kr'], + ['korean', 'euc-kr'], + ['ks_c_5601-1987', 'euc-kr'], + ['ks_c_5601-1989', 'euc-kr'], + ['ksc5601', 'euc-kr'], + ['ksc_5601', 'euc-kr'], + ['windows-949', 'euc-kr'], + ['csiso2022kr', 'replacement'], + ['hz-gb-2312', 'replacement'], + ['iso-2022-cn', 'replacement'], + ['iso-2022-cn-ext', 'replacement'], + ['iso-2022-kr', 'replacement'], + ['replacement', 'replacement'], + ['unicodefffe', 'utf-16be'], + ['utf-16be', 'utf-16be'], + ['csunicode', 'utf-16le'], + ['iso-10646-ucs-2', 'utf-16le'], + ['ucs-2', 'utf-16le'], + ['unicode', 'utf-16le'], + ['unicodefeff', 'utf-16le'], + ['utf-16le', 'utf-16le'], + ['utf-16', 'utf-16le'], + ['x-user-defined', 'x-user-defined'], +]); + +// Unfortunately, String.prototype.trim also removes non-ascii whitespace, +// so we have to do this manually +function trimAsciiWhitespace(label) { + let s = 0; + let e = label.length; + while (s < e && ( + label[s] === '\u0009' || + label[s] === '\u000a' || + label[s] === '\u000c' || + label[s] === '\u000d' || + label[s] === '\u0020')) { + s++; + } + while (e > s && ( + label[e - 1] === '\u0009' || + label[e - 1] === '\u000a' || + label[e - 1] === '\u000c' || + label[e - 1] === '\u000d' || + label[e - 1] === '\u0020')) { + e--; + } + return StringPrototypeSlice(label, s, e); +} + +function getEncodingFromLabel(label) { + const enc = encodings.get(label); + if (enc !== undefined) return enc; + return encodings.get(trimAsciiWhitespace(label.toLowerCase())); +} + +let lazyInspect; + +class TextEncoder { + #encoding = 'utf-8'; + + #encode(input) { + return encodeUtf8String(`${input}`); + } + + #encodeInto(input, dest) { + encodeInto(input, dest); + // We need to read from the binding here since the buffer gets refreshed + // from the snapshot. + const { 0: read, 1: written } = encodeIntoResults; + return { read, written }; + } + + get encoding() { + return this.#encoding; + } + + encode(input = '') { + return this.#encode(input); + } + + encodeInto(src, dest) { + validateString(src, 'src'); + if (!dest || !isUint8Array(dest)) + throw new ERR_INVALID_ARG_TYPE('dest', 'Uint8Array', dest); + + return this.#encodeInto(src, dest); + } + + [inspect](depth, opts) { + if (typeof depth === 'number' && depth < 0) + return this; + const ctor = getConstructorOf(this); + const obj = { __proto__: { + constructor: ctor === null ? TextEncoder : ctor, + } }; + obj.encoding = this.#encoding; + // Lazy to avoid circular dependency + lazyInspect ??= require('internal/util/inspect').inspect; + return lazyInspect(obj, opts); + } +} + +ObjectDefineProperties( + TextEncoder.prototype, { + 'encode': kEnumerableProperty, + 'encodeInto': kEnumerableProperty, + 'encoding': kEnumerableProperty, + [SymbolToStringTag]: { __proto__: null, configurable: true, value: 'TextEncoder' }, + }); + +function parseInput(input) { + if (isAnyArrayBuffer(input)) { + try { + return new FastBuffer(input); + } catch { + return empty; + } + } else if (isArrayBufferView(input)) { + try { + return new FastBuffer(input.buffer, input.byteOffset, input.byteLength); + } catch { + return empty; + } + } else { + throw new ERR_INVALID_ARG_TYPE('input', ['ArrayBuffer', 'ArrayBufferView'], input); + } +} + +let icuDecode, icuGetConverter; +if (hasIntl) { + ;({ + decode: icuDecode, + getConverter: icuGetConverter, + } = internalBinding('icu')); +} + +const kBOMSeen = Symbol('BOM seen'); + +let StringDecoder; +function lazyStringDecoder() { + if (StringDecoder === undefined) + ({ StringDecoder } = require('string_decoder')); + return StringDecoder; +} + +class TextDecoder { + constructor(encoding = 'utf-8', options = kEmptyObject) { + encoding = `${encoding}`; + validateObject(options, 'options', kValidateObjectAllowObjectsAndNull); + + const enc = getEncodingFromLabel(encoding); + if (enc === undefined) + throw new ERR_ENCODING_NOT_SUPPORTED(encoding); + + let flags = 0; + if (options !== null) { + flags |= options.fatal ? CONVERTER_FLAGS_FATAL : 0; + flags |= options.ignoreBOM ? CONVERTER_FLAGS_IGNORE_BOM : 0; + } + + this[kDecoder] = true; + this[kFlags] = flags; + this[kEncoding] = enc; + this[kIgnoreBOM] = Boolean(options?.ignoreBOM); + this[kFatal] = Boolean(options?.fatal); + this[kUTF8FastPath] = false; + this[kHandle] = undefined; + this[kSingleByte] = undefined; // Does not care about streaming or BOM + this[kChunk] = null; // A copy of previous streaming tail or null + + if (enc === 'utf-8') { + this[kUTF8FastPath] = true; + this[kBOMSeen] = false; + } else if (isSinglebyteEncoding(enc)) { + this[kSingleByte] = createSinglebyteDecoder(enc, this[kFatal]); + } else { + this.#prepareConverter(); // Need to throw early if we don't support the encoding + } + } + + #prepareConverter() { + if (hasIntl) { + let icuEncoding = this[kEncoding]; + if (icuEncoding === 'gbk') icuEncoding = 'gb18030'; // 10.1.1. GBK's decoder is gb18030's decoder + const handle = icuGetConverter(icuEncoding, this[kFlags]); + if (handle === undefined) + throw new ERR_ENCODING_NOT_SUPPORTED(this[kEncoding]); + this[kHandle] = handle; + } else if (this[kEncoding] === 'utf-16le') { + if (this[kFatal]) throw new ERR_NO_ICU('"fatal" option'); + this[kHandle] = new (lazyStringDecoder())(this[kEncoding]); + this[kBOMSeen] = false; + } else { + throw new ERR_ENCODING_NOT_SUPPORTED(this[kEncoding]); + } + } + + decode(input = empty, options = kEmptyObject) { + validateDecoder(this); + validateObject(options, 'options', kValidateObjectAllowObjectsAndNull); + + if (this[kSingleByte]) return this[kSingleByte](parseInput(input)); + + const stream = options?.stream; + if (this[kUTF8FastPath]) { + const chunk = this[kChunk]; + const ignoreBom = this[kIgnoreBOM] || this[kBOMSeen]; + if (!stream) { + this[kBOMSeen] = false; + if (!chunk) return decodeUTF8(input, ignoreBom, this[kFatal]); + } + + let u = parseInput(input); + if (u.length === 0 && stream) return ''; // no state change + let prefix; + if (chunk) { + const merged = mergePrefixUtf8(u, this[kChunk]); + if (u.length < 3) { + u = merged; // Might be unfinished, but fully consumed old u + } else { + prefix = merged; // Stops at complete chunk + const add = prefix.length - this[kChunk].length; + if (add > 0) u = u.subarray(add); + } + + this[kChunk] = null; + } + + if (stream) { + const trail = unfinishedBytesUtf8(u, u.length); + if (trail > 0) { + this[kChunk] = new FastBuffer(u.subarray(-trail)); // copy + if (!prefix && trail === u.length) return ''; // No further state change + u = u.subarray(0, -trail); + } + } + + try { + const res = (prefix ? decodeUTF8(prefix, ignoreBom, this[kFatal]) : '') + + decodeUTF8(u, ignoreBom || prefix, this[kFatal]); + + // "BOM seen" is set on the current decode call only if it did not error, + // in "serialize I/O queue" after decoding + // We don't get here if we had no complete data to process, + // and we don't want BOM processing after that if streaming + if (stream) this[kBOMSeen] = true; + + return res; + } catch (e) { + this[kChunk] = null; // Reset unfinished chunk on errors + // The correct way per spec seems to be not destroying the decoder state (aka BOM here) in stream mode + throw e; + } + } + + if (hasIntl) { + const flags = stream ? 0 : CONVERTER_FLAGS_FLUSH; + return icuDecode(this[kHandle], input, flags, this[kEncoding]); + } + + input = parseInput(input); + + let result = stream ? this[kHandle].write(input) : this[kHandle].end(input); + + if (result.length > 0 && !this[kBOMSeen] && !this[kIgnoreBOM]) { + // If the very first result in the stream is a BOM, and we are not + // explicitly told to ignore it, then we discard it. + if (result[0] === '\ufeff') { + result = StringPrototypeSlice(result, 1); + } + this[kBOMSeen] = true; + } + + if (!stream) this[kBOMSeen] = false; + + return result; + } +} + +// Mix in some shared properties. +const sharedProperties = ObjectGetOwnPropertyDescriptors({ + get encoding() { + validateDecoder(this); + return this[kEncoding]; + }, + + get fatal() { + validateDecoder(this); + return (this[kFlags] & CONVERTER_FLAGS_FATAL) === CONVERTER_FLAGS_FATAL; + }, + + get ignoreBOM() { + validateDecoder(this); + return (this[kFlags] & CONVERTER_FLAGS_IGNORE_BOM) === + CONVERTER_FLAGS_IGNORE_BOM; + }, + + [inspect](depth, opts) { + validateDecoder(this); + if (typeof depth === 'number' && depth < 0) + return this; + const constructor = getConstructorOf(this) || TextDecoder; + const obj = { __proto__: { constructor } }; + obj.encoding = this.encoding; + obj.fatal = this.fatal; + obj.ignoreBOM = this.ignoreBOM; + if (opts.showHidden) { + obj[kFlags] = this[kFlags]; + obj[kHandle] = this[kHandle]; + } + // Lazy to avoid circular dependency + const { inspect } = require('internal/util/inspect'); + return `${constructor.name} ${inspect(obj)}`; + }, +}); +const propertiesValues = ObjectValues(sharedProperties); +for (let i = 0; i < propertiesValues.length; i++) { + // We want to use null-prototype objects to not rely on globally mutable + // %Object.prototype%. + ObjectSetPrototypeOf(propertiesValues[i], null); +} +sharedProperties[inspect].enumerable = false; + +ObjectDefineProperties(TextDecoder.prototype, { + decode: kEnumerableProperty, + ...sharedProperties, + [SymbolToStringTag]: { + __proto__: null, + configurable: true, + value: 'TextDecoder', + }, +}); + +module.exports = { + getEncodingFromLabel, + TextDecoder, + TextEncoder, +}; diff --git a/test/js/node/test/common/nodeinternals/internal/encoding/single-byte.js b/test/js/node/test/common/nodeinternals/internal/encoding/single-byte.js new file mode 100644 index 000000000000..abce7171fc66 --- /dev/null +++ b/test/js/node/test/common/nodeinternals/internal/encoding/single-byte.js @@ -0,0 +1,155 @@ +// Simplified version extracted from https://npmjs.com/package/@exodus/bytes codepath for 1-byte encodings +// Copyright Exodus Movement. Licensed under MIT License. + +'use strict'; + +const { + Array, + ArrayPrototypeFill, + ObjectKeys, + ObjectPrototypeHasOwnProperty, + SafeArrayIterator, + SafeMap, + SafeSet, + StringPrototypeIncludes, + TypedArrayFrom, + TypedArrayOf, + TypedArrayPrototypeIncludes, + TypedArrayPrototypeSet, + Uint16Array, +} = primordials; + +const { isAscii } = require('buffer'); + +const { FastBuffer } = require('internal/buffer'); + +const { + ERR_ENCODING_NOT_SUPPORTED, + ERR_ENCODING_INVALID_ENCODED_DATA, +} = require('internal/errors').codes; + +const isBigEndian = new FastBuffer(TypedArrayOf(Uint16Array, 258).buffer)[1] === 2; + +const it = (x) => new SafeArrayIterator(x); + +/* fallback/single-byte.encodings.js */ + +const r = 0xfffd; +const e = (x) => it(ArrayPrototypeFill(new Array(x), 1)); +const h = (x) => it(ArrayPrototypeFill(new Array(x), r)); + +/* eslint-disable @stylistic/js/max-len */ + +// Index tables from https://encoding.spec.whatwg.org/#legacy-single-byte-encodings +// Each table in the spec lists only mapping from byte 0x80 onwards, as below that they are all ASCII and mapped as identity +// Here, 0xfffd (replacement charcode) designates a hole (unmapped offset), as not all encodings map all offsets +// All other numbers are deltas from the last seen mapped value, starting with 0x7f (127, highest ASCII) +// Thus, [0x80, 0x81, , 0x83] is stored as [1, 1, r, 2] +// Truncation (length < 128) means that all remaining ones are mapped as identity (offset i => codepoint i), not unmapped +const encodings = { + '__proto__': null, + 'ibm866': [913, ...e(47), 8530, 1, 1, -145, 34, 61, 1, -12, -1, 14, -18, 6, 6, -1, -1, -75, 4, 32, -8, -16, -28, 60, 34, 1, -5, -6, 21, -3, -6, -16, 28, -5, 1, -4, 1, -12, -1, -6, 1, 24, -1, -82, -12, 124, -4, 8, 4, -16, -8512, ...e(15), -78, 80, -77, 80, -77, 80, -73, 80, -942, 8553, -8546, 8547, -260, -8306, 9468, -9472], + 'iso-8859-10': [...e(33), 100, 14, 16, 8, -2, 14, -143, 148, -43, 80, 6, 23, -208, 189, -32, -154, 85, 14, 16, 8, -2, 14, -128, 133, -43, 80, 6, 23, 7831, -7850, -32, -75, -63, ...e(5), 104, -34, -67, 79, -77, 75, -73, 1, 1, 1, 117, 7, -121, 1, 1, 1, 146, -144, 154, -152, ...e(5), 34, -32, ...e(5), 73, -34, -36, 48, -46, 44, -42, 1, 1, 1, 86, 7, -90, 1, 1, 1, 115, -113, 123, -121, 1, 1, 1, 1, 58], + 'iso-8859-13': [...e(33), 8061, -8059, 1, 1, 8058, -8056, 1, 49, -47, 173, -171, 1, 1, 1, 24, -22, 1, 1, 1, 8041, -8039, 1, 1, 65, -63, 158, -156, 1, 1, 1, 40, 30, 42, -46, 6, -66, 1, 83, -6, -6, -67, 176, -99, 12, 20, -12, 17, 37, -29, 2, -114, 121, -119, 1, 1, 155, -49, 25, 16, -142, 159, 2, -158, 38, 42, -46, 6, -35, 1, 52, -6, -6, -36, 145, -99, 12, 20, -12, 17, 37, -29, 2, -83, 90, -88, 1, 1, 124, -49, 25, 16, -111, 128, 2, 7835], + 'iso-8859-14': [...e(33), 7522, 1, -7520, 103, 1, 7423, -7523, 7641, -7639, 7641, -119, 231, -7749, 1, 202, 7334, 1, -7423, 1, 7455, 1, -7563, 7584, 43, -42, 44, -35, 147, -111, 1, -36, -7585, ...e(15), 165, -163, ...e(5), 7572, -7570, ...e(5), 153, -151, ...e(16), 134, -132, ...e(5), 7541, -7539, ...e(5), 122], + 'iso-8859-15': [...e(33), 1, 1, 1, 8201, -8199, 187, -185, 186, -184, ...e(10), 202, -200, 1, 1, 199, -197, 1, 1, 151, 1, 37], + 'iso-8859-16': [...e(33), 100, 1, 60, 8043, -142, -7870, -185, 186, -184, 367, -365, 206, -204, 205, 1, -203, 1, 91, 54, 59, 7840, -8039, 1, 199, -113, 268, -350, 151, 1, 37, 4, -188, 1, 1, 64, -62, 66, -64, ...e(9), 65, 51, -113, 1, 1, 124, -122, 132, 22, -151, 1, 1, 1, 60, 258, -315, 1, 1, 1, 33, -31, 35, -33, ...e(9), 34, 51, -82, 1, 1, 93, -91, 101, 22, -120, 1, 1, 1, 29, 258], + 'iso-8859-2': [...e(33), 100, 468, -407, -157, 153, 29, -179, 1, 184, -2, 6, 21, -204, 208, -2, -203, 85, 470, -409, -142, 138, 29, 364, -527, 169, -2, 6, 21, 355, -351, -2, -40, -147, 1, 64, -62, 117, -51, -63, 69, -67, 79, -77, 79, -77, 1, 64, 2, 51, 4, -116, 1, 124, -122, 1, 129, 22, -148, 150, -148, 1, 133, -131, 118, -116, 1, 33, -31, 86, -51, -32, 38, -36, 48, -46, 48, -46, 1, 33, 2, 51, 4, -85, 1, 93, -91, 1, 98, 22, -117, 119, -117, 1, 102, 374], + 'iso-8859-3': [...e(33), 134, 434, -565, 1, r, 128, -125, 1, 136, 46, -64, 22, -135, r, 206, -203, 119, -117, 1, 1, 1, 112, -110, 1, 121, 46, -64, 22, -120, r, 191, -188, 1, 1, r, 2, 70, -2, -65, ...e(8), r, 2, 1, 1, 1, 76, -74, 1, 69, -67, 1, 1, 1, 144, -16, -125, 1, 1, 1, r, 2, 39, -2, -34, ...e(8), r, 2, 1, 1, 1, 45, -43, 1, 38, -36, 1, 1, 1, 113, -16, 380], + 'iso-8859-4': [...e(33), 100, 52, 30, -178, 132, 19, -148, 1, 184, -78, 16, 68, -185, 208, -206, 1, 85, 470, -388, -163, 117, 19, 395, -527, 169, -78, 16, 68, -29, 52, -51, -75, -63, ...e(5), 104, -34, -67, 79, -77, 75, -73, 1, 92, -26, 53, 7, -22, -98, 1, 1, 1, 1, 154, -152, 1, 1, 140, 2, -139, 34, -32, ...e(5), 73, -34, -36, 48, -46, 44, -42, 1, 61, -26, 53, 7, -22, -67, 1, 1, 1, 1, 123, -121, 1, 1, 109, 2, 366], + 'iso-8859-5': [...e(33), 865, ...e(11), -863, 865, ...e(65), 7367, -7365, ...e(11), -949, 951, 1], + 'iso-8859-6': [...e(33), r, r, r, 4, ...h(7), 1384, -1375, ...h(13), 1390, r, r, r, 4, r, 2, ...e(25), r, r, r, r, r, 6, ...e(18), ...h(13)], + 'iso-8859-7': [...e(33), 8056, 1, -8054, 8201, 3, -8201, 1, 1, 1, 721, -719, 1, 1, r, 8040, -8037, 1, 1, 1, 721, 1, 1, -719, 721, 1, 1, -719, 721, -719, 721, ...e(19), r, 2, ...e(43), r], + 'iso-8859-8': [...e(33), r, 2, ...e(7), 46, -44, ...e(14), 62, -60, 1, 1, 1, ...h(32), 8025, -6727, ...e(26), r, r, 6692, 1, r], + 'koi8-r': [9345, 2, 10, 4, 4, 4, 4, 8, 8, 8, 8, 68, 4, 4, 4, 4, 1, 1, 1, -627, 640, -903, 1, 46, 28, 1, -8645, 8833, -8817, 2, 5, 64, 9305, 1, 1, -8449, 8450, ...e(14), -8544, 8545, ...e(10), -9411, 933, -30, 1, 21, -18, 1, 15, -17, 18, -13, ...e(7), 16, -15, 1, 1, 1, -13, -4, 26, -1, -20, 17, 5, -4, -2, 3, -28, -30, 1, 21, -18, 1, 15, -17, 18, -13, ...e(7), 16, -15, 1, 1, 1, -13, -4, 26, -1, -20, 17, 5, -4, -2, 3], + 'koi8-u': [9345, 2, 10, 4, 4, 4, 4, 8, 8, 8, 8, 68, 4, 4, 4, 4, 1, 1, 1, -627, 640, -903, 1, 46, 28, 1, -8645, 8833, -8817, 2, 5, 64, 9305, 1, 1, -8449, 3, 8448, -8446, 1, 8448, 1, 1, 1, 1, -8394, -51, 8448, 1, 1, 1, -8544, 3, 8543, -8541, 1, 8543, 1, 1, 1, 1, -8410, -130, -869, 933, -30, 1, 21, -18, 1, 15, -17, 18, -13, ...e(7), 16, -15, 1, 1, 1, -13, -4, 26, -1, -20, 17, 5, -4, -2, 3, -28, -30, 1, 21, -18, 1, 15, -17, 18, -13, ...e(7), 16, -15, 1, 1, 1, -13, -4, 26, -1, -20, 17, 5, -4, -2, 3], + 'macintosh': [69, 1, 2, 2, 8, 5, 6, 5, -1, 2, 2, -1, 2, 2, 2, -1, 2, 1, 2, -1, 2, 1, 2, 2, -1, 2, 2, -1, 5, -1, 2, 1, 7972, -8048, -14, 1, 4, 8059, -8044, 41, -49, -5, 8313, -8302, -12, 8632, -8602, 18, 8518, -8557, 8627, 1, -8640, 16, 8525, 15, -2, -7759, 7787, -8577, 16, 751, -707, 18, -57, -30, 11, 8558, -8328, 8374, -66, -8539, 16, 8043, -8070, 32, 3, 18, 125, 1, 7872, 1, 8, 1, -5, 1, -7970, 9427, -9419, 121, 7884, 104, -115, 1, 56007, 1, -56033, -8042, 8035, 4, 18, -8046, 8, -9, 10, -3, 5, 1, 1, -3, 7, 1, 63531, -63533, 8, 1, -2, 88, 405, 22, -557, 553, 1, 1, -546, 549, -2, -20], + 'windows-1250': [8237, -8235, 8089, -8087, 8091, 8, -6, 1, -8089, 8104, -7888, 7897, -7903, 10, 25, -4, -233, 8072, 1, 3, 1, 5, -15, 1, -8060, 8330, -8129, 7897, -7903, 10, 25, -4, -218, 551, 17, -407, -157, 96, -94, 1, 1, 1, 181, -179, 1, 1, 1, 205, -203, 1, 554, -409, -142, 1, 1, 1, 1, 77, 90, -164, 130, 416, -415, 62, -40, -147, 1, 64, -62, 117, -51, -63, 69, -67, 79, -77, 79, -77, 1, 64, 2, 51, 4, -116, 1, 124, -122, 1, 129, 22, -148, 150, -148, 1, 133, -131, 118, -116, 1, 33, -31, 86, -51, -32, 38, -36, 48, -46, 48, -46, 1, 33, 2, 51, 4, -85, 1, 93, -91, 1, 98, 22, -117, 119, -117, 1, 102, 374], + 'windows-1251': [899, 1, 7191, -7111, 7115, 8, -6, 1, 139, -124, -7207, 7216, -7215, 2, -1, 4, 67, 7110, 1, 3, 1, 5, -15, 1, -8060, 8330, -7369, 7137, -7136, 2, -1, 4, -959, 878, 80, -86, -868, 1004, -1002, 1, 858, -856, 859, -857, 1, 1, 1, 857, -855, 1, 853, 80, 59, -988, 1, 1, 922, 7365, -7362, -921, 925, -83, 80, 2, -71, ...e(63)], + 'windows-1252': [8237, -8235, 8089, -7816, 7820, 8, -6, 1, -7515, 7530, -7888, 7897, -7911, -197, 240, -238, 1, 8072, 1, 3, 1, 5, -15, 1, -7480, 7750, -8129, 7897, -7911, -182, 225, -6], + 'windows-1253': [8237, -8235, 8089, -7816, 7820, 8, -6, 1, -8089, 8104, -8102, 8111, -8109, 1, 1, 1, 1, 8072, 1, 3, 1, 5, -15, 1, -8060, 8330, -8328, 8096, -8094, 1, 1, 1, 1, 741, 1, -739, 1, 1, 1, 1, 1, 1, r, 2, 1, 1, 1, 8039, -8037, 1, 1, 1, 721, -719, 1, 1, 721, 1, 1, -719, 721, -719, 721, ...e(19), r, 2, ...e(43), r], + 'windows-1254': [8237, -8235, 8089, -7816, 7820, 8, -6, 1, -7515, 7530, -7888, 7897, -7911, -197, 1, 1, 1, 8072, 1, 3, 1, 5, -15, 1, -7480, 7750, -8129, 7897, -7911, -182, 1, 218, -216, ...e(47), 79, -77, ...e(11), 84, 46, -127, ...e(16), 48, -46, ...e(11), 53, 46], + 'windows-1255': [8237, -8235, 8089, -7816, 7820, 8, -6, 1, -7515, 7530, -8102, 8111, -8109, 1, 1, 1, 1, 8072, 1, 3, 1, 5, -15, 1, -7480, 7750, -8328, 8096, -8094, ...e(7), 8199, -8197, 1, 1, 1, 1, 46, -44, ...e(14), 62, -60, 1, 1, 1, 1, 1265, ...e(19), 45, 1, 1, 1, 1, ...h(7), -36, ...e(26), r, r, 6692, 1, r], + 'windows-1256': [8237, -6702, 6556, -7816, 7820, 8, -6, 1, -7515, 7530, -6583, 6592, -7911, 1332, 18, -16, 39, 6505, 1, 3, 1, 5, -15, 1, -6507, 6777, -6801, 6569, -7911, 7865, 1, -6483, -1562, 1388, -1386, ...e(7), 1557, -1555, ...e(14), 1378, -1376, 1, 1, 1, 1377, 162, -160, ...e(21), -1375, 1376, 1, 1, 1, 6, 1, 1, 1, -1379, 1380, -1378, 1379, 1, 1, 1, -1377, 1, 1, 1, 1, 1374, 1, -1372, 1, 1372, 1, 1, 1, -1370, 1371, 1, -1369, 1370, -1368, 1369, -1367, 1, 7954, 1, -6461], + 'windows-1257': [8237, -8235, 8089, -8087, 8091, 8, -6, 1, -8089, 8104, -8102, 8111, -8109, 28, 543, -527, -40, 8072, 1, 3, 1, 5, -15, 1, -8060, 8330, -8328, 8096, -8094, 19, 556, -572, 1, r, 2, 1, 1, r, 2, 1, 49, -47, 173, -171, 1, 1, 1, 24, -22, ...e(5), 1, 1, 65, -63, 158, -156, 1, 1, 1, 40, 30, 42, -46, 6, -66, 1, 83, -6, -6, -67, 176, -99, 12, 20, -12, 17, 37, -29, 2, -114, 121, -119, 1, 1, 155, -49, 25, 16, -142, 159, 2, -158, 38, 42, -46, 6, -35, 1, 52, -6, -6, -36, 145, -99, 12, 20, -12, 17, 37, -29, 2, -83, 90, -88, 1, 1, 124, -49, 25, 16, -111, 128, 2, 347], + 'windows-1258': [8237, -8235, 8089, -7816, 7820, 8, -6, 1, -7515, 7530, -8102, 8111, -7911, -197, 1, 1, 1, 8072, 1, 3, 1, 5, -15, 1, -7480, 7750, -8328, 8096, -7911, -182, 1, 218, -216, ...e(34), 64, -62, ...e(7), 565, -563, 1, 1, 65, -63, 568, -566, 1, 204, -202, 1, 1, 1, 1, 1, 1, 211, 340, -548, 1, 1, 1, 33, -31, ...e(7), 534, -532, 1, 1, 34, -32, 562, -560, 1, 173, -171, 1, 1, 1, 1, 1, 1, 180, 7931], + 'windows-874': [8237, -8235, 1, 1, 1, 8098, -8096, ...e(10), 8072, 1, 3, 1, 5, -15, 1, -8060, ...e(8), 3425, ...e(57), r, r, r, r, 5, ...e(28), r, r, r, r], + 'x-mac-cyrillic': [913, ...e(31), 7153, -8048, 992, -1005, 4, 8059, -8044, 848, -856, -5, 8313, -7456, 80, 7694, -7773, 80, 7627, -8557, 8627, 1, -7695, -929, 988, -137, -4, 80, -77, 80, -78, 80, -79, 80, -2, -83, -857, 8558, -8328, 8374, -66, -8539, 16, 8043, -8070, 875, 80, -79, 80, -7, 7102, 1, 8, 1, -5, 1, -7970, 7975, -7184, 80, -79, 80, 7351, -7445, 80, -2, -31, ...e(30), 7262], +}; + +/* eslint-enable @stylistic/js/max-len */ + +/* fallback/single-byte.js + single-byte.node.js, simplified */ + +const l256 = { __proto__: null, length: 256 }; + +function getEncoding(encoding) { + if (encoding === 'x-user-defined') { + // https://encoding.spec.whatwg.org/#x-user-defined-decoder, 14.5.1. x-user-defined decoder + return TypedArrayFrom(Uint16Array, l256, (_, i) => (i >= 0x80 ? 0xf700 + i : i)); + } + + if (!ObjectPrototypeHasOwnProperty(encodings, encoding)) { + throw new ERR_ENCODING_NOT_SUPPORTED(encoding); + } + + const map = TypedArrayFrom(Uint16Array, l256, (_, i) => i); // Unicode subset + let prev = 127; + map.set(TypedArrayFrom(Uint16Array, it(encodings[encoding]), (x) => (x === r ? x : (prev += x))), 128); + return map; +} + +const supported = new SafeSet(it(ObjectKeys(encodings))).add('iso-8859-8-i').add('x-user-defined'); +const isSinglebyteEncoding = (enc) => supported.has(enc); + +const decodersLoose = new SafeMap(); +const decodersFatal = new SafeMap(); + +function createSinglebyteDecoder(encoding, fatal) { + const id = encoding === 'iso-8859-8-i' ? 'iso-8859-8' : encoding; + const decoders = fatal ? decodersFatal : decodersLoose; + const cached = decoders.get(id); + if (cached) return cached; + + const map = getEncoding(id); + const incomplete = TypedArrayPrototypeIncludes(map, r); + + // Expects type-checked Buffer input + const decoder = (buf) => { + if (buf.byteLength === 0) return ''; + if (isAscii(buf)) return buf.latin1Slice(); // .latin1Slice is faster than .asciiSlice + const o = new Uint16Array(buf.length); + TypedArrayPrototypeSet(o, buf); // Copy to modify in-place, also those are 16-bit now + + let i = 0; + for (const end7 = o.length - 7; i < end7; i += 8) { + o[i] = map[o[i]]; + o[i + 1] = map[o[i + 1]]; + o[i + 2] = map[o[i + 2]]; + o[i + 3] = map[o[i + 3]]; + o[i + 4] = map[o[i + 4]]; + o[i + 5] = map[o[i + 5]]; + o[i + 6] = map[o[i + 6]]; + o[i + 7] = map[o[i + 7]]; + } + + for (const end = o.length; i < end; i++) o[i] = map[o[i]]; + + const b = new FastBuffer(o.buffer, o.byteOffset, o.byteLength); + if (isBigEndian) b.swap16(); + const string = b.ucs2Slice(); + if (fatal && incomplete && StringPrototypeIncludes(string, '\uFFFD')) { + throw new ERR_ENCODING_INVALID_ENCODED_DATA(encoding, undefined); + } + return string; + }; + + decoders.set(id, decoder); + return decoder; +} + +module.exports = { + isSinglebyteEncoding, + createSinglebyteDecoder, + getEncoding, // for tests +}; diff --git a/test/js/node/test/common/nodeinternals/internal/encoding/util.js b/test/js/node/test/common/nodeinternals/internal/encoding/util.js new file mode 100644 index 000000000000..107a0f41b5d8 --- /dev/null +++ b/test/js/node/test/common/nodeinternals/internal/encoding/util.js @@ -0,0 +1,72 @@ +// From https://npmjs.com/package/@exodus/bytes +// Copyright Exodus Movement. Licensed under MIT License. + +'use strict'; + +const { + Uint8Array, +} = primordials; + + +/** + * Get a number of last bytes in an Uint8Array `data` ending at `len` that don't + * form a codepoint yet, but can be a part of a single codepoint on more data. + * @param {Uint8Array} data Uint8Array of potentially UTF-8 bytes + * @param {number} len Position to look behind from + * @returns {number} Number of unfinished potentially valid UTF-8 bytes ending at position `len` + */ +function unfinishedBytesUtf8(data, len) { + // 0-3 + let pos = 0; + while (pos < 2 && pos < len && (data[len - pos - 1] & 0xc0) === 0x80) pos++; // Go back 0-2 trailing bytes + if (pos === len) return 0; // no space for lead + const lead = data[len - pos - 1]; + if (lead < 0xc2 || lead > 0xf4) return 0; // not a lead + if (pos === 0) return 1; // Nothing to recheck, we have only lead, return it. 2-byte must return here + if (lead < 0xe0 || (lead < 0xf0 && pos >= 2)) return 0; // 2-byte, or 3-byte or less and we already have 2 trailing + const lower = lead === 0xf0 ? 0x90 : lead === 0xe0 ? 0xa0 : 0x80; + const upper = lead === 0xf4 ? 0x8f : lead === 0xed ? 0x9f : 0xbf; + const next = data[len - pos]; + return next >= lower && next <= upper ? pos + 1 : 0; +} + +/** + * Merge prefix `chunk` with `data` and return new combined prefix. + * For data.length < 3, fully consumes data and can return unfinished data, + * otherwise returns a prefix with no unfinished bytes + * @param {Uint8Array} data Uint8Array of potentially UTF-8 bytes + * @param {Uint8Array} chunk Prefix to prepend before `data` + * @returns {Uint8Array} If data.length >= 3: an Uint8Array containing `chunk` and a slice of `data` + * so that the result has no unfinished UTF-8 codepoints. If data.length < 3: concat(chunk, data). + */ +function mergePrefixUtf8(data, chunk) { + if (data.length === 0) return chunk; + if (data.length < 3) { + // No reason to bruteforce offsets, also it's possible this doesn't yet end the sequence + const res = new Uint8Array(data.length + chunk.length); + res.set(chunk); + res.set(data, chunk.length); + return res; + } + + // Slice off a small portion of data into prefix chunk so we can decode them separately without extending array size + const temp = new Uint8Array(chunk.length + 3); // We have 1-3 bytes and need 1-3 more bytes + temp.set(chunk); + temp.set(data.subarray(0, 3), chunk.length); + + // Stop at the first offset where unfinished bytes reaches 0 or fits into data + // If that doesn't happen (data too short), just concat chunk and data completely (above) + for (let i = 1; i <= 3; i++) { + const unfinished = unfinishedBytesUtf8(temp, chunk.length + i); // 0-3 + if (unfinished <= i) { + // Always reachable at 3, but we still need 'unfinished' value for it + const add = i - unfinished; // 0-3 + return add > 0 ? temp.subarray(0, chunk.length + add) : chunk; + } + } + + // Unreachable + return null; +} + +module.exports = { unfinishedBytesUtf8, mergePrefixUtf8 }; diff --git a/test/js/node/test/parallel/test-whatwg-encoding-custom-internals.js b/test/js/node/test/parallel/test-whatwg-encoding-custom-internals.js new file mode 100644 index 000000000000..d4780c809588 --- /dev/null +++ b/test/js/node/test/parallel/test-whatwg-encoding-custom-internals.js @@ -0,0 +1,287 @@ +// Flags: --expose-internals +'use strict'; + +// This tests internal mapping of the Node.js encoding implementation + +require('../common'); + +const assert = require('assert'); +const { getEncodingFromLabel } = require('internal/encoding'); + +// Test Encoding Mappings +{ + const mappings = { + 'utf-8': [ + 'unicode-1-1-utf-8', + 'unicode11utf8', + 'unicode20utf8', + 'utf8', + 'x-unicode20utf8', + ], + 'utf-16be': [ + 'unicodefffe', + ], + 'utf-16le': [ + 'csunicode', + 'iso-10646-ucs-2', + 'ucs-2', + 'unicode', + 'unicodefeff', + 'utf-16', + ], + 'ibm866': [ + '866', + 'cp866', + 'csibm866', + ], + 'iso-8859-2': [ + 'csisolatin2', + 'iso-ir-101', + 'iso8859-2', + 'iso88592', + 'iso_8859-2', + 'iso_8859-2:1987', + 'l2', + 'latin2', + ], + 'iso-8859-3': [ + 'csisolatin3', + 'iso-ir-109', + 'iso8859-3', + 'iso88593', + 'iso_8859-3', + 'iso_8859-3:1988', + 'l3', + 'latin3', + ], + 'iso-8859-4': [ + 'csisolatin4', + 'iso-ir-110', + 'iso8859-4', + 'iso88594', + 'iso_8859-4', + 'iso_8859-4:1988', + 'l4', + 'latin4', + ], + 'iso-8859-5': [ + 'csisolatincyrillic', + 'cyrillic', + 'iso-ir-144', + 'iso8859-5', + 'iso88595', + 'iso_8859-5', + 'iso_8859-5:1988', + ], + 'iso-8859-6': [ + 'arabic', + 'asmo-708', + 'csiso88596e', + 'csiso88596i', + 'csisolatinarabic', + 'ecma-114', + 'iso-8859-6-e', + 'iso-8859-6-i', + 'iso-ir-127', + 'iso8859-6', + 'iso88596', + 'iso_8859-6', + 'iso_8859-6:1987', + ], + 'iso-8859-7': [ + 'csisolatingreek', + 'ecma-118', + 'elot_928', + 'greek', + 'greek8', + 'iso-ir-126', + 'iso8859-7', + 'iso88597', + 'iso_8859-7', + 'iso_8859-7:1987', + 'sun_eu_greek', + ], + 'iso-8859-8': [ + 'csiso88598e', + 'csisolatinhebrew', + 'hebrew', + 'iso-8859-8-e', + 'iso-ir-138', + 'iso8859-8', + 'iso88598', + 'iso_8859-8', + 'iso_8859-8:1988', + 'visual', + ], + 'iso-8859-8-i': [ + 'csiso88598i', + 'logical', + ], + 'iso-8859-10': [ + 'csisolatin6', + 'iso-ir-157', + 'iso8859-10', + 'iso885910', + 'l6', + 'latin6', + ], + 'iso-8859-13': [ + 'iso8859-13', + 'iso885913', + ], + 'iso-8859-14': [ + 'iso8859-14', + 'iso885914', + ], + 'iso-8859-15': [ + 'csisolatin9', + 'iso8859-15', + 'iso885915', + 'iso_8859-15', + 'l9', + ], + 'koi8-r': [ + 'cskoi8r', + 'koi', + 'koi8', + 'koi8_r', + ], + 'koi8-u': [ + 'koi8-ru', + ], + 'macintosh': [ + 'csmacintosh', + 'mac', + 'x-mac-roman', + ], + 'windows-874': [ + 'dos-874', + 'iso-8859-11', + 'iso8859-11', + 'iso885911', + 'tis-620', + ], + 'windows-1250': [ + 'cp1250', + 'x-cp1250', + ], + 'windows-1251': [ + 'cp1251', + 'x-cp1251', + ], + 'windows-1252': [ + 'ansi_x3.4-1968', + 'ascii', + 'cp1252', + 'cp819', + 'csisolatin1', + 'ibm819', + 'iso-8859-1', + 'iso-ir-100', + 'iso8859-1', + 'iso88591', + 'iso_8859-1', + 'iso_8859-1:1987', + 'l1', + 'latin1', + 'us-ascii', + 'x-cp1252', + ], + 'windows-1253': [ + 'cp1253', + 'x-cp1253', + ], + 'windows-1254': [ + 'cp1254', + 'csisolatin5', + 'iso-8859-9', + 'iso-ir-148', + 'iso8859-9', + 'iso88599', + 'iso_8859-9', + 'iso_8859-9:1989', + 'l5', + 'latin5', + 'x-cp1254', + ], + 'windows-1255': [ + 'cp1255', + 'x-cp1255', + ], + 'windows-1256': [ + 'cp1256', + 'x-cp1256', + ], + 'windows-1257': [ + 'cp1257', + 'x-cp1257', + ], + 'windows-1258': [ + 'cp1258', + 'x-cp1258', + ], + 'x-mac-cyrillic': [ + 'x-mac-ukrainian', + ], + 'gbk': [ + 'chinese', + 'csgb2312', + 'csiso58gb231280', + 'gb2312', + 'gb_2312', + 'gb_2312-80', + 'iso-ir-58', + 'x-gbk', + ], + 'gb18030': [ ], + 'big5': [ + 'big5-hkscs', + 'cn-big5', + 'csbig5', + 'x-x-big5', + ], + 'euc-jp': [ + 'cseucpkdfmtjapanese', + 'x-euc-jp', + ], + 'iso-2022-jp': [ + 'csiso2022jp', + ], + 'shift_jis': [ + 'csshiftjis', + 'ms932', + 'ms_kanji', + 'shift-jis', + 'sjis', + 'windows-31j', + 'x-sjis', + ], + 'euc-kr': [ + ' euc-kr \t', + 'EUC-kr \n', + 'cseuckr', + 'csksc56011987', + 'iso-ir-149', + 'korean', + 'ks_c_5601-1987', + 'ks_c_5601-1989', + 'ksc5601', + 'ksc_5601', + 'windows-949', + ], + 'replacement': [ + 'csiso2022kr', + 'hz-gb-2312', + 'iso-2022-cn', + 'iso-2022-cn-ext', + 'iso-2022-kr', + ], + 'x-user-defined': [] + }; + for (const [enc, labels] of Object.entries(mappings)) { + assert.strictEqual(getEncodingFromLabel(enc), enc); + labels.forEach((l) => assert.strictEqual(getEncodingFromLabel(l), enc)); + } + + assert.strictEqual(getEncodingFromLabel('made-up'), undefined); +} From ea2349c285865af80b19a98050a002d9d00d78c5 Mon Sep 17 00:00:00 2001 From: Ciro Spaciari MacBook Date: Fri, 24 Jul 2026 23:36:22 +0000 Subject: [PATCH 5/5] process: fix promise-rejection listener stacks and warning emission (+2 tests) Three node-parity fixes in the promise rejection plumbing: - 'unhandledRejection' listeners were called straight from native code with no JS frames on the stack, so Error.captureStackTrace(err, listener) inside a listener yielded an empty CallSite array (node's own common.mustNotCall crashes on stack[0]). Dispatch now goes through a JS trampoline that forwards to process.emit, so listeners see caller frames like node's processPromiseRejections path. Listener-exception handling is unchanged (same emitter underneath). - Warnings created while no JS is executing had stack === undefined; node always produces at least the ': ' header. The string branch of Process::emitWarning now writes that header when no stack was captured. - PromiseRejectionHandledWarning was emitted even when a 'rejectionHandled' listener existed; node only warns when nothing handled the event. Reordered to match. Passing: test-promise-unhandled-error-with-reading-file, test-promise-unhandled-warn; the rest of the vendored test-promise-unhandled-* / test-promises-* family still passes. --- src/js/builtins/ProcessObjectInternals.ts | 8 ++++ src/jsc/bindings/BunProcess.cpp | 41 ++++++++++++++++--- ...omise-unhandled-error-with-reading-file.js | 29 +++++++++++++ .../parallel/test-promise-unhandled-warn.js | 41 +++++++++++++++++++ 4 files changed, 114 insertions(+), 5 deletions(-) create mode 100644 test/js/node/test/parallel/test-promise-unhandled-error-with-reading-file.js create mode 100644 test/js/node/test/parallel/test-promise-unhandled-warn.js diff --git a/src/js/builtins/ProcessObjectInternals.ts b/src/js/builtins/ProcessObjectInternals.ts index 37681a67f7c6..59c26635f1a0 100644 --- a/src/js/builtins/ProcessObjectInternals.ts +++ b/src/js/builtins/ProcessObjectInternals.ts @@ -568,3 +568,11 @@ export function getChannel() { } })(); } + +// Called (with `this` = process) by the native promise-rejection tracker so +// 'unhandledRejection' listeners run with a JS frame on the stack: node +// dispatches through internal/process/promises and listeners may call +// Error.captureStackTrace(err, listener) expecting caller frames to remain. +export function emitUnhandledRejectionFromNative(reason, promise) { + return this.emit("unhandledRejection", reason, promise); +} diff --git a/src/jsc/bindings/BunProcess.cpp b/src/jsc/bindings/BunProcess.cpp index 13f684982669..3154f53300c6 100644 --- a/src/jsc/bindings/BunProcess.cpp +++ b/src/jsc/bindings/BunProcess.cpp @@ -1443,10 +1443,20 @@ extern "C" int Bun__handleUnhandledRejection(JSC::JSGlobalObject* lexicalGlobalO auto eventType = Identifier::fromString(JSC::getVM(globalObject), "unhandledRejection"_s); auto& wrapped = process->wrapped(); if (wrapped.listenerCount(eventType) > 0) { + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); + // Dispatch through a JS trampoline (which forwards to process.emit and + // the same listener store) so listeners run with JS caller frames, as + // in node; Error.captureStackTrace(err, listener) inside a listener + // must leave a non-empty stack. Listener exceptions are reported by + // the emitter itself, same as the direct wrapped.emit() path. + JSC::JSFunction* emitter = JSC::JSFunction::create(vm, globalObject, processObjectInternalsEmitUnhandledRejectionFromNativeCodeGenerator(vm), globalObject); MarkedArgumentBuffer args; args.append(reason); args.append(promise); - wrapped.emit(eventType, args); + auto callData = JSC::getCallData(emitter); + JSC::profiledCall(globalObject, JSC::ProfilingReason::API, emitter, callData, process, args); + CLEAR_IF_EXCEPTION(scope); return true; } @@ -1465,10 +1475,6 @@ extern "C" bool Bun__emitHandledPromiseEvent(JSC::JSGlobalObject* lexicalGlobalO auto eventType = Identifier::fromString(JSC::getVM(globalObject), "rejectionHandled"_s); - if (Bun__VM__allowRejectionHandledWarning(globalObject->bunVM())) { - Process::emitWarning(globalObject, jsString(globalObject->vm(), String("Promise rejection was handled asynchronously"_s)), jsString(globalObject->vm(), String("PromiseRejectionHandledWarning"_s)), jsUndefined(), jsUndefined()); - CLEAR_IF_EXCEPTION(scope); - } auto& wrapped = process->wrapped(); if (wrapped.listenerCount(eventType) > 0) { MarkedArgumentBuffer args; @@ -1477,6 +1483,13 @@ extern "C" bool Bun__emitHandledPromiseEvent(JSC::JSGlobalObject* lexicalGlobalO return true; } + // Node only warns when nothing handled the 'rejectionHandled' event + // (processPromiseRejections: `if (!process.emit('rejectionHandled', ...))`). + if (Bun__VM__allowRejectionHandledWarning(globalObject->bunVM())) { + Process::emitWarning(globalObject, jsString(globalObject->vm(), String("Promise rejection was handled asynchronously"_s)), jsString(globalObject->vm(), String("PromiseRejectionHandledWarning"_s)), jsUndefined(), jsUndefined()); + CLEAR_IF_EXCEPTION(scope); + } + return false; } @@ -2038,6 +2051,24 @@ JSValue Process::emitWarning(JSC::JSGlobalObject* lexicalGlobalObject, JSValue w auto s = warning.getString(globalObject); errorInstance = createError(globalObject, !s.isEmpty() ? s : "Warning"_s); errorInstance->putDirect(vm, vm.propertyNames->name, type, JSC::PropertyAttribute::DontEnum | 0); + // With no JS frames on the stack (native emission) the created error + // has no `stack` at all; node always produces at least the + // ": " header line and warning handlers read it. + JSValue existingStack = errorInstance->get(globalObject, vm.propertyNames->stack); + RETURN_IF_EXCEPTION(scope, {}); + bool stackMissing = existingStack.isUndefinedOrNull(); + if (!stackMissing && existingStack.isString()) { + auto stackString = existingStack.getString(globalObject); + RETURN_IF_EXCEPTION(scope, {}); + stackMissing = stackString.isEmpty(); + } + if (stackMissing) { + auto typeString = type.isString() ? type.getString(globalObject) : String("Warning"_s); + RETURN_IF_EXCEPTION(scope, {}); + errorInstance->putDirect(vm, vm.propertyNames->stack, + jsString(vm, makeString(typeString, ": "_s, !s.isEmpty() ? s : String("Warning"_s))), + static_cast(JSC::PropertyAttribute::DontEnum)); + } } else if (warning.isCell() && warning.asCell()->type() == ErrorInstanceType) { errorInstance = warning.getObject(); } else { diff --git a/test/js/node/test/parallel/test-promise-unhandled-error-with-reading-file.js b/test/js/node/test/parallel/test-promise-unhandled-error-with-reading-file.js new file mode 100644 index 000000000000..5a037eec7ca2 --- /dev/null +++ b/test/js/node/test/parallel/test-promise-unhandled-error-with-reading-file.js @@ -0,0 +1,29 @@ +// Flags: --unhandled-rejections=strict +'use strict'; + +const common = require('../common'); +const fs = require('fs'); +const assert = require('assert'); + +process.on('unhandledRejection', common.mustNotCall); + +process.on('uncaughtException', common.mustCall((err) => { + assert.ok(err.message.includes('foo')); +})); + + +async function readFile() { + return fs.promises.readFile(__filename); +} + +async function crash() { + throw new Error('foo'); +} + + +async function main() { + crash(); + readFile(); +} + +main(); diff --git a/test/js/node/test/parallel/test-promise-unhandled-warn.js b/test/js/node/test/parallel/test-promise-unhandled-warn.js new file mode 100644 index 000000000000..f13e6d29e2f0 --- /dev/null +++ b/test/js/node/test/parallel/test-promise-unhandled-warn.js @@ -0,0 +1,41 @@ +// Flags: --unhandled-rejections=warn +'use strict'; + +const common = require('../common'); +const assert = require('assert'); + +// Verify that ignoring unhandled rejection works fine and that no warning is +// logged. + +new Promise(() => { + throw new Error('One'); +}); + +Promise.reject('test'); + +function lookForMeInStackTrace() { + Promise.reject(new class ErrorLike { + constructor() { + Error.captureStackTrace(this); + this.message = 'ErrorLike'; + } + }()); +} +lookForMeInStackTrace(); + +// Unhandled rejections trigger two warning per rejection. One is the rejection +// reason and the other is a note where this warning is coming from. +process.on('warning', common.mustCall((reason) => { + if (reason.message.includes('ErrorLike')) { + assert.match(reason.stack, /lookForMeInStackTrace/); + } +}, 6)); +process.on('uncaughtException', common.mustNotCall('uncaughtException')); +process.on('rejectionHandled', common.mustCall(3)); + +process.on('unhandledRejection', (reason, promise) => { + // Handle promises but still warn! + promise.catch(() => {}); +}); + +setTimeout(common.mustCall(), 2);