Skip to content

Commit a2a6570

Browse files
authored
Add Blob polyfill (#108)
Polyfilling the [Blob](https://developer.mozilla.org/en-US/docs/Web/API/Blob) interface, sans the following methods: - `stream()` - `splice()` - `options.ending` and the following behaviors: - Sanitizing `options.type`. Instead, the given MIME type is expected to already be ASCII-encoded and in lower case. - Processing any blobParts past `blobParts[0]`. - UTF-8 decoding in `text()`.
1 parent c64c8a8 commit a2a6570

File tree

11 files changed

+343
-3
lines changed

11 files changed

+343
-3
lines changed

CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ option(JSRUNTIMEHOST_POLYFILL_XMLHTTPREQUEST "Include JsRuntimeHost Polyfill XML
7070
option(JSRUNTIMEHOST_POLYFILL_URL "Include JsRuntimeHost Polyfill URL and URLSearchParams." ON)
7171
option(JSRUNTIMEHOST_POLYFILL_ABORT_CONTROLLER "Include JsRuntimeHost Polyfills AbortController and AbortSignal." ON)
7272
option(JSRUNTIMEHOST_POLYFILL_WEBSOCKET "Include JsRuntimeHost Polyfill WebSocket." ON)
73+
option(JSRUNTIMEHOST_POLYFILL_BLOB "Include JsRuntimeHost Polyfill Blob." ON)
7374

7475
# --------------------------------------------------
7576

Core/Node-API-JSI/Include/napi/napi-inl.h

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,25 @@ inline bool Value::IsArrayBuffer() const {
241241
}
242242

243243
inline bool Value::IsTypedArray() const {
244-
throw std::runtime_error{"TODO"};
244+
if (!_value.isObject()) {
245+
return false;
246+
}
247+
248+
auto object{_value.getObject(_env->rt)};
249+
try {
250+
const auto name{object.getPropertyAsObject(_env->rt, "constructor").getProperty(_env->rt, "name").getString(_env->rt).utf8(_env->rt)};
251+
return name == "Int8Array" ||
252+
name == "Uint8Array" ||
253+
name == "Uint8ClampedArray" ||
254+
name == "Int16Array" ||
255+
name == "Uint16Array" ||
256+
name == "Int32Array" ||
257+
name == "Uint32Array" ||
258+
name == "Float32Array" ||
259+
name == "Float64Array";
260+
} catch (const jsi::JSIException&) {
261+
return false;
262+
}
245263
}
246264

247265
inline bool Value::IsObject() const {
@@ -257,7 +275,17 @@ inline bool Value::IsPromise() const {
257275
}
258276

259277
inline bool Value::IsDataView() const {
260-
throw std::runtime_error{"TODO"};
278+
if (!_value.isObject()) {
279+
return false;
280+
}
281+
282+
auto object{_value.getObject(_env->rt)};
283+
try {
284+
const auto name{object.getPropertyAsObject(_env->rt, "constructor").getProperty(_env->rt, "name").getString(_env->rt).utf8(_env->rt)};
285+
return name == "DataView";
286+
} catch (const jsi::JSIException&) {
287+
return false;
288+
}
261289
}
262290

263291
inline bool Value::IsExternal() const {

Polyfills/Blob/CMakeLists.txt

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/Blob.h"
3+
"Source/Blob.cpp"
4+
"Source/Blob.h")
5+
6+
add_library(Blob ${SOURCES})
7+
warnings_as_errors(Blob)
8+
9+
target_include_directories(Blob PUBLIC "Include")
10+
11+
target_link_libraries(Blob
12+
PUBLIC JsRuntime)
13+
14+
set_property(TARGET Blob 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::Blob
7+
{
8+
void BABYLON_API Initialize(Napi::Env env);
9+
}

Polyfills/Blob/Source/Blob.cpp

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#include "Blob.h"
2+
#include <Babylon/JsRuntime.h>
3+
#include <Babylon/Polyfills/Blob.h>
4+
5+
namespace Babylon::Polyfills::Internal
6+
{
7+
void Blob::Initialize(Napi::Env env)
8+
{
9+
static constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob";
10+
if (env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).IsUndefined())
11+
{
12+
Napi::Function func = DefineClass(
13+
env,
14+
JS_BLOB_CONSTRUCTOR_NAME,
15+
{
16+
InstanceAccessor("size", &Blob::GetSize, nullptr),
17+
InstanceAccessor("type", &Blob::GetType, nullptr),
18+
InstanceMethod("text", &Blob::Text),
19+
InstanceMethod("arrayBuffer", &Blob::ArrayBuffer),
20+
InstanceMethod("bytes", &Blob::Bytes)
21+
});
22+
23+
env.Global().Set(JS_BLOB_CONSTRUCTOR_NAME, func);
24+
}
25+
}
26+
27+
Blob::Blob(const Napi::CallbackInfo& info)
28+
: Napi::ObjectWrap<Blob>(info)
29+
{
30+
if (info.Length() > 0)
31+
{
32+
const auto blobParts = info[0].As<Napi::Array>();
33+
34+
if (blobParts.Length() > 0)
35+
{
36+
const auto firstPart = blobParts.Get(uint32_t{0});
37+
ProcessBlobPart(firstPart);
38+
39+
if (blobParts.Length() > 1)
40+
{
41+
throw Napi::Error::New(Env(), "Using multiple BlobParts in Blob constructor is not implemented.");
42+
}
43+
}
44+
}
45+
46+
if (info.Length() > 1)
47+
{
48+
const auto options = info[1].As<Napi::Object>();
49+
50+
if (options.Has("type"))
51+
{
52+
m_type = options.Get("type").As<Napi::String>().Utf8Value();
53+
}
54+
}
55+
}
56+
57+
Napi::Value Blob::GetSize(const Napi::CallbackInfo&)
58+
{
59+
return Napi::Value::From(Env(), m_data.size());
60+
}
61+
62+
Napi::Value Blob::GetType(const Napi::CallbackInfo&)
63+
{
64+
return Napi::String::From(Env(), m_type);
65+
}
66+
67+
Napi::Value Blob::Text(const Napi::CallbackInfo&)
68+
{
69+
// NOTE: This will not check for UTF-8 validity
70+
const auto begin = reinterpret_cast<const char*>(m_data.data());
71+
std::string text(begin, m_data.size());
72+
73+
const auto deferred = Napi::Promise::Deferred::New(Env());
74+
deferred.Resolve(Napi::String::New(Env(), text));
75+
return deferred.Promise();
76+
}
77+
78+
Napi::Value Blob::ArrayBuffer(const Napi::CallbackInfo&)
79+
{
80+
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size());
81+
std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size());
82+
83+
const auto deferred = Napi::Promise::Deferred::New(Env());
84+
deferred.Resolve(arrayBuffer);
85+
return deferred.Promise();
86+
}
87+
88+
Napi::Value Blob::Bytes(const Napi::CallbackInfo&)
89+
{
90+
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size());
91+
std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size());
92+
const auto uint8Array = Napi::Uint8Array::New(Env(), m_data.size(), arrayBuffer, 0);
93+
94+
const auto deferred = Napi::Promise::Deferred::New(Env());
95+
deferred.Resolve(uint8Array);
96+
return deferred.Promise();
97+
}
98+
99+
void Blob::ProcessBlobPart(const Napi::Value& blobPart)
100+
{
101+
if (blobPart.IsArrayBuffer())
102+
{
103+
const auto buffer = blobPart.As<Napi::ArrayBuffer>();
104+
const auto begin = static_cast<const std::byte*>(buffer.Data());
105+
m_data.assign(begin, begin + buffer.ByteLength());
106+
}
107+
else if (blobPart.IsTypedArray() || blobPart.IsDataView())
108+
{
109+
const auto array = blobPart.As<Napi::TypedArray>();
110+
const auto buffer = array.ArrayBuffer();
111+
const auto begin = static_cast<const std::byte*>(buffer.Data()) + array.ByteOffset();
112+
m_data.assign(begin, begin + array.ByteLength());
113+
}
114+
else if (blobPart.IsString())
115+
{
116+
const auto str = blobPart.As<Napi::String>().Utf8Value();
117+
const auto begin = reinterpret_cast<const std::byte*>(str.data());
118+
m_data.assign(begin, begin + str.length());
119+
}
120+
else
121+
{
122+
// Assume it's another Blob object
123+
const auto obj = blobPart.As<Napi::Object>();
124+
const auto blobObj = Napi::ObjectWrap<Blob>::Unwrap(obj);
125+
m_data.assign(blobObj->m_data.begin(), blobObj->m_data.end());
126+
}
127+
}
128+
}
129+
130+
namespace Babylon::Polyfills::Blob
131+
{
132+
void BABYLON_API Initialize(Napi::Env env)
133+
{
134+
Internal::Blob::Initialize(env);
135+
}
136+
}

Polyfills/Blob/Source/Blob.h

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
#pragma once
2+
3+
#include <napi/napi.h>
4+
5+
#include <vector>
6+
#include <string>
7+
8+
namespace Babylon::Polyfills::Internal
9+
{
10+
class Blob : public Napi::ObjectWrap<Blob>
11+
{
12+
public:
13+
static void Initialize(Napi::Env env);
14+
15+
explicit Blob(const Napi::CallbackInfo& info);
16+
17+
private:
18+
Napi::Value GetSize(const Napi::CallbackInfo& info);
19+
Napi::Value GetType(const Napi::CallbackInfo& info);
20+
Napi::Value Text(const Napi::CallbackInfo& info);
21+
Napi::Value ArrayBuffer(const Napi::CallbackInfo& info);
22+
Napi::Value Bytes(const Napi::CallbackInfo& info);
23+
24+
void ProcessBlobPart(const Napi::Value& blobPart);
25+
26+
std::vector<std::byte> m_data;
27+
std::string m_type;
28+
};
29+
}

Polyfills/CMakeLists.txt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,3 +21,7 @@ endif()
2121
if(JSRUNTIMEHOST_POLYFILL_WEBSOCKET)
2222
add_subdirectory(WebSocket)
2323
endif()
24+
25+
if(JSRUNTIMEHOST_POLYFILL_BLOB)
26+
add_subdirectory(Blob)
27+
endif()

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,5 @@ target_link_libraries(UnitTestsJNI
3737
PRIVATE UrlLib
3838
PRIVATE XMLHttpRequest
3939
PRIVATE WebSocket
40-
PRIVATE gtest_main)
40+
PRIVATE gtest_main
41+
PRIVATE Blob)

Tests/UnitTests/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ target_link_libraries(UnitTests
5353
PRIVATE WebSocket
5454
PRIVATE gtest_main
5555
PRIVATE Foundation
56+
PRIVATE Blob
5657
${ADDITIONAL_LIBRARIES})
5758

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

Tests/UnitTests/Scripts/tests.js

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -727,6 +727,120 @@ describe("Console", function () {
727727
});
728728
});
729729

730+
describe("Blob", function () {
731+
let emptyBlobs, helloBlobs, stringBlob, typedArrayBlob, arrayBufferBlob, blobBlob;
732+
733+
before(function () {
734+
emptyBlobs = [new Blob(), new Blob([])];
735+
stringBlob = new Blob(["Hello"]);
736+
typedArrayBlob = new Blob([new Uint8Array([72, 101, 108, 108, 111])]),
737+
arrayBufferBlob = new Blob([new Uint8Array([72, 101, 108, 108, 111]).buffer]),
738+
blobBlob = new Blob([new Blob(["Hello"])]),
739+
helloBlobs = [stringBlob, typedArrayBlob, arrayBufferBlob, blobBlob]
740+
});
741+
742+
// -------------------------------- Blob Construction --------------------------------
743+
it("creates empty blobs", function () {
744+
for (const blob of emptyBlobs) {
745+
expect(blob.size).to.equal(0);
746+
expect(blob.type).to.equal("");
747+
}
748+
});
749+
750+
it("creates blob from string array", function () {
751+
expect(stringBlob.size).to.equal(5);
752+
expect(stringBlob.type).to.equal("");
753+
});
754+
755+
it("creates blob from TypedArray", function () {
756+
expect(typedArrayBlob.size).to.equal(5);
757+
expect(typedArrayBlob.type).to.equal("");
758+
});
759+
760+
it("creates blob from ArrayBuffer", function () {
761+
expect(arrayBufferBlob.size).to.equal(5);
762+
expect(arrayBufferBlob.type).to.equal("");
763+
});
764+
765+
it("creates blob from another Blob", function () {
766+
expect(blobBlob.size).to.equal(5);
767+
expect(blobBlob.type).to.equal("");
768+
});
769+
770+
it("applies MIME type from options", function () {
771+
const modelGltfJson = new Blob(["glTF"], { type: "model/gltf+json" })
772+
expect(modelGltfJson.type).to.equal("model/gltf+json");
773+
});
774+
775+
// -------------------------------- Blob.text() --------------------------------
776+
it("returns empty string for empty blobs", async function () {
777+
for (const blob of emptyBlobs) {
778+
const text = await blob.text();
779+
expect(text).to.equal("");
780+
}
781+
});
782+
783+
it("returns correct string content for non-empty blobs", async function () {
784+
for (const blob of helloBlobs) {
785+
const text = await blob.text();
786+
expect(text).to.equal("Hello");
787+
}
788+
});
789+
790+
it("handles multi-byte UTF-8 characters", async function () {
791+
const utf8Blob = new Blob(["你好, 世界"]);
792+
const text = await utf8Blob.text();
793+
expect(text).to.equal("你好, 世界");
794+
});
795+
796+
it("preserves line endings like default transparent mode", async function () {
797+
const lineEndingsBlob = new Blob(["Hello\nWorld"]);
798+
const text = await lineEndingsBlob.text();
799+
expect(text).to.equal("Hello\nWorld");
800+
});
801+
802+
// -------------------------------- Blob.bytes() --------------------------------
803+
it("returns empty Uint8Array for empty blobs", async function () {
804+
for (const blob of emptyBlobs) {
805+
const bytes = await blob.bytes();
806+
expect(bytes).to.be.instanceOf(Uint8Array);
807+
expect(bytes.length).to.equal(0);
808+
}
809+
});
810+
811+
it("returns correct byte content from non-empty blobs", async function () {
812+
for (const blob of helloBlobs) {
813+
const bytes = await blob.bytes();
814+
expect(bytes).to.be.instanceOf(Uint8Array);
815+
expect(bytes.length).to.equal(5);
816+
expect(bytes[0]).to.equal(72); // 'H'
817+
expect(bytes[4]).to.equal(111); // 'o'
818+
}
819+
});
820+
821+
// -------------------------------- Blob.arrayBuffer() --------------------------------
822+
it("returns empty buffer for empty blobs", async function () {
823+
for (const blob of emptyBlobs) {
824+
const buffer = await blob.arrayBuffer();
825+
expect(buffer).to.be.instanceOf(ArrayBuffer);
826+
expect(buffer.byteLength).to.equal(0);
827+
}
828+
});
829+
830+
it("returns correct buffer content for non-empty blobs", async function () {
831+
for (const blob of helloBlobs) {
832+
const buffer = await blob.arrayBuffer();
833+
expect(buffer).to.be.instanceOf(ArrayBuffer);
834+
expect(buffer.byteLength).to.equal(5);
835+
836+
const view = new Uint8Array(buffer);
837+
expect(view[0]).to.equal(72); // 'H'
838+
expect(view[4]).to.equal(111); // 'o'
839+
840+
}
841+
});
842+
});
843+
730844
function runTests() {
731845
mocha.run(failures => {
732846
// Test program will wait for code to be set before exiting

0 commit comments

Comments
 (0)