From 944fd6e7d8b311fb3858a827e193e182dc9ada43 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:16:54 +0100 Subject: [PATCH 1/8] Add TextDecoder polyfill with tests - Add Polyfills/TextDecoder with CMakeLists.txt, public header, and implementation - Register JSRUNTIMEHOST_POLYFILL_TEXTDECODER option in root CMakeLists.txt - Add TextDecoder subdirectory to Polyfills/CMakeLists.txt - Initialize TextDecoder in Tests/UnitTests/Shared/Shared.cpp - Link TextDecoder in Tests/UnitTests/CMakeLists.txt - Add TextDecoder decode tests in Tests/UnitTests/Scripts/tests.ts Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CMakeLists.txt | 1 + Polyfills/CMakeLists.txt | 4 + Polyfills/TextDecoder/CMakeLists.txt | 14 ++++ .../Include/Babylon/Polyfills/TextDecoder.h | 9 +++ Polyfills/TextDecoder/Source/TextDecoder.cpp | 79 +++++++++++++++++++ Tests/UnitTests/CMakeLists.txt | 1 + Tests/UnitTests/Scripts/tests.ts | 22 ++++++ Tests/UnitTests/Shared/Shared.cpp | 2 + 8 files changed, 132 insertions(+) create mode 100644 Polyfills/TextDecoder/CMakeLists.txt create mode 100644 Polyfills/TextDecoder/Include/Babylon/Polyfills/TextDecoder.h create mode 100644 Polyfills/TextDecoder/Source/TextDecoder.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 6318344e..8d11b52b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -81,6 +81,7 @@ option(JSRUNTIMEHOST_POLYFILL_ABORT_CONTROLLER "Include JsRuntimeHost Polyfills option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocket." ON) option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON) option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON) +option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON) # Sanitizers option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF) diff --git a/Polyfills/CMakeLists.txt b/Polyfills/CMakeLists.txt index a9b3f865..87e6d70a 100644 --- a/Polyfills/CMakeLists.txt +++ b/Polyfills/CMakeLists.txt @@ -28,4 +28,8 @@ endif() if(JSRUNTIMEHOST_POLYFILL_PERFORMANCE) add_subdirectory(Performance) +endif() + +if(JSRUNTIMEHOST_POLYFILL_TEXTDECODER) + add_subdirectory(TextDecoder) endif() \ No newline at end of file diff --git a/Polyfills/TextDecoder/CMakeLists.txt b/Polyfills/TextDecoder/CMakeLists.txt new file mode 100644 index 00000000..e0165d96 --- /dev/null +++ b/Polyfills/TextDecoder/CMakeLists.txt @@ -0,0 +1,14 @@ +set(SOURCES + "Include/Babylon/Polyfills/TextDecoder.h" + "Source/TextDecoder.cpp") + +add_library(TextDecoder ${SOURCES}) +warnings_as_errors(TextDecoder) + +target_include_directories(TextDecoder PUBLIC "Include") + +target_link_libraries(TextDecoder + PUBLIC napi) + +set_property(TARGET TextDecoder PROPERTY FOLDER Polyfills) +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) diff --git a/Polyfills/TextDecoder/Include/Babylon/Polyfills/TextDecoder.h b/Polyfills/TextDecoder/Include/Babylon/Polyfills/TextDecoder.h new file mode 100644 index 00000000..d2a310d9 --- /dev/null +++ b/Polyfills/TextDecoder/Include/Babylon/Polyfills/TextDecoder.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include + +namespace Babylon::Polyfills::TextDecoder +{ + void BABYLON_API Initialize(Napi::Env env); +} diff --git a/Polyfills/TextDecoder/Source/TextDecoder.cpp b/Polyfills/TextDecoder/Source/TextDecoder.cpp new file mode 100644 index 00000000..3be10da2 --- /dev/null +++ b/Polyfills/TextDecoder/Source/TextDecoder.cpp @@ -0,0 +1,79 @@ +#include + +#include +#include +#include +#include + +namespace +{ + static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder"; + + class TextDecoder final : public Napi::ObjectWrap + { + public: + static void Initialize(Napi::Env env) + { + Napi::HandleScope scope{env}; + + Napi::Function func = DefineClass( + env, + JS_TEXTDECODER_CONSTRUCTOR_NAME, + { + InstanceMethod("decode", &TextDecoder::Decode), + }); + + env.Global().Set(JS_TEXTDECODER_CONSTRUCTOR_NAME, func); + } + + explicit TextDecoder(const Napi::CallbackInfo& info) + : Napi::ObjectWrap{info} + , m_encoding{"utf-8"} + { + if (info.Length() > 0 && info[0].IsString()) + { + m_encoding = info[0].As().Utf8Value(); + } + } + + private: + Napi::Value Decode(const Napi::CallbackInfo& info) + { + if (info.Length() < 1) + { + return Napi::String::New(info.Env(), ""); + } + + std::vector data; + + if (info[0].IsTypedArray()) + { + auto typedArray = info[0].As(); + auto arrayBuffer = typedArray.ArrayBuffer(); + auto byteOffset = typedArray.ByteOffset(); + auto byteLength = typedArray.ByteLength(); + data.resize(byteLength); + std::memcpy(data.data(), static_cast(arrayBuffer.Data()) + byteOffset, byteLength); + } + else if (info[0].IsArrayBuffer()) + { + auto arrayBuffer = info[0].As(); + data.resize(arrayBuffer.ByteLength()); + std::memcpy(data.data(), arrayBuffer.Data(), arrayBuffer.ByteLength()); + } + + std::string result(data.begin(), data.end()); + return Napi::String::New(info.Env(), result); + } + + std::string m_encoding; + }; +} + +namespace Babylon::Polyfills::TextDecoder +{ + void BABYLON_API Initialize(Napi::Env env) + { + ::TextDecoder::Initialize(env); + } +} diff --git a/Tests/UnitTests/CMakeLists.txt b/Tests/UnitTests/CMakeLists.txt index 0f68bc1b..e0ab9452 100644 --- a/Tests/UnitTests/CMakeLists.txt +++ b/Tests/UnitTests/CMakeLists.txt @@ -54,6 +54,7 @@ target_link_libraries(UnitTests PRIVATE Foundation PRIVATE Blob PRIVATE Performance + PRIVATE TextDecoder ${ADDITIONAL_LIBRARIES}) # See https://gitlab.kitware.com/cmake/cmake/-/issues/23543 diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 21b523fc..70ad2e4d 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -1240,6 +1240,28 @@ describe("Performance", function () { }); }); +describe("TextDecoder", function () { + it("should decode a Uint8Array to a string", function () { + const decoder = new TextDecoder(); + const encoded = new Uint8Array([72, 101, 108, 108, 111]); // "Hello" + const result = decoder.decode(encoded); + expect(result).to.equal("Hello"); + }); + + it("should decode an empty Uint8Array to an empty string", function () { + const decoder = new TextDecoder(); + const result = decoder.decode(new Uint8Array([])); + expect(result).to.equal(""); + }); + + it("should decode an ArrayBuffer to a string", function () { + const decoder = new TextDecoder(); + const buffer = new Uint8Array([87, 111, 114, 108, 100]).buffer; // "World" + const result = decoder.decode(buffer); + expect(result).to.equal("World"); + }); +}); + function runTests() { mocha.run((failures: number) => { // Test program will wait for code to be set before exiting diff --git a/Tests/UnitTests/Shared/Shared.cpp b/Tests/UnitTests/Shared/Shared.cpp index 5ed3706d..ecf63f33 100644 --- a/Tests/UnitTests/Shared/Shared.cpp +++ b/Tests/UnitTests/Shared/Shared.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -68,6 +69,7 @@ TEST(JavaScript, All) Babylon::Polyfills::WebSocket::Initialize(env); Babylon::Polyfills::XMLHttpRequest::Initialize(env); Babylon::Polyfills::Blob::Initialize(env); + Babylon::Polyfills::TextDecoder::Initialize(env); auto setExitCodeCallback = Napi::Function::New( env, [&exitCodePromise](const Napi::CallbackInfo& info) { From 39595fd9adf686ce440c1dda7024dbfdd5f1a737 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:23:51 +0100 Subject: [PATCH 2/8] missing dependency --- Polyfills/TextDecoder/CMakeLists.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Polyfills/TextDecoder/CMakeLists.txt b/Polyfills/TextDecoder/CMakeLists.txt index e0165d96..93f56ea1 100644 --- a/Polyfills/TextDecoder/CMakeLists.txt +++ b/Polyfills/TextDecoder/CMakeLists.txt @@ -8,7 +8,8 @@ warnings_as_errors(TextDecoder) target_include_directories(TextDecoder PUBLIC "Include") target_link_libraries(TextDecoder - PUBLIC napi) + PUBLIC napi + PUBLIC Foundation) set_property(TARGET TextDecoder PROPERTY FOLDER Polyfills) source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) From 20d450bb22768a86ac110dbbb60e2b13f36bd26c Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:44:43 +0100 Subject: [PATCH 3/8] android cmake --- Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt index 7f83454e..b513a866 100644 --- a/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt +++ b/Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt @@ -39,4 +39,5 @@ target_link_libraries(UnitTestsJNI PRIVATE WebSocket PRIVATE gtest_main PRIVATE Blob + PRIVATE TextDecoder PRIVATE Performance) From 091838694c4de086d0ca3d503904a79c8d15a424 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:48:39 +0100 Subject: [PATCH 4/8] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- Polyfills/TextDecoder/Source/TextDecoder.cpp | 13 +++++++--- Tests/UnitTests/Scripts/tests.ts | 25 ++++++++++++++++++++ 2 files changed, 35 insertions(+), 3 deletions(-) diff --git a/Polyfills/TextDecoder/Source/TextDecoder.cpp b/Polyfills/TextDecoder/Source/TextDecoder.cpp index 3be10da2..2d0b2f54 100644 --- a/Polyfills/TextDecoder/Source/TextDecoder.cpp +++ b/Polyfills/TextDecoder/Source/TextDecoder.cpp @@ -53,13 +53,20 @@ namespace auto byteOffset = typedArray.ByteOffset(); auto byteLength = typedArray.ByteLength(); data.resize(byteLength); - std::memcpy(data.data(), static_cast(arrayBuffer.Data()) + byteOffset, byteLength); + if (byteLength > 0) + { + std::memcpy(data.data(), static_cast(arrayBuffer.Data()) + byteOffset, byteLength); + } } else if (info[0].IsArrayBuffer()) { auto arrayBuffer = info[0].As(); - data.resize(arrayBuffer.ByteLength()); - std::memcpy(data.data(), arrayBuffer.Data(), arrayBuffer.ByteLength()); + auto byteLength = arrayBuffer.ByteLength(); + data.resize(byteLength); + if (byteLength > 0) + { + std::memcpy(data.data(), arrayBuffer.Data(), byteLength); + } } std::string result(data.begin(), data.end()); diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 70ad2e4d..22626e39 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -1260,6 +1260,31 @@ describe("TextDecoder", function () { const result = decoder.decode(buffer); expect(result).to.equal("World"); }); + + it("should decode a DataView to a string", function () { + const decoder = new TextDecoder(); + const bytes = new Uint8Array([72, 105]); // "Hi" + const view = new DataView(bytes.buffer); + const result = decoder.decode(view); + expect(result).to.equal("Hi"); + }); + + it("should decode a TypedArray subarray with non-zero byteOffset", function () { + const decoder = new TextDecoder(); + const full = new Uint8Array([88, 72, 105]); // "XHi" + const sub = full.subarray(1); // [72, 105] -> "Hi" + const result = decoder.decode(sub); + expect(result).to.equal("Hi"); + }); + + it("should throw when decoding from an invalid input type", function () { + const decoder = new TextDecoder(); + expect(() => decoder.decode("not-a-buffer" as any)).to.throw(); + }); + + it("should throw for unsupported encodings", function () { + expect(() => new TextDecoder("utf-32" as any)).to.throw(); + }); }); function runTests() { From 845ecb15648f24b86b656246d35c195eb95441f2 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:48:54 +0100 Subject: [PATCH 5/8] PR feedback --- Polyfills/TextDecoder/Source/TextDecoder.cpp | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/Polyfills/TextDecoder/Source/TextDecoder.cpp b/Polyfills/TextDecoder/Source/TextDecoder.cpp index 3be10da2..94f1afba 100644 --- a/Polyfills/TextDecoder/Source/TextDecoder.cpp +++ b/Polyfills/TextDecoder/Source/TextDecoder.cpp @@ -7,8 +7,6 @@ namespace { - static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder"; - class TextDecoder final : public Napi::ObjectWrap { public: @@ -16,14 +14,18 @@ namespace { Napi::HandleScope scope{env}; - Napi::Function func = DefineClass( - env, - JS_TEXTDECODER_CONSTRUCTOR_NAME, - { - InstanceMethod("decode", &TextDecoder::Decode), - }); + if (env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).IsUndefined()) + { + static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder"; + Napi::Function func = DefineClass( + env, + JS_TEXTDECODER_CONSTRUCTOR_NAME, + { + InstanceMethod("decode", &TextDecoder::Decode), + }); - env.Global().Set(JS_TEXTDECODER_CONSTRUCTOR_NAME, func); + env.Global().Set(JS_TEXTDECODER_CONSTRUCTOR_NAME, func); + } } explicit TextDecoder(const Napi::CallbackInfo& info) From c91af6d0dd64801ac196684c3f5a6c3a48d1ecb4 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 16:57:52 +0100 Subject: [PATCH 6/8] missing class name --- Polyfills/TextDecoder/Source/TextDecoder.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Polyfills/TextDecoder/Source/TextDecoder.cpp b/Polyfills/TextDecoder/Source/TextDecoder.cpp index 73bcd25a..b8f5e8c2 100644 --- a/Polyfills/TextDecoder/Source/TextDecoder.cpp +++ b/Polyfills/TextDecoder/Source/TextDecoder.cpp @@ -14,9 +14,9 @@ namespace { Napi::HandleScope scope{env}; - if (env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).IsUndefined()) + static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder"; + if (env.Global().Get(JS_TEXTDECODER_CONSTRUCTOR_NAME).IsUndefined()) { - static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder"; Napi::Function func = DefineClass( env, JS_TEXTDECODER_CONSTRUCTOR_NAME, From 9985420e704c298d86d4f39f1ea3182eb5c48a0f Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:22:14 +0100 Subject: [PATCH 7/8] removed tests --- Tests/UnitTests/Scripts/tests.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/Tests/UnitTests/Scripts/tests.ts b/Tests/UnitTests/Scripts/tests.ts index 22626e39..51128709 100644 --- a/Tests/UnitTests/Scripts/tests.ts +++ b/Tests/UnitTests/Scripts/tests.ts @@ -1261,14 +1261,6 @@ describe("TextDecoder", function () { expect(result).to.equal("World"); }); - it("should decode a DataView to a string", function () { - const decoder = new TextDecoder(); - const bytes = new Uint8Array([72, 105]); // "Hi" - const view = new DataView(bytes.buffer); - const result = decoder.decode(view); - expect(result).to.equal("Hi"); - }); - it("should decode a TypedArray subarray with non-zero byteOffset", function () { const decoder = new TextDecoder(); const full = new Uint8Array([88, 72, 105]); // "XHi" @@ -1276,15 +1268,6 @@ describe("TextDecoder", function () { const result = decoder.decode(sub); expect(result).to.equal("Hi"); }); - - it("should throw when decoding from an invalid input type", function () { - const decoder = new TextDecoder(); - expect(() => decoder.decode("not-a-buffer" as any)).to.throw(); - }); - - it("should throw for unsupported encodings", function () { - expect(() => new TextDecoder("utf-32" as any)).to.throw(); - }); }); function runTests() { From a074873e43526a251d0e50672f43b19323b75234 Mon Sep 17 00:00:00 2001 From: Cedric Guillemet <1312968+CedricGuillemet@users.noreply.github.com> Date: Wed, 25 Feb 2026 10:09:43 +0100 Subject: [PATCH 8/8] feedback --- Polyfills/TextDecoder/README.md | 39 ++++++++++++++++++++ Polyfills/TextDecoder/Source/TextDecoder.cpp | 24 +++++++----- 2 files changed, 54 insertions(+), 9 deletions(-) create mode 100644 Polyfills/TextDecoder/README.md diff --git a/Polyfills/TextDecoder/README.md b/Polyfills/TextDecoder/README.md new file mode 100644 index 00000000..d11cb9ac --- /dev/null +++ b/Polyfills/TextDecoder/README.md @@ -0,0 +1,39 @@ +# TextDecoder Polyfill + +A C++ implementation of the [WHATWG Encoding API](https://encoding.spec.whatwg.org/) `TextDecoder` interface for use in Babylon Native JavaScript runtimes via [Napi](https://github.com/nodejs/node-addon-api). + +## Current State + +### Supported + +- Decoding `Uint8Array`, `Int8Array`, and other typed array views from a UTF-8 encoded byte sequence. +- Decoding raw `ArrayBuffer` objects. +- Constructing `TextDecoder` with no argument (defaults to `utf-8`). +- Constructing `TextDecoder` with the explicit encoding label `"utf-8"` or `"UTF-8"`. +- Calling `decode()` with no argument or `undefined` returns an empty string (matches the Web API). + +### Not Supported + +- Encodings other than UTF-8 — passing any other label (e.g. `"utf-16"`, `"iso-8859-1"`) throws a JavaScript `Error`. +- `DataView` is not accepted by `decode()` — due to missing `Napi::DataView` support in the underlying JSI layer. +- Passing a non-BufferSource value (e.g. a string or number) to `decode()` throws a `TypeError`. +- The `fatal` option: decoding errors are not detected and do not throw a `TypeError`. +- The `ignoreBOM` option: the byte order mark is not stripped. +- Streaming decode (passing `{ stream: true }` to `decode()`) — each call is stateless. +- The `encoding` property on the `TextDecoder` instance is not exposed. + +## Usage + +```javascript +const decoder = new TextDecoder(); // utf-8 +const decoder = new TextDecoder("utf-8"); // explicit, also fine + +const bytes = new Uint8Array([72, 101, 108, 108, 111]); +decoder.decode(bytes); // "Hello" +``` + +Passing an unsupported encoding throws: + +```javascript +new TextDecoder("utf-16"); // Error: TextDecoder: unsupported encoding 'utf-16', only 'utf-8' is supported +``` diff --git a/Polyfills/TextDecoder/Source/TextDecoder.cpp b/Polyfills/TextDecoder/Source/TextDecoder.cpp index b8f5e8c2..15ade200 100644 --- a/Polyfills/TextDecoder/Source/TextDecoder.cpp +++ b/Polyfills/TextDecoder/Source/TextDecoder.cpp @@ -3,7 +3,6 @@ #include #include #include -#include namespace { @@ -30,23 +29,27 @@ namespace explicit TextDecoder(const Napi::CallbackInfo& info) : Napi::ObjectWrap{info} - , m_encoding{"utf-8"} { if (info.Length() > 0 && info[0].IsString()) { - m_encoding = info[0].As().Utf8Value(); + auto encoding = info[0].As().Utf8Value(); + if (encoding != "utf-8" && encoding != "UTF-8") + { + Napi::Error::New(info.Env(), "TextDecoder: unsupported encoding '" + encoding + "', only 'utf-8' is supported") + .ThrowAsJavaScriptException(); + } } } private: Napi::Value Decode(const Napi::CallbackInfo& info) { - if (info.Length() < 1) + if (info.Length() < 1 || info[0].IsUndefined()) { return Napi::String::New(info.Env(), ""); } - std::vector data; + std::string data; if (info[0].IsTypedArray()) { @@ -70,12 +73,15 @@ namespace std::memcpy(data.data(), arrayBuffer.Data(), byteLength); } } + else + { + Napi::TypeError::New(info.Env(), "TextDecoder.decode: input must be a BufferSource (ArrayBuffer or TypedArray)") + .ThrowAsJavaScriptException(); + return info.Env().Undefined(); + } - std::string result(data.begin(), data.end()); - return Napi::String::New(info.Env(), result); + return Napi::String::New(info.Env(), data); } - - std::string m_encoding; }; }