Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions Polyfills/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,8 @@ endif()

if(JSRUNTIMEHOST_POLYFILL_PERFORMANCE)
add_subdirectory(Performance)
endif()

if(JSRUNTIMEHOST_POLYFILL_TEXTDECODER)
add_subdirectory(TextDecoder)
endif()
15 changes: 15 additions & 0 deletions Polyfills/TextDecoder/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
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
PUBLIC Foundation)

set_property(TARGET TextDecoder PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
9 changes: 9 additions & 0 deletions Polyfills/TextDecoder/Include/Babylon/Polyfills/TextDecoder.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#pragma once

#include <napi/env.h>
#include <Babylon/Api.h>

namespace Babylon::Polyfills::TextDecoder
{
void BABYLON_API Initialize(Napi::Env env);
}
39 changes: 39 additions & 0 deletions Polyfills/TextDecoder/README.md
Original file line number Diff line number Diff line change
@@ -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
```
94 changes: 94 additions & 0 deletions Polyfills/TextDecoder/Source/TextDecoder.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
#include <Babylon/Polyfills/TextDecoder.h>

#include <napi/napi.h>
#include <cstring>
#include <string>

namespace
{
class TextDecoder final : public Napi::ObjectWrap<TextDecoder>
{
public:
static void Initialize(Napi::Env env)
{
Napi::HandleScope scope{env};

static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder";
if (env.Global().Get(JS_TEXTDECODER_CONSTRUCTOR_NAME).IsUndefined())
{
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<TextDecoder>{info}
{
if (info.Length() > 0 && info[0].IsString())
{
auto encoding = info[0].As<Napi::String>().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 || info[0].IsUndefined())
{
return Napi::String::New(info.Env(), "");
}

std::string data;

if (info[0].IsTypedArray())
{
auto typedArray = info[0].As<Napi::TypedArray>();
auto arrayBuffer = typedArray.ArrayBuffer();
auto byteOffset = typedArray.ByteOffset();
auto byteLength = typedArray.ByteLength();
data.resize(byteLength);
if (byteLength > 0)
{
std::memcpy(data.data(), static_cast<uint8_t*>(arrayBuffer.Data()) + byteOffset, byteLength);
}
}
else if (info[0].IsArrayBuffer())
{
auto arrayBuffer = info[0].As<Napi::ArrayBuffer>();
auto byteLength = arrayBuffer.ByteLength();
data.resize(byteLength);
if (byteLength > 0)
{
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();
}

return Napi::String::New(info.Env(), data);
}
};
}

namespace Babylon::Polyfills::TextDecoder
{
void BABYLON_API Initialize(Napi::Env env)
{
::TextDecoder::Initialize(env);
}
}
1 change: 1 addition & 0 deletions Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,5 @@ target_link_libraries(UnitTestsJNI
PRIVATE WebSocket
PRIVATE gtest_main
PRIVATE Blob
PRIVATE TextDecoder
PRIVATE Performance)
1 change: 1 addition & 0 deletions Tests/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
30 changes: 30 additions & 0 deletions Tests/UnitTests/Scripts/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1240,6 +1240,36 @@ 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");
});

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");
});
});

function runTests() {
mocha.run((failures: number) => {
// Test program will wait for code to be set before exiting
Expand Down
2 changes: 2 additions & 0 deletions Tests/UnitTests/Shared/Shared.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include <Babylon/Polyfills/WebSocket.h>
#include <Babylon/Polyfills/XMLHttpRequest.h>
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Polyfills/TextDecoder.h>
#include <gtest/gtest.h>
#include <future>
#include <iostream>
Expand Down Expand Up @@ -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) {
Expand Down