From 66878e10dd1139edba1a5c36d17887837b7993f0 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Wed, 17 Jun 2026 17:24:10 +0530 Subject: [PATCH 1/2] feat(node:buffer): implement transcode() `buffer.transcode(source, fromEncoding, toEncoding)` previously threw "Not implemented". Implement it for the encodings Node supports (ascii, latin1/binary, ucs2/utf16le, utf8), substituting '?' for code points the target encoding cannot represent. The dispatch mirrors Node's src/node_i18n.cc: simdutf fast paths for ASCII/Latin-1 -> UCS2 and UTF8 <-> UCS2, with a UTF-16 pivot for the remaining pairs. Invalid arguments throw the same errors as Node (ERR_INVALID_ARG_TYPE, "Unable to transcode Buffer [...]"). Closes #24235 --- src/jsc/modules/NodeBufferModule.h | 351 ++++++++++++++++++++++++++++- test/js/node/buffer.test.js | 51 ++++- 2 files changed, 390 insertions(+), 12 deletions(-) diff --git a/src/jsc/modules/NodeBufferModule.h b/src/jsc/modules/NodeBufferModule.h index c679ae05d490..c8ceda0e3bb4 100644 --- a/src/jsc/modules/NodeBufferModule.h +++ b/src/jsc/modules/NodeBufferModule.h @@ -8,7 +8,10 @@ #include "NodeValidator.h" #include "_NativeModule.h" #include "wtf/SIMDUTF.h" +#include "BufferEncodingType.h" +#include "JSBufferEncodingType.h" #include +#include namespace Zig { using namespace WebCore; @@ -125,16 +128,348 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferConstructorFunction_isAscii, BUN_DECLARE_HOST_FUNCTION(jsFunctionResolveObjectURL); -JSC_DEFINE_HOST_FUNCTION(jsFunctionNotImplemented, - (JSGlobalObject * globalObject, +namespace { + +// The encodings buffer.transcode() can convert between, mirroring the subset +// ICU supports in Node's src/node_i18n.cc (ASCII, LATIN1, UCS2, UTF8). +enum class TranscodeEncoding : uint8_t { + ASCII, + Latin1, + UCS2, + UTF8, + Unsupported, +}; + +static TranscodeEncoding toTranscodeEncoding(WebCore::BufferEncodingType encoding) +{ + switch (encoding) { + case WebCore::BufferEncodingType::ascii: + return TranscodeEncoding::ASCII; + case WebCore::BufferEncodingType::latin1: + return TranscodeEncoding::Latin1; + case WebCore::BufferEncodingType::ucs2: + case WebCore::BufferEncodingType::utf16le: + return TranscodeEncoding::UCS2; + case WebCore::BufferEncodingType::utf8: + return TranscodeEncoding::UTF8; + default: + return TranscodeEncoding::Unsupported; + } +} + +// Node's buffer.transcode honors only string encoding arguments; any other +// value parses as the default BUFFER encoding, which then fails the supported +// check. Restricting to strings here also avoids running user toString() code +// that could detach the source view mid-call. +static TranscodeEncoding parseTranscodeEncoding(JSC::JSGlobalObject* globalObject, JSC::JSValue value) +{ + auto* string = dynamicDowncast(value); + if (!string) [[unlikely]] + return TranscodeEncoding::Unsupported; + + const auto view = string->view(globalObject); + auto parsed = WebCore::parseEnumerationFromView(view); + if (!parsed) + return TranscodeEncoding::Unsupported; + + return toTranscodeEncoding(parsed.value()); +} + +// Mirrors the error Node throws from buffer.transcode: a generic Error whose +// message embeds the ICU status name, with matching `code` and `errno`. +static JSC::EncodedJSValue throwTranscodeError(JSC::ThrowScope& scope, JSC::JSGlobalObject* globalObject, UErrorCode status) +{ + auto& vm = JSC::getVM(globalObject); + ASCIILiteral code = status == U_INVALID_CHAR_FOUND ? "U_INVALID_CHAR_FOUND"_s : "U_ILLEGAL_ARGUMENT_ERROR"_s; + auto* error = createError(globalObject, makeString("Unable to transcode Buffer ["_s, code, "]"_s)); + error->putDirect(vm, JSC::Identifier::fromString(vm, "code"_s), JSC::jsString(vm, String(code)), 0); + error->putDirect(vm, JSC::Identifier::fromString(vm, "errno"_s), JSC::jsNumber(static_cast(status)), 0); + scope.throwException(globalObject, error); + return {}; +} + +static void appendCodePoint(Vector& out, char32_t codePoint) +{ + if (codePoint <= 0xFFFF) { + out.append(static_cast(codePoint)); + return; + } + codePoint -= 0x10000; + out.append(static_cast(0xD800 + (codePoint >> 10))); + out.append(static_cast(0xDC00 + (codePoint & 0x3FF))); +} + +// UTF-8 -> UTF-16 decode that substitutes U+FFFD for ill-formed input using the +// Unicode "maximal subpart" rule (matching ICU's to-Unicode substitution, which +// is what Node's ICU generic path relies on). Only invoked on input simdutf has +// already rejected as invalid. +static Vector decodeUtf8Replacing(std::span input) +{ + Vector out; + const size_t length = input.size(); + for (size_t i = 0; i < length;) { + const uint8_t lead = input[i]; + if (lead < 0x80) { + out.append(lead); + i++; + continue; + } + + size_t continuations; + char32_t codePoint; + uint8_t lowerBound = 0x80; + uint8_t upperBound = 0xBF; + if (lead >= 0xC2 && lead <= 0xDF) { + continuations = 1; + codePoint = lead & 0x1F; + } else if (lead == 0xE0) { + continuations = 2; + codePoint = lead & 0x0F; + lowerBound = 0xA0; + } else if (lead >= 0xE1 && lead <= 0xEC) { + continuations = 2; + codePoint = lead & 0x0F; + } else if (lead == 0xED) { + continuations = 2; + codePoint = lead & 0x0F; + upperBound = 0x9F; + } else if (lead >= 0xEE && lead <= 0xEF) { + continuations = 2; + codePoint = lead & 0x0F; + } else if (lead == 0xF0) { + continuations = 3; + codePoint = lead & 0x07; + lowerBound = 0x90; + } else if (lead >= 0xF1 && lead <= 0xF3) { + continuations = 3; + codePoint = lead & 0x07; + } else if (lead == 0xF4) { + continuations = 3; + codePoint = lead & 0x07; + upperBound = 0x8F; + } else { + out.append(0xFFFD); + i++; + continue; + } + + size_t consumed = i + 1; + bool valid = true; + for (size_t k = 0; k < continuations; k++) { + const uint8_t low = k == 0 ? lowerBound : 0x80; + const uint8_t high = k == 0 ? upperBound : 0xBF; + if (consumed >= length || input[consumed] < low || input[consumed] > high) { + valid = false; + break; + } + codePoint = (codePoint << 6) | (input[consumed] & 0x3F); + consumed++; + } + + if (!valid) { + out.append(0xFFFD); + i = consumed; + continue; + } + + appendCodePoint(out, codePoint); + i = consumed; + } + return out; +} + +// Decode source bytes into a UTF-16 pivot. ASCII bytes >= 0x80 become U+FFFD, +// matching ICU's us-ascii to-Unicode behavior used by Node's generic path (this +// helper feeds the generic path only; ASCII/LATIN1 -> UCS2 takes the dedicated +// simdutf path that treats ASCII as Latin-1, as Node does). +static Vector decodeToUtf16(TranscodeEncoding from, std::span source) +{ + Vector pivot; + switch (from) { + case TranscodeEncoding::ASCII: + pivot.reserveInitialCapacity(source.size()); + for (uint8_t byte : source) + pivot.append(byte < 0x80 ? static_cast(byte) : 0xFFFD); + break; + case TranscodeEncoding::Latin1: { + pivot.grow(source.size()); + [[maybe_unused]] const size_t converted = simdutf::convert_latin1_to_utf16le(reinterpret_cast(source.data()), source.size(), pivot.mutableSpan().data()); + break; + } + case TranscodeEncoding::UTF8: { + const char* pointer = reinterpret_cast(source.data()); + if (simdutf::validate_utf8(pointer, source.size())) { + pivot.grow(simdutf::utf16_length_from_utf8(pointer, source.size())); + [[maybe_unused]] const size_t converted = simdutf::convert_utf8_to_utf16le(pointer, source.size(), pivot.mutableSpan().data()); + } else { + pivot = decodeUtf8Replacing(source); + } + break; + } + case TranscodeEncoding::UCS2: { + const size_t lengthInChars = source.size() / sizeof(char16_t); + pivot.grow(lengthInChars); + memcpy(pivot.mutableSpan().data(), source.data(), lengthInChars * sizeof(char16_t)); + break; + } + default: + break; + } + return pivot; +} + +// Encode a UTF-16 pivot into the target encoding, substituting '?' for code +// points the target cannot represent (matching ICU's from-Unicode substitution). +static Vector encodeFromUtf16(TranscodeEncoding to, std::span pivot) +{ + Vector out; + switch (to) { + case TranscodeEncoding::ASCII: + case TranscodeEncoding::Latin1: { + const char32_t maxCodePoint = to == TranscodeEncoding::ASCII ? 0x7F : 0xFF; + out.reserveInitialCapacity(pivot.size()); + for (size_t i = 0; i < pivot.size();) { + char32_t codePoint = pivot[i++]; + // Combine a surrogate pair so one supplementary code point maps to a + // single substitution character rather than two. + if (codePoint >= 0xD800 && codePoint <= 0xDBFF && i < pivot.size() && pivot[i] >= 0xDC00 && pivot[i] <= 0xDFFF) + codePoint = 0x10000 + ((codePoint - 0xD800) << 10) + (pivot[i++] - 0xDC00); + out.append(codePoint <= maxCodePoint ? static_cast(codePoint) : '?'); + } + break; + } + case TranscodeEncoding::UCS2: + out.grow(pivot.size() * sizeof(char16_t)); + memcpy(out.mutableSpan().data(), pivot.data(), pivot.size() * sizeof(char16_t)); + break; + case TranscodeEncoding::UTF8: { + out.grow(simdutf::utf8_length_from_utf16le(pivot.data(), pivot.size())); + const size_t length = simdutf::convert_utf16le_to_utf8(pivot.data(), pivot.size(), reinterpret_cast(out.mutableSpan().data())); + out.shrink(length); + break; + } + default: + break; + } + return out; +} + +// Node's generic Transcode() and TranscodeFromUcs2(): pivot through UTF-16 with +// '?' substitution. Always succeeds for the supported encodings. +static JSC::JSUint8Array* transcodeGeneric(JSC::JSGlobalObject* globalObject, TranscodeEncoding from, TranscodeEncoding to, std::span source) +{ + const Vector pivot = decodeToUtf16(from, source); + const Vector result = encodeFromUtf16(to, pivot.span()); + return WebCore::createBuffer(globalObject, result.span()); +} + +// Node's TranscodeLatin1ToUcs2(): ASCII/LATIN1 -> UCS2 via simdutf. +static JSC::JSUint8Array* transcodeLatin1ToUcs2(JSC::JSGlobalObject* globalObject, std::span source, UErrorCode* status) +{ + Vector result(source.size()); + const size_t length = simdutf::convert_latin1_to_utf16le(reinterpret_cast(source.data()), source.size(), result.mutableSpan().data()); + if (!length) { + *status = U_INVALID_CHAR_FOUND; + return nullptr; + } + + return WebCore::createBuffer(globalObject, reinterpret_cast(result.span().data()), length * sizeof(char16_t)); +} + +// Node's TranscodeUcs2FromUtf8(): UTF8 -> UCS2 via simdutf. +static JSC::JSUint8Array* transcodeUcs2FromUtf8(JSC::JSGlobalObject* globalObject, std::span source, UErrorCode* status) +{ + const char* sourcePointer = reinterpret_cast(source.data()); + Vector result(simdutf::utf16_length_from_utf8(sourcePointer, source.size())); + const size_t length = simdutf::convert_utf8_to_utf16le(sourcePointer, source.size(), result.mutableSpan().data()); + if (!length) { + *status = U_INVALID_CHAR_FOUND; + return nullptr; + } + + return WebCore::createBuffer(globalObject, reinterpret_cast(result.span().data()), length * sizeof(char16_t)); +} + +// Node's TranscodeUtf8FromUcs2(): UCS2 -> UTF8 via simdutf. +static JSC::JSUint8Array* transcodeUtf8FromUcs2(JSC::JSGlobalObject* globalObject, std::span source, UErrorCode* status) +{ + const size_t lengthInChars = source.size() / sizeof(char16_t); + Vector sourceChars(lengthInChars); + memcpy(sourceChars.mutableSpan().data(), source.data(), lengthInChars * sizeof(char16_t)); + + Vector result(simdutf::utf8_length_from_utf16le(sourceChars.span().data(), lengthInChars)); + const size_t length = simdutf::convert_utf16le_to_utf8(sourceChars.span().data(), lengthInChars, result.mutableSpan().data()); + if (!length) { + *status = U_INVALID_CHAR_FOUND; + return nullptr; + } + + return WebCore::createBuffer(globalObject, reinterpret_cast(result.span().data()), length); +} + +} // namespace + +// buffer.transcode(source, fromEncoding, toEncoding). Ports the dispatch in +// Node's src/node_i18n.cc Transcode(). +JSC_DEFINE_HOST_FUNCTION(jsBufferConstructorFunction_transcode, + (JSGlobalObject * lexicalGlobalObject, CallFrame* callFrame)) { - VM& vm = globalObject->vm(); + auto& vm = JSC::getVM(lexicalGlobalObject); auto scope = DECLARE_THROW_SCOPE(vm); - throwException(globalObject, scope, - createError(globalObject, "Not implemented"_s)); - return {}; + JSValue sourceValue = callFrame->argument(0); + auto* source = dynamicDowncast(sourceValue); + if (!source) [[unlikely]] + return Bun::ERR::INVALID_ARG_TYPE_INSTANCE(scope, lexicalGlobalObject, "source"_s, "Buffer or Uint8Array"_s, sourceValue); + + const size_t byteLength = source->byteLength(); + if (!byteLength) + return JSValue::encode(WebCore::createEmptyBuffer(lexicalGlobalObject)); + + const TranscodeEncoding fromEncoding = parseTranscodeEncoding(lexicalGlobalObject, callFrame->argument(1)); + RETURN_IF_EXCEPTION(scope, {}); + const TranscodeEncoding toEncoding = parseTranscodeEncoding(lexicalGlobalObject, callFrame->argument(2)); + RETURN_IF_EXCEPTION(scope, {}); + + if (fromEncoding == TranscodeEncoding::Unsupported || toEncoding == TranscodeEncoding::Unsupported) + return throwTranscodeError(scope, lexicalGlobalObject, U_ILLEGAL_ARGUMENT_ERROR); + + // Copy the source bytes so the conversion routines work on a stable, + // aligned buffer (the UCS2 paths reinterpret these bytes as char16_t). + Vector sourceBytes(byteLength); + memcpy(sourceBytes.mutableSpan().data(), source->typedVector(), byteLength); + const std::span sourceSpan = sourceBytes.span(); + + UErrorCode status = U_ZERO_ERROR; + JSC::JSUint8Array* result = nullptr; + switch (fromEncoding) { + case TranscodeEncoding::ASCII: + case TranscodeEncoding::Latin1: + result = toEncoding == TranscodeEncoding::UCS2 + ? transcodeLatin1ToUcs2(lexicalGlobalObject, sourceSpan, &status) + : transcodeGeneric(lexicalGlobalObject, fromEncoding, toEncoding, sourceSpan); + break; + case TranscodeEncoding::UTF8: + result = toEncoding == TranscodeEncoding::UCS2 + ? transcodeUcs2FromUtf8(lexicalGlobalObject, sourceSpan, &status) + : transcodeGeneric(lexicalGlobalObject, fromEncoding, toEncoding, sourceSpan); + break; + case TranscodeEncoding::UCS2: + // UCS2 -> UTF8 takes the dedicated simdutf path; UCS2 -> UCS2/ASCII/LATIN1 + // pivot through the generic path. + result = toEncoding == TranscodeEncoding::UTF8 + ? transcodeUtf8FromUcs2(lexicalGlobalObject, sourceSpan, &status) + : transcodeGeneric(lexicalGlobalObject, fromEncoding, toEncoding, sourceSpan); + break; + default: + break; + } + + if (result) + return JSValue::encode(result); + + return throwTranscodeError(scope, lexicalGlobalObject, status); } JSC_DEFINE_CUSTOM_GETTER(jsGetter_INSPECT_MAX_BYTES, (JSGlobalObject * lexicalGlobalObject, JSC::EncodedJSValue thisValue, PropertyName propertyName)) @@ -203,9 +538,7 @@ DEFINE_NATIVE_MODULE(NodeBuffer) put(atobI, atobV); put(btoaI, btoaV); - auto* transcode = InternalFunction::createFunctionThatMasqueradesAsUndefined(vm, globalObject, 1, "transcode"_s, jsFunctionNotImplemented); - - put(JSC::Identifier::fromString(vm, "transcode"_s), transcode); + put(JSC::Identifier::fromString(vm, "transcode"_s), JSC::JSFunction::create(vm, globalObject, 3, "transcode"_s, jsBufferConstructorFunction_transcode, ImplementationVisibility::Public, NoIntrinsic, jsBufferConstructorFunction_transcode)); auto* resolveObjectURL = JSC::JSFunction::create(vm, globalObject, 1, "resolveObjectURL"_s, jsFunctionResolveObjectURL, ImplementationVisibility::Public, NoIntrinsic, jsFunctionResolveObjectURL); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 244f51ae9d8f..412b5318d76d 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -2535,10 +2535,55 @@ for (let withOverridenBufferWrite of [false, true]) { }); it("transcode", () => { - expect(typeof BufferModule.transcode).toBe("undefined"); + expect(typeof BufferModule.transcode).toBe("function"); - // This is a masqueradesAsUndefined function - expect(() => BufferModule.transcode()).toThrow("Not implemented"); + // Unmappable code points are replaced with '?' (issue #24235). + expect(BufferModule.transcode(Buffer.from("€"), "utf8", "ascii").toString("ascii")).toBe("?"); + + // Vectors from Node's test/parallel/test-icu-transcode.js. + const orig = Buffer.from("těst ☕", "utf8"); + expect([...BufferModule.transcode(orig, "utf8", "latin1")]).toEqual([0x74, 0x3f, 0x73, 0x74, 0x20, 0x3f]); + expect([...BufferModule.transcode(orig, "utf8", "ascii")]).toEqual([0x74, 0x3f, 0x73, 0x74, 0x20, 0x3f]); + const ucs2 = [0x74, 0x00, 0x1b, 0x01, 0x73, 0x00, 0x74, 0x00, 0x20, 0x00, 0x15, 0x26]; + expect([...BufferModule.transcode(orig, "utf8", "ucs2")]).toEqual(ucs2); + + // ucs2 -> utf8 round trips back to the original. + expect(BufferModule.transcode(Buffer.from(ucs2), "ucs2", "utf8").toString()).toBe(orig.toString()); + + // ascii/latin1 -> utf16le. + expect(BufferModule.transcode(Buffer.from("hi", "ascii"), "ascii", "utf16le")).toEqual(Buffer.from("hi", "utf16le")); + expect(BufferModule.transcode(Buffer.from("hä", "latin1"), "latin1", "utf16le")).toEqual(Buffer.from("hä", "utf16le")); + + // A plain Uint8Array is accepted as the source. + const u8 = new Uint8Array([...Buffer.from("hä", "latin1")]); + expect(BufferModule.transcode(u8, "latin1", "utf16le")).toEqual(Buffer.from("hä", "utf16le")); + + // Empty input short-circuits to an empty Buffer without touching the encodings. + const empty = BufferModule.transcode(Buffer.alloc(0), "utf8", "ucs2"); + expect(empty.length).toBe(0); + expect(Buffer.isBuffer(empty)).toBe(true); + + // Non-Uint8Array source throws ERR_INVALID_ARG_TYPE. + expect(() => BufferModule.transcode(null, "utf8", "ascii")).toThrow( + 'The "source" argument must be an instance of Buffer or Uint8Array. Received null', + ); + + // Unsupported/unknown encodings throw the ICU illegal-argument error. + for (const args of [ + [Buffer.from("a"), "b", "utf8"], + [Buffer.from("a"), "utf8", "b"], + [Buffer.from("a"), "hex", "utf8"], + ]) { + let err; + try { + BufferModule.transcode(...args); + } catch (e) { + err = e; + } + expect(err).toBeInstanceOf(Error); + expect(err.message).toBe("Unable to transcode Buffer [U_ILLEGAL_ARGUMENT_ERROR]"); + expect(err.code).toBe("U_ILLEGAL_ARGUMENT_ERROR"); + } }); it("Buffer.from (Node.js test/test-buffer-from.js)", () => { From cce0575f4c5cd78f86776859abc342569ce515b0 Mon Sep 17 00:00:00 2001 From: 0xfandom Date: Thu, 18 Jun 2026 12:21:23 +0530 Subject: [PATCH 2/2] fix(node:buffer): guard detached source and zero-length ucs2 in transcode() Reject a detached Uint8Array source with ERR_INVALID_STATE before reading byteLength or copying its backing storage, matching the detached-view guard used by isUtf8/isAscii. Return an empty Buffer when a ucs2 source has no complete code units (e.g. a single-byte input) instead of reporting U_INVALID_CHAR_FOUND, mirroring the generic ucs2 conversion path. --- src/jsc/modules/NodeBufferModule.h | 6 ++++++ test/js/node/buffer.test.js | 30 +++++++++++++++++++++++++----- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/jsc/modules/NodeBufferModule.h b/src/jsc/modules/NodeBufferModule.h index c8ceda0e3bb4..b53591e15ac1 100644 --- a/src/jsc/modules/NodeBufferModule.h +++ b/src/jsc/modules/NodeBufferModule.h @@ -394,6 +394,9 @@ static JSC::JSUint8Array* transcodeUcs2FromUtf8(JSC::JSGlobalObject* globalObjec static JSC::JSUint8Array* transcodeUtf8FromUcs2(JSC::JSGlobalObject* globalObject, std::span source, UErrorCode* status) { const size_t lengthInChars = source.size() / sizeof(char16_t); + if (!lengthInChars) + return WebCore::createEmptyBuffer(globalObject); + Vector sourceChars(lengthInChars); memcpy(sourceChars.mutableSpan().data(), source.data(), lengthInChars * sizeof(char16_t)); @@ -423,6 +426,9 @@ JSC_DEFINE_HOST_FUNCTION(jsBufferConstructorFunction_transcode, if (!source) [[unlikely]] return Bun::ERR::INVALID_ARG_TYPE_INSTANCE(scope, lexicalGlobalObject, "source"_s, "Buffer or Uint8Array"_s, sourceValue); + if (source->isDetached()) [[unlikely]] + return Bun::ERR::INVALID_STATE(scope, lexicalGlobalObject, "Cannot transcode on a detached buffer"_s); + const size_t byteLength = source->byteLength(); if (!byteLength) return JSValue::encode(WebCore::createEmptyBuffer(lexicalGlobalObject)); diff --git a/test/js/node/buffer.test.js b/test/js/node/buffer.test.js index 412b5318d76d..ee34e1ae32b1 100644 --- a/test/js/node/buffer.test.js +++ b/test/js/node/buffer.test.js @@ -2551,20 +2551,39 @@ for (let withOverridenBufferWrite of [false, true]) { expect(BufferModule.transcode(Buffer.from(ucs2), "ucs2", "utf8").toString()).toBe(orig.toString()); // ascii/latin1 -> utf16le. - expect(BufferModule.transcode(Buffer.from("hi", "ascii"), "ascii", "utf16le")).toEqual(Buffer.from("hi", "utf16le")); - expect(BufferModule.transcode(Buffer.from("hä", "latin1"), "latin1", "utf16le")).toEqual(Buffer.from("hä", "utf16le")); + expect(BufferModule.transcode(Buffer.from("hi", "ascii"), "ascii", "utf16le")).toEqual( + Buffer.from("hi", "utf16le"), + ); + expect(BufferModule.transcode(Buffer.from("hä", "latin1"), "latin1", "utf16le")).toEqual( + Buffer.from("hä", "utf16le"), + ); // A plain Uint8Array is accepted as the source. const u8 = new Uint8Array([...Buffer.from("hä", "latin1")]); expect(BufferModule.transcode(u8, "latin1", "utf16le")).toEqual(Buffer.from("hä", "utf16le")); - // Empty input short-circuits to an empty Buffer without touching the encodings. - const empty = BufferModule.transcode(Buffer.alloc(0), "utf8", "ucs2"); + // Empty input short-circuits to an empty Buffer before the encodings are + // ever parsed, so even invalid encoding names are accepted here. + const empty = BufferModule.transcode(Buffer.alloc(0), "not-an-encoding", "also-not-an-encoding"); expect(empty.length).toBe(0); expect(Buffer.isBuffer(empty)).toBe(true); + // An odd-length ucs2 source has no complete code units, so ucs2->utf8 + // yields an empty Buffer rather than throwing. + const oddUcs2 = BufferModule.transcode(Buffer.from([0x61]), "ucs2", "utf8"); + expect(oddUcs2.length).toBe(0); + expect(Buffer.isBuffer(oddUcs2)).toBe(true); + + // A detached source throws instead of reading freed backing storage. + const detached = new Uint8Array(8); + structuredClone(detached.buffer, { transfer: [detached.buffer] }); + const onDetached = () => BufferModule.transcode(detached, "utf8", "ascii"); + expect(onDetached).toThrowWithCode(Error, "ERR_INVALID_STATE"); + // Non-Uint8Array source throws ERR_INVALID_ARG_TYPE. - expect(() => BufferModule.transcode(null, "utf8", "ascii")).toThrow( + const onInvalidSource = () => BufferModule.transcode(null, "utf8", "ascii"); + expect(onInvalidSource).toThrowWithCode(TypeError, "ERR_INVALID_ARG_TYPE"); + expect(onInvalidSource).toThrow( 'The "source" argument must be an instance of Buffer or Uint8Array. Received null', ); @@ -2583,6 +2602,7 @@ for (let withOverridenBufferWrite of [false, true]) { expect(err).toBeInstanceOf(Error); expect(err.message).toBe("Unable to transcode Buffer [U_ILLEGAL_ARGUMENT_ERROR]"); expect(err.code).toBe("U_ILLEGAL_ARGUMENT_ERROR"); + expect(err.errno).toBe(1); } });