Skip to content
Merged
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ FetchContent_Declare(llhttp
EXCLUDE_FROM_ALL)
FetchContent_Declare(UrlLib
GIT_REPOSITORY https://github.com/BabylonJS/UrlLib.git
GIT_TAG e86ffb34e77092266145497681efc74e0a920ffe
GIT_TAG 0c991337a1160ba7a2d062bf8e342d0a66f48dc9
EXCLUDE_FROM_ALL)
FetchContent_Declare(quickjs-ng
GIT_REPOSITORY https://github.com/quickjs-ng/quickjs.git
Expand Down
7 changes: 6 additions & 1 deletion Core/Node-API/Source/js_native_api_javascriptcore.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1547,7 +1547,12 @@ napi_status napi_typeof(napi_env env, napi_value value, napi_valuetype* result)
case kJSTypeSymbol: *result = napi_symbol; break;
default:
JSObjectRef object{ToJSObject(env, value)};
if (JSObjectIsFunction(env->context, object)) {
// Consult JSObjectIsConstructor in addition to JSObjectIsFunction: some JSC builds (e.g.
// libjavascriptcoregtk) report constructors created via JSObjectMakeConstructor -- such as
// node-addon-api DefineClass constructors -- as not-a-function, which would otherwise be
// classified as napi_object and make Napi IsFunction() reject otherwise-valid constructors
// (see issue #194).
if (JSObjectIsFunction(env->context, object) || JSObjectIsConstructor(env->context, object)) {
*result = napi_function;
} else {
NativeInfo* info = NativeInfo::Get<NativeInfo>(object);
Expand Down
8 changes: 8 additions & 0 deletions Polyfills/Blob/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
set(SOURCES
"Include/Babylon/Polyfills/Blob.h"
"InternalInclude/Babylon/Polyfills/BlobInternal.h"
"Source/Blob.cpp"
"Source/Blob.h")

add_library(Blob ${SOURCES})
warnings_as_errors(Blob)

target_include_directories(Blob PUBLIC "Include")
target_include_directories(Blob PRIVATE "InternalInclude")

target_link_libraries(Blob
PUBLIC JsRuntime)

set_property(TARGET Blob PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})

# Seam for other polyfills in this repo that need to read a Blob's contents. Deliberately not part
# of the public "Include" directory: the contract is not one we are ready to commit to for embedders.
add_library(BlobInternal INTERFACE)
target_include_directories(BlobInternal INTERFACE "InternalInclude")
target_link_libraries(BlobInternal INTERFACE Blob)
29 changes: 29 additions & 0 deletions Polyfills/Blob/InternalInclude/Babylon/Polyfills/BlobInternal.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
#pragma once

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

#include <cstddef>
#include <memory>
#include <optional>
#include <string>
#include <vector>

namespace Babylon::Polyfills::Blob
{
// The contents of a Blob JS object created by this polyfill.
struct BlobData
{
// A Blob is immutable once constructed, so this is a shared reference to the Blob's buffer
// rather than a copy. It stays valid for as long as the caller holds it, even if the JS
// object it came from is collected.
std::shared_ptr<const std::vector<std::byte>> Data;

// The Blob's MIME type, which may be empty.
std::string Type;
};

// Synchronously reads the contents of `object`, which may be a Blob or a subclass such as File.
// Returns std::nullopt if `object` is not a Blob created by this polyfill.
std::optional<BlobData> BABYLON_API TryGetData(const Napi::Object& object);
}
93 changes: 78 additions & 15 deletions Polyfills/Blob/Source/Blob.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
#include "Blob.h"
#include <Babylon/JsRuntime.h>
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Polyfills/BlobInternal.h>

namespace Babylon::Polyfills::Internal
{
Expand Down Expand Up @@ -56,7 +57,7 @@ namespace Babylon::Polyfills::Internal

Napi::Value Blob::GetSize(const Napi::CallbackInfo&)
{
return Napi::Value::From(Env(), m_data.size());
return Napi::Value::From(Env(), m_data->size());
}

Napi::Value Blob::GetType(const Napi::CallbackInfo&)
Expand All @@ -67,8 +68,8 @@ namespace Babylon::Polyfills::Internal
Napi::Value Blob::Text(const Napi::CallbackInfo&)
{
// NOTE: This will not check for UTF-8 validity
const auto begin = reinterpret_cast<const char*>(m_data.data());
std::string text(begin, m_data.size());
const auto begin = reinterpret_cast<const char*>(m_data->data());
std::string text(begin, m_data->size());

const auto deferred = Napi::Promise::Deferred::New(Env());
deferred.Resolve(Napi::String::New(Env(), text));
Expand All @@ -77,10 +78,10 @@ namespace Babylon::Polyfills::Internal

Napi::Value Blob::ArrayBuffer(const Napi::CallbackInfo&)
{
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size());
if (m_data.data())
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data->size());
if (m_data->data())
{
std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size());
std::memcpy(arrayBuffer.Data(), m_data->data(), m_data->size());
}

const auto deferred = Napi::Promise::Deferred::New(Env());
Expand All @@ -90,12 +91,12 @@ namespace Babylon::Polyfills::Internal

Napi::Value Blob::Bytes(const Napi::CallbackInfo&)
{
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data.size());
if (m_data.data())
const auto arrayBuffer = Napi::ArrayBuffer::New(Env(), m_data->size());
if (m_data->data())
{
std::memcpy(arrayBuffer.Data(), m_data.data(), m_data.size());
std::memcpy(arrayBuffer.Data(), m_data->data(), m_data->size());
}
const auto uint8Array = Napi::Uint8Array::New(Env(), m_data.size(), arrayBuffer, 0);
const auto uint8Array = Napi::Uint8Array::New(Env(), m_data->size(), arrayBuffer, 0);

const auto deferred = Napi::Promise::Deferred::New(Env());
deferred.Resolve(uint8Array);
Expand All @@ -108,27 +109,28 @@ namespace Babylon::Polyfills::Internal
{
const auto buffer = blobPart.As<Napi::ArrayBuffer>();
const auto begin = static_cast<const std::byte*>(buffer.Data());
m_data.assign(begin, begin + buffer.ByteLength());
m_data = std::make_shared<const std::vector<std::byte>>(begin, begin + buffer.ByteLength());
}
else if (blobPart.IsTypedArray() || blobPart.IsDataView())
{
const auto array = blobPart.As<Napi::TypedArray>();
const auto buffer = array.ArrayBuffer();
const auto begin = static_cast<const std::byte*>(buffer.Data()) + array.ByteOffset();
m_data.assign(begin, begin + array.ByteLength());
m_data = std::make_shared<const std::vector<std::byte>>(begin, begin + array.ByteLength());
}
else if (blobPart.IsString())
{
const auto str = blobPart.As<Napi::String>().Utf8Value();
const auto begin = reinterpret_cast<const std::byte*>(str.data());
m_data.assign(begin, begin + str.length());
m_data = std::make_shared<const std::vector<std::byte>>(begin, begin + str.length());
}
else
{
// Assume it's another Blob object
// Assume it's another Blob object. Blobs are immutable, so the buffer can be shared
// rather than copied.
const auto obj = blobPart.As<Napi::Object>();
const auto blobObj = Napi::ObjectWrap<Blob>::Unwrap(obj);
m_data.assign(blobObj->m_data.begin(), blobObj->m_data.end());
m_data = blobObj->m_data;
}
}
}
Expand All @@ -139,4 +141,65 @@ namespace Babylon::Polyfills::Blob
{
Internal::Blob::Initialize(env);
}

std::optional<BlobData> BABYLON_API TryGetData(const Napi::Object& object)
{
const auto env = object.Env();
const auto global = env.Global();

// Verify `object` is a Blob (or a subclass such as File) by walking its prototype chain and
// comparing each link against Blob.prototype, using the JS-level Object.getPrototypeOf.
//
// We use Object.getPrototypeOf rather than the raw napi_get_prototype C API because the
// latter is not exposed by every adapter (e.g. JSI) and, on JSC, returns the raw
// [[Prototype]] which differs from the JS-visible prototype of a DefineClass instance.
// This keeps the check portable across QuickJS, V8, JavaScriptCore, Chakra and JSI, and it
// also accepts Blob subclasses (e.g. File). We deliberately avoid napi_instanceof, whose
// node-addon-api wrapper requires a Napi::Function.
const auto blobConstructor = global.Get("Blob");
if (!blobConstructor.IsFunction())
{
return std::nullopt;
}

const auto blobPrototype = blobConstructor.As<Napi::Object>().Get("prototype");
if (!blobPrototype.IsObject())
{
return std::nullopt;
}

const auto objectConstructor = global.Get("Object");
if (!objectConstructor.IsObject())
{
return std::nullopt;
}

const auto getPrototypeOf = objectConstructor.As<Napi::Object>().Get("getPrototypeOf");
if (!getPrototypeOf.IsFunction())
{
return std::nullopt;
}

const auto getPrototypeOfFn = getPrototypeOf.As<Napi::Function>();

bool isBlob = false;
Napi::Value current = getPrototypeOfFn.Call({object});
while (current.IsObject())
{
if (current.StrictEquals(blobPrototype))
{
isBlob = true;
break;
}
current = getPrototypeOfFn.Call({current});
}

if (!isBlob)
{
return std::nullopt;
}

const auto* blob = Internal::Blob::Unwrap(object);
return BlobData{blob->Data(), blob->Type()};
}
}
10 changes: 9 additions & 1 deletion Polyfills/Blob/Source/Blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

#include <napi/napi.h>

#include <cstddef>
#include <memory>
#include <vector>
#include <string>

Expand All @@ -14,6 +16,12 @@ namespace Babylon::Polyfills::Internal

explicit Blob(const Napi::CallbackInfo& info);

// Synchronous accessors for internal cross-polyfill use (e.g. URL.createObjectURL).
// A Blob is immutable once constructed, so the bytes are held in a shared_ptr and can be
// shared with consumers rather than copied. Never null.
const std::shared_ptr<const std::vector<std::byte>>& Data() const { return m_data; }
const std::string& Type() const { return m_type; }

private:
Napi::Value GetSize(const Napi::CallbackInfo& info);
Napi::Value GetType(const Napi::CallbackInfo& info);
Expand All @@ -23,7 +31,7 @@ namespace Babylon::Polyfills::Internal

void ProcessBlobPart(const Napi::Value& blobPart);

std::vector<std::byte> m_data;
std::shared_ptr<const std::vector<std::byte>> m_data{std::make_shared<const std::vector<std::byte>>()};
std::string m_type;
};
}
15 changes: 4 additions & 11 deletions Polyfills/Fetch/Source/Fetch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,9 @@ namespace Babylon::Polyfills::Internal
// when an error is thrown) -- in that case the rejection simply carries no synthetic frames.
std::string CaptureCallSiteStack(Napi::Env env)
{
// Detect the global Error constructor with IsUndefined()/IsNull() rather than
// IsFunction(): some JavaScriptCore/JSI builds classify constructor functions as
// typeof 'object', so napi_typeof reports napi_object and IsFunction() would
// incorrectly skip stack capture even though Error is callable (see the Blob check
// below for the same rationale). Error is always present, so this guard is defensive.
// Error is always present and callable; this guard is defensive.
const Napi::Value errorCtor = env.Global().Get("Error");
if (errorCtor.IsUndefined() || errorCtor.IsNull())
if (!errorCtor.IsFunction())
{
return {};
}
Expand Down Expand Up @@ -305,12 +301,9 @@ namespace Babylon::Polyfills::Internal
Napi::Env env = info.Env();
const auto deferred = Napi::Promise::Deferred::New(env);

// Use IsUndefined()/IsNull() rather than IsFunction() to detect the Blob
// polyfill: some JavaScriptCore/JSI builds classify constructor functions as
// typeof 'object', so napi_typeof reports napi_object and IsFunction() would
// incorrectly reject even when the Blob polyfill is installed.
// Require the Blob polyfill to be installed.
const auto blobConstructor = env.Global().Get("Blob");
if (blobConstructor.IsUndefined() || blobConstructor.IsNull())
if (!blobConstructor.IsFunction())
{
deferred.Reject(Napi::Error::New(env, "fetch: Blob is not available in this environment").Value());
return deferred.Promise();
Expand Down
8 changes: 2 additions & 6 deletions Polyfills/File/Source/File.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,9 @@ namespace Babylon::Polyfills::Internal

// Require the native Blob polyfill: File delegates byte storage to
// a Blob, so without it the constructor cannot produce useful
// instances. Use IsUndefined() rather than IsFunction() because
// some JavaScriptCore builds (notably libjavascriptcoregtk on
// Linux) classify constructors created via JSObjectMakeConstructor
// as typeof 'object', not 'function', so napi_typeof returns
// napi_object for them.
// instances.
auto blob = global.Get(JS_BLOB_CONSTRUCTOR_NAME);
if (blob.IsUndefined() || blob.IsNull())
if (!blob.IsFunction())
{
throw Napi::Error::New(env,
"File polyfill requires the Blob polyfill to be installed first.");
Expand Down
2 changes: 2 additions & 0 deletions Polyfills/URL/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ add_library(URL ${SOURCES})
target_include_directories(URL PUBLIC "Include")

target_link_libraries(URL
PRIVATE BlobInternal
PRIVATE UrlLib
PUBLIC JsRuntime)

set_property(TARGET URL PROPERTY FOLDER Polyfills)
Expand Down
6 changes: 2 additions & 4 deletions Polyfills/URL/Readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@ Partial implementation for [`URL`](https://developer.mozilla.org/en-US/docs/Web/
- [`toJSON`](https://developer.mozilla.org/en-US/docs/Web/API/URL/toJSON)
- [`parse`](https://developer.mozilla.org/en-US/docs/Web/API/URL/parse_static)
- [`canParse`](https://developer.mozilla.org/en-US/docs/Web/API/URL/canParse_static)

## Not implemented
- [`createObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static)
- [`revokeObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static)
- [`createObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/createObjectURL_static) (Blob only; mints a `blob:` URL backed by an in-memory, process-global object-URL store)
- [`revokeObjectURL`](https://developer.mozilla.org/en-US/docs/Web/API/URL/revokeObjectURL_static) (releases the stored bytes for a `blob:` URL previously returned by `createObjectURL`; subsequent fetch/XMLHttpRequest against that URL fail as a network error)

# URLSearchParams
Partial implementatioin for [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams)
Expand Down
Loading
Loading