Skip to content

Commit fe50276

Browse files
CedricGuillemetCopilotCopilot
authored
Add TextDecoder polyfill with tests (#138)
Used by GaussianSplatting for parsing ply/compressed ply headers --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 185ef91 commit fe50276

10 files changed

Lines changed: 196 additions & 0 deletions

File tree

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ option(JSRUNTIMEHOST_POLYFILL_ABORT_CONTROLLER "Include JsRuntimeHost Polyfills
8181
option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocket." ON)
8282
option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON)
8383
option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON)
84+
option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON)
8485

8586
# Sanitizers
8687
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)

Polyfills/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,4 +28,8 @@ endif()
2828

2929
if(JSRUNTIMEHOST_POLYFILL_PERFORMANCE)
3030
add_subdirectory(Performance)
31+
endif()
32+
33+
if(JSRUNTIMEHOST_POLYFILL_TEXTDECODER)
34+
add_subdirectory(TextDecoder)
3135
endif()
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
set(SOURCES
2+
"Include/Babylon/Polyfills/TextDecoder.h"
3+
"Source/TextDecoder.cpp")
4+
5+
add_library(TextDecoder ${SOURCES})
6+
warnings_as_errors(TextDecoder)
7+
8+
target_include_directories(TextDecoder PUBLIC "Include")
9+
10+
target_link_libraries(TextDecoder
11+
PUBLIC napi
12+
PUBLIC Foundation)
13+
14+
set_property(TARGET TextDecoder PROPERTY FOLDER Polyfills)
15+
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#pragma once
2+
3+
#include <napi/env.h>
4+
#include <Babylon/Api.h>
5+
6+
namespace Babylon::Polyfills::TextDecoder
7+
{
8+
void BABYLON_API Initialize(Napi::Env env);
9+
}

Polyfills/TextDecoder/README.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# TextDecoder Polyfill
2+
3+
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).
4+
5+
## Current State
6+
7+
### Supported
8+
9+
- Decoding `Uint8Array`, `Int8Array`, and other typed array views from a UTF-8 encoded byte sequence.
10+
- Decoding raw `ArrayBuffer` objects.
11+
- Constructing `TextDecoder` with no argument (defaults to `utf-8`).
12+
- Constructing `TextDecoder` with the explicit encoding label `"utf-8"` or `"UTF-8"`.
13+
- Calling `decode()` with no argument or `undefined` returns an empty string (matches the Web API).
14+
15+
### Not Supported
16+
17+
- Encodings other than UTF-8 — passing any other label (e.g. `"utf-16"`, `"iso-8859-1"`) throws a JavaScript `Error`.
18+
- `DataView` is not accepted by `decode()` — due to missing `Napi::DataView` support in the underlying JSI layer.
19+
- Passing a non-BufferSource value (e.g. a string or number) to `decode()` throws a `TypeError`.
20+
- The `fatal` option: decoding errors are not detected and do not throw a `TypeError`.
21+
- The `ignoreBOM` option: the byte order mark is not stripped.
22+
- Streaming decode (passing `{ stream: true }` to `decode()`) — each call is stateless.
23+
- The `encoding` property on the `TextDecoder` instance is not exposed.
24+
25+
## Usage
26+
27+
```javascript
28+
const decoder = new TextDecoder(); // utf-8
29+
const decoder = new TextDecoder("utf-8"); // explicit, also fine
30+
31+
const bytes = new Uint8Array([72, 101, 108, 108, 111]);
32+
decoder.decode(bytes); // "Hello"
33+
```
34+
35+
Passing an unsupported encoding throws:
36+
37+
```javascript
38+
new TextDecoder("utf-16"); // Error: TextDecoder: unsupported encoding 'utf-16', only 'utf-8' is supported
39+
```
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
#include <Babylon/Polyfills/TextDecoder.h>
2+
3+
#include <napi/napi.h>
4+
#include <cstring>
5+
#include <string>
6+
7+
namespace
8+
{
9+
class TextDecoder final : public Napi::ObjectWrap<TextDecoder>
10+
{
11+
public:
12+
static void Initialize(Napi::Env env)
13+
{
14+
Napi::HandleScope scope{env};
15+
16+
static constexpr auto JS_TEXTDECODER_CONSTRUCTOR_NAME = "TextDecoder";
17+
if (env.Global().Get(JS_TEXTDECODER_CONSTRUCTOR_NAME).IsUndefined())
18+
{
19+
Napi::Function func = DefineClass(
20+
env,
21+
JS_TEXTDECODER_CONSTRUCTOR_NAME,
22+
{
23+
InstanceMethod("decode", &TextDecoder::Decode),
24+
});
25+
26+
env.Global().Set(JS_TEXTDECODER_CONSTRUCTOR_NAME, func);
27+
}
28+
}
29+
30+
explicit TextDecoder(const Napi::CallbackInfo& info)
31+
: Napi::ObjectWrap<TextDecoder>{info}
32+
{
33+
if (info.Length() > 0 && info[0].IsString())
34+
{
35+
auto encoding = info[0].As<Napi::String>().Utf8Value();
36+
if (encoding != "utf-8" && encoding != "UTF-8")
37+
{
38+
Napi::Error::New(info.Env(), "TextDecoder: unsupported encoding '" + encoding + "', only 'utf-8' is supported")
39+
.ThrowAsJavaScriptException();
40+
}
41+
}
42+
}
43+
44+
private:
45+
Napi::Value Decode(const Napi::CallbackInfo& info)
46+
{
47+
if (info.Length() < 1 || info[0].IsUndefined())
48+
{
49+
return Napi::String::New(info.Env(), "");
50+
}
51+
52+
std::string data;
53+
54+
if (info[0].IsTypedArray())
55+
{
56+
auto typedArray = info[0].As<Napi::TypedArray>();
57+
auto arrayBuffer = typedArray.ArrayBuffer();
58+
auto byteOffset = typedArray.ByteOffset();
59+
auto byteLength = typedArray.ByteLength();
60+
data.resize(byteLength);
61+
if (byteLength > 0)
62+
{
63+
std::memcpy(data.data(), static_cast<uint8_t*>(arrayBuffer.Data()) + byteOffset, byteLength);
64+
}
65+
}
66+
else if (info[0].IsArrayBuffer())
67+
{
68+
auto arrayBuffer = info[0].As<Napi::ArrayBuffer>();
69+
auto byteLength = arrayBuffer.ByteLength();
70+
data.resize(byteLength);
71+
if (byteLength > 0)
72+
{
73+
std::memcpy(data.data(), arrayBuffer.Data(), byteLength);
74+
}
75+
}
76+
else
77+
{
78+
Napi::TypeError::New(info.Env(), "TextDecoder.decode: input must be a BufferSource (ArrayBuffer or TypedArray)")
79+
.ThrowAsJavaScriptException();
80+
return info.Env().Undefined();
81+
}
82+
83+
return Napi::String::New(info.Env(), data);
84+
}
85+
};
86+
}
87+
88+
namespace Babylon::Polyfills::TextDecoder
89+
{
90+
void BABYLON_API Initialize(Napi::Env env)
91+
{
92+
::TextDecoder::Initialize(env);
93+
}
94+
}

Tests/UnitTests/Android/app/src/main/cpp/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,5 @@ target_link_libraries(UnitTestsJNI
3939
PRIVATE WebSocket
4040
PRIVATE gtest_main
4141
PRIVATE Blob
42+
PRIVATE TextDecoder
4243
PRIVATE Performance)

Tests/UnitTests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ target_link_libraries(UnitTests
5454
PRIVATE Foundation
5555
PRIVATE Blob
5656
PRIVATE Performance
57+
PRIVATE TextDecoder
5758
${ADDITIONAL_LIBRARIES})
5859

5960
# See https://gitlab.kitware.com/cmake/cmake/-/issues/23543

Tests/UnitTests/Scripts/tests.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,36 @@ describe("Performance", function () {
12401240
});
12411241
});
12421242

1243+
describe("TextDecoder", function () {
1244+
it("should decode a Uint8Array to a string", function () {
1245+
const decoder = new TextDecoder();
1246+
const encoded = new Uint8Array([72, 101, 108, 108, 111]); // "Hello"
1247+
const result = decoder.decode(encoded);
1248+
expect(result).to.equal("Hello");
1249+
});
1250+
1251+
it("should decode an empty Uint8Array to an empty string", function () {
1252+
const decoder = new TextDecoder();
1253+
const result = decoder.decode(new Uint8Array([]));
1254+
expect(result).to.equal("");
1255+
});
1256+
1257+
it("should decode an ArrayBuffer to a string", function () {
1258+
const decoder = new TextDecoder();
1259+
const buffer = new Uint8Array([87, 111, 114, 108, 100]).buffer; // "World"
1260+
const result = decoder.decode(buffer);
1261+
expect(result).to.equal("World");
1262+
});
1263+
1264+
it("should decode a TypedArray subarray with non-zero byteOffset", function () {
1265+
const decoder = new TextDecoder();
1266+
const full = new Uint8Array([88, 72, 105]); // "XHi"
1267+
const sub = full.subarray(1); // [72, 105] -> "Hi"
1268+
const result = decoder.decode(sub);
1269+
expect(result).to.equal("Hi");
1270+
});
1271+
});
1272+
12431273
function runTests() {
12441274
mocha.run((failures: number) => {
12451275
// Test program will wait for code to be set before exiting

Tests/UnitTests/Shared/Shared.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <Babylon/Polyfills/WebSocket.h>
1010
#include <Babylon/Polyfills/XMLHttpRequest.h>
1111
#include <Babylon/Polyfills/Blob.h>
12+
#include <Babylon/Polyfills/TextDecoder.h>
1213
#include <gtest/gtest.h>
1314
#include <future>
1415
#include <iostream>
@@ -68,6 +69,7 @@ TEST(JavaScript, All)
6869
Babylon::Polyfills::WebSocket::Initialize(env);
6970
Babylon::Polyfills::XMLHttpRequest::Initialize(env);
7071
Babylon::Polyfills::Blob::Initialize(env);
72+
Babylon::Polyfills::TextDecoder::Initialize(env);
7173

7274
auto setExitCodeCallback = Napi::Function::New(
7375
env, [&exitCodePromise](const Napi::CallbackInfo& info) {

0 commit comments

Comments
 (0)