diff --git a/src/js/builtins.d.ts b/src/js/builtins.d.ts index 727e7e9cef4d..81ff2e830357 100644 --- a/src/js/builtins.d.ts +++ b/src/js/builtins.d.ts @@ -405,6 +405,7 @@ declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedType: string, ac declare function $ERR_INVALID_ARG_TYPE(argName: string, expectedTypes: string[], actualValue: any): TypeError; declare function $ERR_INVALID_ARG_VALUE(name: string, value: any, reason?: string): TypeError; declare function $ERR_UNKNOWN_ENCODING(enc: string): TypeError; +declare function $ERR_UNKNOWN_SIGNAL(signal: string | number): TypeError; declare function $ERR_STREAM_DESTROYED(method: string): Error; declare function $ERR_METHOD_NOT_IMPLEMENTED(method: string): Error; declare function $ERR_STREAM_ALREADY_FINISHED(method: string): Error; diff --git a/src/js/node/child_process.ts b/src/js/node/child_process.ts index 29eee5b7b3f2..03eb2ec4444d 100644 --- a/src/js/node/child_process.ts +++ b/src/js/node/child_process.ts @@ -823,7 +823,7 @@ function convertToValidSignal(signal) { if (signalName) return signalName; } - throw ERR_UNKNOWN_SIGNAL(signal); + throw $ERR_UNKNOWN_SIGNAL(signal); } function sanitizeKillSignal(killSignal) { @@ -1955,12 +1955,6 @@ function genericNodeError(message, errorProperties) { return err; } -function ERR_UNKNOWN_SIGNAL(name) { - const err = new TypeError(`Unknown signal: ${name}`); - err.code = "ERR_UNKNOWN_SIGNAL"; - return err; -} - function ERR_INVALID_OPT_VALUE(name, value) { const err = new TypeError(`The value "${value}" is invalid for option "${name}"`); err.code = "ERR_INVALID_OPT_VALUE"; diff --git a/src/js/node/http2.ts b/src/js/node/http2.ts index 9c7747d20737..8aba70b64645 100644 --- a/src/js/node/http2.ts +++ b/src/js/node/http2.ts @@ -3852,7 +3852,8 @@ function receivedValueLabel(value) { if (typeof value === "function") return `function ${value.name}`; if (typeof value === "string") return `type string ('${value}')`; if (typeof value === "symbol") return `type symbol (${String(value)})`; - if (typeof value === "number") return `type number (${String(value)})`; + if (typeof value === "number") return `type number (${value === 0 && 1 / value < 0 ? "-0" : String(value)})`; + if (typeof value === "bigint") return `type bigint (${value}n)`; return `type ${typeof value} (${JSON.stringify(value)})`; } diff --git a/src/jsc/bindings/ErrorCode.cpp b/src/jsc/bindings/ErrorCode.cpp index e3818ae41c34..60ff51727f05 100644 --- a/src/jsc/bindings/ErrorCode.cpp +++ b/src/jsc/bindings/ErrorCode.cpp @@ -12,8 +12,11 @@ #include "helpers.h" #include "JavaScriptCore/JSCJSValue.h" #include "JavaScriptCore/ErrorInstance.h" +#include "JavaScriptCore/JSFunction.h" #include "JavaScriptCore/JSString.h" #include "JavaScriptCore/JSType.h" +#include "JavaScriptCore/MathCommon.h" +#include "JavaScriptCore/ProxyObject.h" #include "JavaScriptCore/Symbol.h" #include "wtf/Assertions.h" #include "wtf/Vector.h" @@ -351,6 +354,11 @@ void JSValueToStringSafe(JSC::JSGlobalObject* globalObject, MessageBuilder& buil { ASSERT(!arg.isEmpty()); if (!arg.isCell()) { + // util.inspect and %s keep the sign of -0 (lib/internal/util/inspect.js, formatNumber). ToString drops it. + if (arg.isDouble() && JSC::isNegativeZero(arg.asDouble())) { + builder.append("-0"_s); + return; + } builder.append(arg.toWTFStringForConsole(globalObject)); return; } @@ -412,6 +420,124 @@ void JSValueToStringSafe(JSC::JSGlobalObject* globalObject, MessageBuilder& buil builder.append(Bun__inspect_singleline(defaultGlobalObject(globalObject), arg).transferToWTFString()); } +// builtInObjects in node's lib/internal/util/inspect.js: the ECMAScript globals only. +static bool isBuiltInObjectName(const WTF::String& name) +{ + static constexpr ASCIILiteral names[] = { + "AggregateError"_s, "Array"_s, "ArrayBuffer"_s, "Atomics"_s, "BigInt"_s, "BigInt64Array"_s, + "BigUint64Array"_s, "Boolean"_s, "DataView"_s, "Date"_s, "Error"_s, "EvalError"_s, + "FinalizationRegistry"_s, "Float32Array"_s, "Float64Array"_s, "Function"_s, "Infinity"_s, + "Int16Array"_s, "Int32Array"_s, "Int8Array"_s, "Intl"_s, "Iterator"_s, "JSON"_s, "Map"_s, + "Math"_s, "NaN"_s, "Number"_s, "Object"_s, "Promise"_s, "Proxy"_s, "RangeError"_s, + "ReferenceError"_s, "Reflect"_s, "RegExp"_s, "Set"_s, "String"_s, "Symbol"_s, "SyntaxError"_s, + "TypeError"_s, "URIError"_s, "Uint16Array"_s, "Uint32Array"_s, "Uint8Array"_s, + "Uint8ClampedArray"_s, "WeakMap"_s, "WeakRef"_s, "WeakSet"_s + }; + for (const auto& candidate : names) { + if (name == candidate) + return true; + } + return false; +} + +// hasBuiltInToString in node's lib/internal/util/inspect.js. +static bool hasBuiltInToString(JSC::JSGlobalObject* globalObject, JSC::JSObject* object) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + while (auto* proxy = dynamicDowncast(object)) { + if (proxy->isRevoked()) + return true; + object = proxy->target(); + } + + const JSC::Identifier& toStringName = vm.propertyNames->toString; + const JSC::Identifier& toPrimitiveName = vm.propertyNames->toPrimitiveSymbol; + bool walkToString = true; + bool walkToPrimitive = true; + + JSValue toString = object->get(globalObject, toStringName); + RETURN_IF_EXCEPTION(scope, true); + JSValue toPrimitive = object->get(globalObject, toPrimitiveName); + RETURN_IF_EXCEPTION(scope, true); + if (!toString.isCallable()) { + if (!toPrimitive.isCallable()) + return true; + walkToString = false; + } else { + bool ownToString = object->hasOwnProperty(globalObject, toStringName); + RETURN_IF_EXCEPTION(scope, true); + if (ownToString) + return false; + walkToPrimitive = toPrimitive.isCallable(); + } + if (walkToPrimitive) { + bool ownToPrimitive = object->hasOwnProperty(globalObject, toPrimitiveName); + RETURN_IF_EXCEPTION(scope, true); + if (ownToPrimitive) + return false; + } + + // Find the prototype that owns the method, then ask whether its constructor is a built-in. + JSObject* pointer = object; + while (true) { + JSValue prototype = pointer->getPrototype(globalObject); + RETURN_IF_EXCEPTION(scope, true); + if (!prototype.isObject()) + return true; + pointer = JSC::asObject(prototype); + bool owns = false; + if (walkToString) { + owns = pointer->hasOwnProperty(globalObject, toStringName); + RETURN_IF_EXCEPTION(scope, true); + } + if (!owns && walkToPrimitive) { + owns = pointer->hasOwnProperty(globalObject, toPrimitiveName); + RETURN_IF_EXCEPTION(scope, true); + } + if (owns) + break; + } + + JSC::PropertySlot slot(pointer, JSC::PropertySlot::InternalMethodType::GetOwnProperty); + bool hasConstructor = pointer->getOwnPropertySlot(pointer, globalObject, vm.propertyNames->constructor, slot); + RETURN_IF_EXCEPTION(scope, true); + if (!hasConstructor || slot.isAccessor()) + return false; + JSValue constructor = slot.getValue(globalObject, vm.propertyNames->constructor); + RETURN_IF_EXCEPTION(scope, true); + if (!constructor.isCallable()) + return false; + JSValue name = constructor.get(globalObject, vm.propertyNames->name); + RETURN_IF_EXCEPTION(scope, true); + if (!name.isString()) + return false; + auto nameString = name.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, true); + return isBuiltInObjectName(nameString); +} + +void JSValueToStringLikeFormatS(JSC::JSGlobalObject* globalObject, MessageBuilder& builder, JSValue arg) +{ + auto& vm = JSC::getVM(globalObject); + auto scope = DECLARE_THROW_SCOPE(vm); + if (arg.isObject()) { + // typeof "function" values go through String() in node, so a function prints its source. + bool stringify = arg.isCallable(); + if (!stringify) { + stringify = !hasBuiltInToString(globalObject, arg.getObject()); + RETURN_IF_EXCEPTION(scope, ); + } + if (stringify) { + auto string = arg.toWTFString(globalObject); + RETURN_IF_EXCEPTION(scope, ); + builder.append(string); + return; + } + } + RELEASE_AND_RETURN(scope, JSValueToStringSafe(globalObject, builder, arg)); +} + void determineSpecificType(JSC::VM& vm, JSC::JSGlobalObject* globalObject, MessageBuilder& builder, JSValue value) { auto scope = DECLARE_TOP_EXCEPTION_SCOPE(vm); @@ -1260,20 +1386,11 @@ JSC::EncodedJSValue INVALID_FILE_URL_PATH(JSC::ThrowScope& throwScope, JSC::JSGl return {}; } -JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding) -{ - MessageBuilder message; - message.append("Unknown encoding: "_s, encoding); - throwScope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_ENCODING, message)); - throwScope.release(); - return {}; -} - JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& scope, JSGlobalObject* globalObject, JSValue encodingValue) { MessageBuilder builder; builder.append("Unknown encoding: "_s); - JSValueToStringSafe(globalObject, builder, encodingValue); + JSValueToStringLikeFormatS(globalObject, builder, encodingValue); RELEASE_RETURN_IF_EXCEPTION(scope, {}); scope.throwException(globalObject, createError(globalObject, ErrorCode::ERR_UNKNOWN_ENCODING, builder)); scope.release(); @@ -1878,9 +1995,9 @@ JSC::EncodedJSValue Bun::throwError(JSC::JSGlobalObject* globalObject, JSC::Thro namespace Bun { -// Error codes whose message is fixed text around one or two stringified -// arguments; `jsFunctionMakeErrorWithCode` builds these from the table instead -// of a switch case each. +// Error codes whose message is fixed text around one or two `%s` arguments; +// `jsFunctionMakeErrorWithCode` builds these from the table instead of a switch +// case each. struct SimpleErrorMessage { Bun::ErrorCode code; uint8_t argumentCount; @@ -1926,6 +2043,7 @@ static constexpr SimpleErrorMessage simpleErrorMessages[] = { { ErrorCode::ERR_VM_MODULE_STATUS, 1, { "Module status "_s, ""_s, ""_s } }, { ErrorCode::ERR_ZSTD_INVALID_PARAM, 1, { ""_s, " is not a valid zstd parameter"_s, ""_s } }, { ErrorCode::ERR_INSPECTOR_COMMAND, 1, { "Inspector error "_s, ""_s, ""_s } }, + { ErrorCode::ERR_UNKNOWN_SIGNAL, 1, { "Unknown signal: "_s, ""_s, ""_s } }, }; static JSC::EncodedJSValue makeSimpleErrorMessage(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame, JSC::ThrowScope& scope, const SimpleErrorMessage& entry) @@ -1933,9 +2051,8 @@ static JSC::EncodedJSValue makeSimpleErrorMessage(JSC::JSGlobalObject* globalObj MessageBuilder builder; builder.append(entry.pieces[0]); for (unsigned i = 0; i < entry.argumentCount; ++i) { - auto string = callFrame->argument(i + 1).toWTFString(globalObject); + JSValueToStringLikeFormatS(globalObject, builder, callFrame->argument(i + 1)); RETURN_IF_EXCEPTION(scope, {}); - builder.append(string); builder.append(entry.pieces[i + 1]); } return JSC::JSValue::encode(createError(globalObject, entry.code, builder)); @@ -2055,7 +2172,7 @@ JSC_DEFINE_HOST_FUNCTION_WITH_ATTRIBUTES(Bun::jsFunctionMakeErrorWithCode, __att auto arg0 = callFrame->argument(1); MessageBuilder builder; builder.append("Unknown encoding: "_s); - JSValueToStringSafe(globalObject, builder, arg0); + JSValueToStringLikeFormatS(globalObject, builder, arg0); RETURN_IF_EXCEPTION(scope, {}); return JSC::JSValue::encode(createError(globalObject, error, builder)); } diff --git a/src/jsc/bindings/ErrorCode.h b/src/jsc/bindings/ErrorCode.h index 84761a1c0f77..20de153012eb 100644 --- a/src/jsc/bindings/ErrorCode.h +++ b/src/jsc/bindings/ErrorCode.h @@ -106,6 +106,10 @@ void determineSpecificType(JSC::VM& vm, JSC::JSGlobalObject* globalObject, Messa // stringified, everything else through util.inspect. `quotesLikeInspect` quotes strings. void JSValueToStringSafe(JSC::JSGlobalObject* globalObject, MessageBuilder& builder, JSC::JSValue arg, bool quotesLikeInspect); +// Appends the value the way util.format's `%s` renders it: a function and an object without a +// built-in toString go through String(), any other object through util.inspect. +void JSValueToStringLikeFormatS(JSC::JSGlobalObject* globalObject, MessageBuilder& builder, JSC::JSValue arg); + enum Bound { LOWER, UPPER, @@ -134,7 +138,6 @@ JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobal JSC::EncodedJSValue INVALID_ARG_VALUE_RangeError(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, WTF::ASCIILiteral name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, JSC::JSValue name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); JSC::EncodedJSValue INVALID_ARG_VALUE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& name, JSC::JSValue value, const WTF::String& reason = "is invalid"_s); -JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::StringView encoding); JSC::EncodedJSValue UNKNOWN_ENCODING(JSC::ThrowScope&, JSC::JSGlobalObject*, JSValue encodingValue); JSC::EncodedJSValue INVALID_STATE(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject, const WTF::String& statemsg); JSC::EncodedJSValue STRING_TOO_LONG(JSC::ThrowScope& throwScope, JSC::JSGlobalObject* globalObject); diff --git a/src/jsc/bindings/JSBuffer.cpp b/src/jsc/bindings/JSBuffer.cpp index f9dd25d36898..326242cecaac 100644 --- a/src/jsc/bindings/JSBuffer.cpp +++ b/src/jsc/bindings/JSBuffer.cpp @@ -324,7 +324,7 @@ static WebCore::BufferEncodingType parseEncoding(JSC::ThrowScope& scope, JSC::JS Bun::V::validateString(scope, lexicalGlobalObject, arg, "encoding"_s); RETURN_IF_EXCEPTION(scope, WebCore::BufferEncodingType::utf8); } - Bun::ERR::UNKNOWN_ENCODING(scope, lexicalGlobalObject, view); + Bun::ERR::UNKNOWN_ENCODING(scope, lexicalGlobalObject, arg); return WebCore::BufferEncodingType::utf8; } @@ -675,11 +675,7 @@ static JSC::EncodedJSValue constructBufferFromStringAndEncoding(JSC::JSGlobalObj std::optional encoded = parseEnumeration(*lexicalGlobalObject, arg1); RETURN_IF_EXCEPTION(scope, {}); if (!encoded) { - auto* encodingString = arg1.toString(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); - const auto& view = encodingString->view(lexicalGlobalObject); - RETURN_IF_EXCEPTION(scope, {}); - return Bun::ERR::UNKNOWN_ENCODING(scope, lexicalGlobalObject, view); + return Bun::ERR::UNKNOWN_ENCODING(scope, lexicalGlobalObject, arg1); } encoding = encoded.value(); @@ -1850,7 +1846,7 @@ static int64_t indexOf(JSC::JSGlobalObject* lexicalGlobalObject, ThrowScope& sco if (valueValue.isString()) { if (!encoding.has_value()) { - return Bun::ERR::UNKNOWN_ENCODING(scope, lexicalGlobalObject, encodingString); + return Bun::ERR::UNKNOWN_ENCODING(scope, lexicalGlobalObject, encodingValue); } auto* str = valueValue.toString(lexicalGlobalObject); RETURN_IF_EXCEPTION(scope, -1); diff --git a/src/jsc/bindings/JSStringDecoder.cpp b/src/jsc/bindings/JSStringDecoder.cpp index 903c326d2934..13f7527fdcbc 100644 --- a/src/jsc/bindings/JSStringDecoder.cpp +++ b/src/jsc/bindings/JSStringDecoder.cpp @@ -582,11 +582,7 @@ static JSStringDecoder* createDecoder(JSC::JSGlobalObject* lexicalGlobalObject, if (opt.has_value()) { encoding = opt.value(); } else { - auto* encodingString = jsEncoding.toString(lexicalGlobalObject); - RETURN_IF_EXCEPTION(throwScope, nullptr); - const auto& view = encodingString->view(lexicalGlobalObject); - RETURN_IF_EXCEPTION(throwScope, nullptr); - Bun::ERR::UNKNOWN_ENCODING(throwScope, lexicalGlobalObject, view); + Bun::ERR::UNKNOWN_ENCODING(throwScope, lexicalGlobalObject, jsEncoding); return nullptr; } } diff --git a/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp b/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp index 05dbedda9385..4b7b0a01c3f9 100644 --- a/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp +++ b/src/jsc/bindings/webcore/streams/ReadableStreamOperations.cpp @@ -809,7 +809,7 @@ JSReadableStream* createReadableByteStream(JSGlobalObject* globalObject, SourceK static void throwNotIterable(JSGlobalObject* globalObject, JSC::ThrowScope& scope, JSValue iterable) { Bun::MessageBuilder builder; - Bun::JSValueToStringSafe(globalObject, builder, iterable, false); + Bun::JSValueToStringLikeFormatS(globalObject, builder, iterable); RETURN_IF_EXCEPTION(scope, ); builder.append(" must be iterable"_s); Bun::throwError(globalObject, scope, Bun::ErrorCode::ERR_ARG_NOT_ITERABLE, builder); diff --git a/src/runtime/node/node_crypto_binding.rs b/src/runtime/node/node_crypto_binding.rs index 5c76bcdec710..898fb18e5cf4 100644 --- a/src/runtime/node/node_crypto_binding.rs +++ b/src/runtime/node/node_crypto_binding.rs @@ -232,6 +232,28 @@ pub mod random { }; const MAX_RANGE: i64 = 0xffff_ffff_ffff; + // addNumericalSeparator in node's lib/internal/errors.js, applied past 2^32. + fn received_integer(value: i64) -> String { + let digits = value.to_string(); + if value.unsigned_abs() <= 1 << 32 { + return digits; + } + let (sign, digits) = match digits.strip_prefix('-') { + Some(rest) => ("-", rest), + None => ("", digits.as_str()), + }; + let mut out = String::with_capacity(digits.len() + digits.len() / 3 + 1); + out.push_str(sign); + let lead = digits.len() % 3; + for (i, ch) in digits.chars().enumerate() { + if i != 0 && (i + 3 - lead) % 3 == 0 { + out.push('_'); + } + out.push(ch); + } + out + } + impl JobContext for RandomFillJob { type OffThread = Self; type Js = RandomFillJs; @@ -348,38 +370,24 @@ pub mod random { let max: i64 = max_value.as_number().trunc() as i64; if max <= min { + let received = if max_value.as_number().is_sign_negative() && max == 0 { + String::from("-0") + } else { + received_integer(max) + }; return Err(global .err( jsc::ErrorCode::OUT_OF_RANGE, format_args!( "The value of \"max\" is out of range. It must be greater than the value of \"min\" ({}). Received {}", - min, max + min, received ), ) .throw()); } if max - min > MAX_RANGE { - // Node's ERR_OUT_OF_RANGE adds "_" numerical separators to integer - // "Received" values whose magnitude exceeds 2^32 - // (lib/internal/errors.js, addNumericalSeparator). - let received = { - let digits = (max - min).to_string(); - let (sign, digits) = match digits.strip_prefix('-') { - Some(rest) => ("-", rest), - None => ("", digits.as_str()), - }; - let mut out = String::with_capacity(digits.len() + digits.len() / 3 + 1); - out.push_str(sign); - let lead = digits.len() % 3; - for (i, ch) in digits.chars().enumerate() { - if i != 0 && (i + 3 - lead) % 3 == 0 { - out.push('_'); - } - out.push(ch); - } - out - }; + let received = received_integer(max - min); if min_specified { return Err(global .err( diff --git a/test/js/node/errors/error-code-messages.test.ts b/test/js/node/errors/error-code-messages.test.ts index cbb3d1222d79..b0b62a370280 100644 --- a/test/js/node/errors/error-code-messages.test.ts +++ b/test/js/node/errors/error-code-messages.test.ts @@ -1,7 +1,11 @@ import { expect, test } from "bun:test"; import child_process from "node:child_process"; +import crypto from "node:crypto"; import http from "node:http"; -import { Readable } from "node:stream"; +import http2 from "node:http2"; +import { createHistogram } from "node:perf_hooks"; +import { Readable, Writable } from "node:stream"; +import { StringDecoder } from "node:string_decoder"; import tls from "node:tls"; import zlib from "node:zlib"; @@ -35,3 +39,126 @@ test("table-driven ERR_* codes keep their exact messages", () => { "ERR_METHOD_NOT_IMPLEMENTED | Error | The _read() method is not implemented", ); }); + +// Node renders the value in these messages with util.inspect, or with the %s of util.format. +// Both keep the sign of -0. Node v26.3.0 prints these exact messages. +test("a received -0 keeps its sign at sites that use the shared value renderer", () => { + expect({ + // ERR_OUT_OF_RANGE thrown from C++, with numeric bounds and with a range string. + readUIntBE: capture(() => Buffer.alloc(8).readUIntBE(0, -0)), + percentile: capture(() => createHistogram().percentile(-0)), + // ERR_OUT_OF_RANGE thrown from JS ($ERR_OUT_OF_RANGE). + figures: capture(() => createHistogram({ figures: -0 })), + // ERR_INVALID_ARG_VALUE. + paramEncoding: capture(() => crypto.generateKeyPairSync("ec", { namedCurve: "P-256", paramEncoding: -0 as any })), + // %s codes. + setDefaultEncoding: capture(() => new Writable().setDefaultEncoding(-0 as any)), + readableStreamFrom: capture(() => ReadableStream.from(-0 as any)), + // Positive zero has no sign. + positiveZero: capture(() => Buffer.alloc(8).readUIntBE(0, 0)), + }).toEqual({ + readUIntBE: + 'ERR_OUT_OF_RANGE | RangeError | The value of "byteLength" is out of range. It must be >= 1 and <= 6. Received -0', + percentile: + 'ERR_OUT_OF_RANGE | RangeError | The value of "percentile" is out of range. It must be > 0 && <= 100. Received -0', + figures: + 'ERR_OUT_OF_RANGE | RangeError | The value of "options.figures" is out of range. It must be >= 1 && <= 5. Received -0', + paramEncoding: "ERR_INVALID_ARG_VALUE | TypeError | The property 'options.paramEncoding' is invalid. Received -0", + setDefaultEncoding: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: -0", + readableStreamFrom: "ERR_ARG_NOT_ITERABLE | TypeError | -0 must be iterable", + positiveZero: + 'ERR_OUT_OF_RANGE | RangeError | The value of "byteLength" is out of range. It must be >= 1 and <= 6. Received 0', + }); +}); + +test("a received -0 keeps its sign at sites that format the number themselves", () => { + expect({ + bufferToString: capture(() => Buffer.alloc(1).toString(-0 as any)), + stringDecoder: capture(() => new StringDecoder(-0 as any)), + setEncoding: capture(() => new Readable().setEncoding(-0 as any)), + randomInt: capture(() => crypto.randomInt(-0)), + randomIntWithMin: capture(() => crypto.randomInt(5, -0)), + getUnpackedSettings: capture(() => http2.getUnpackedSettings(-0 as any)), + validateHeaderName: capture(() => http.validateHeaderName(-0 as any)), + killSignal: capture(() => child_process.spawnSync("true", [], { killSignal: -0 as any })), + // Positive zero has no sign. + positiveZero: capture(() => crypto.randomInt(0)), + }).toEqual({ + bufferToString: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: -0", + stringDecoder: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: -0", + setEncoding: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: -0", + randomInt: + 'ERR_OUT_OF_RANGE | RangeError | The value of "max" is out of range. It must be greater than the value of "min" (0). Received -0', + randomIntWithMin: + 'ERR_OUT_OF_RANGE | RangeError | The value of "max" is out of range. It must be greater than the value of "min" (5). Received -0', + getUnpackedSettings: + 'ERR_INVALID_ARG_TYPE | TypeError | The "buf" argument must be an instance of Buffer or TypedArray. Received type number (-0)', + validateHeaderName: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["-0"]', + killSignal: "ERR_UNKNOWN_SIGNAL | TypeError | Unknown signal: -0", + positiveZero: + 'ERR_OUT_OF_RANGE | RangeError | The value of "max" is out of range. It must be greater than the value of "min" (0). Received 0', + }); +}); + +// The %s of util.format prints an object through String() when its toString or +// Symbol.toPrimitive is user code, and through util.inspect otherwise. +test("%s codes render an object like util.format", () => { + class Enc { + toString() { + return "enc!"; + } + } + const toPrimitive = { [Symbol.toPrimitive]: () => "prim" }; + expect({ + userToString: capture(() => Buffer.alloc(1).toString(new Enc() as any)), + userToPrimitive: capture(() => new StringDecoder(toPrimitive as any)), + proxied: capture(() => http.validateHeaderName(new Proxy(new Enc(), {}) as any)), + plainObject: capture(() => http.validateHeaderName({ a: 1 } as any)), + nullPrototype: capture(() => http.validateHeaderName(Object.create(null))), + builtinToString: capture(() => http.validateHeaderName(new Date(0) as any)), + ownBoundToString: capture(() => http.validateHeaderName({ toString: (() => "bound").bind(null) } as any)), + inheritedToString: capture(() => http.validateHeaderName(new (class extends Enc {})() as any)), + inheritedToPrimitive: capture(() => http.validateHeaderName({ __proto__: toPrimitive } as any)), + // Only the ECMAScript globals count as built-in constructors. Buffer, TypedArray and URL do not. + buffer: capture(() => http.validateHeaderName(Buffer.from("a") as any)), + uint8Array: capture(() => http.validateHeaderName(new Uint8Array([1]) as any)), + url: capture(() => http.validateHeaderName(new URL("http://x/") as any)), + toStringNotCallable: capture(() => http.validateHeaderName({ toString: 5 } as any)), + fn: capture(() => http.validateHeaderName(function foo() {} as any)), + notIterable: capture(() => ReadableStream.from(new Enc() as any)), + }).toEqual({ + userToString: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: enc!", + userToPrimitive: "ERR_UNKNOWN_ENCODING | TypeError | Unknown encoding: prim", + proxied: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["enc!"]', + plainObject: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["{ a: 1 }"]', + nullPrototype: + 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["[Object: null prototype] {}"]', + builtinToString: + 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["1970-01-01T00:00:00.000Z"]', + ownBoundToString: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["bound"]', + inheritedToString: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["enc!"]', + inheritedToPrimitive: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["prim"]', + buffer: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["a"]', + uint8Array: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["1"]', + url: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["http://x/"]', + toStringNotCallable: + 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["{ toString: 5 }"]', + fn: 'ERR_INVALID_HTTP_TOKEN | TypeError | Header name must be a valid HTTP token ["function foo() {}"]', + notIterable: "ERR_ARG_NOT_ITERABLE | TypeError | enc! must be iterable", + }); +}); + +test("randomInt renders a large received max with numerical separators like node", () => { + expect({ + large: capture(() => crypto.randomInt(2 ** 40, 2 ** 40 - 1)), + negative: capture(() => crypto.randomInt(1, -5_000_000_000)), + bigintBuf: capture(() => http2.getUnpackedSettings(1n as any)), + }).toEqual({ + large: + 'ERR_OUT_OF_RANGE | RangeError | The value of "max" is out of range. It must be greater than the value of "min" (1099511627776). Received 1_099_511_627_775', + negative: + 'ERR_OUT_OF_RANGE | RangeError | The value of "max" is out of range. It must be greater than the value of "min" (1). Received -5_000_000_000', + bigintBuf: + 'ERR_INVALID_ARG_TYPE | TypeError | The "buf" argument must be an instance of Buffer or TypedArray. Received type bigint (1n)', + }); +});