Skip to content
Open
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
4 changes: 4 additions & 0 deletions Core/Node-API-JSI/Include/napi/napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -2315,12 +2315,14 @@ ObjectWrap<T>::DefineClass(napi_env env,
prototype;

if (p.staticVoidMethod != nullptr) {
descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0});
descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0,
[env, method{p.staticVoidMethod}, data{p.data}](jsi::Runtime& /*rt*/, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
(*method)({env, thisVal, args, count, nullptr, data});
return {};
}));
} else if (p.staticMethod != nullptr) {
descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0});
descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0,
[env, method{p.staticMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
return {rt, (*method)({env, thisVal, args, count, nullptr, data})};
Expand All @@ -2344,13 +2346,15 @@ ObjectWrap<T>::DefineClass(napi_env env,
descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0});
descriptor.setProperty(rt, "value", static_cast<const jsi::Value&>(p.staticValue));
} else if (p.instanceVoidMethod != nullptr) {
descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0});
descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0,
[env, method{p.instanceVoidMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
T* nativeObject{Unwrap(env, thisVal.getObject(rt))};
(nativeObject->*method)({env, thisVal, args, count, nullptr, data});
return {};
}));
} else if (p.instanceMethod != nullptr) {
descriptor.setProperty(rt, "writable", jsi::Value{(p.attributes & napi_writable) != 0});
descriptor.setProperty(rt, "value", jsi::Function::createFromHostFunction(rt, name, 0,
[env, method{p.instanceMethod}, data{p.data}](jsi::Runtime& rt, const jsi::Value& thisVal, const jsi::Value* args, size_t count) -> jsi::Value {
T* nativeObject{Unwrap(env, thisVal.getObject(rt))};
Expand Down
25 changes: 23 additions & 2 deletions Polyfills/Blob/Source/Blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,17 @@
#include <Babylon/Polyfills/Blob.h>
#include <Babylon/Polyfills/BlobInternal.h>

namespace
{
constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob";
// Hidden global holding the polyfill's own constructor (see Babylon::Polyfills::Blob::TryGetData).
constexpr auto JS_BLOB_CONSTRUCTOR_KEY = "__jsRuntimeHostBlob";
}

namespace Babylon::Polyfills::Internal
{
void Blob::Initialize(Napi::Env env)
{
static constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob";
if (env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).IsUndefined())
{
Napi::Function func = DefineClass(
Expand All @@ -21,6 +27,15 @@ namespace Babylon::Polyfills::Internal
InstanceMethod("bytes", &Blob::Bytes),
});

// TryGetData identifies our instances through this hidden, non-writable, non-configurable
// global, so a script that later replaces the global `Blob` (a test shim, another
// polyfill) cannot make a foreign object look like one of ours. A plain global rather
// than the JsRuntime native object: worker environments have no JsRuntime.
auto constructorDescriptor = Napi::Object::New(env);
constructorDescriptor.Set("value", func);
env.Global().Get("Object").As<Napi::Object>().Get("defineProperty").As<Napi::Function>().Call(
env.Global().Get("Object"),
{env.Global(), Napi::String::New(env, JS_BLOB_CONSTRUCTOR_KEY), constructorDescriptor});
env.Global().Set(JS_BLOB_CONSTRUCTOR_NAME, func);
}
}
Expand Down Expand Up @@ -156,7 +171,9 @@ namespace Babylon::Polyfills::Blob
// 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");
// Our constructor is the one Initialize pinned under the hidden key, not the global `Blob`
// binding: a script may have replaced that with its own class whose instances wrap nothing.
const auto blobConstructor = global.Get(JS_BLOB_CONSTRUCTOR_KEY);
if (!blobConstructor.IsFunction())
{
return std::nullopt;
Expand Down Expand Up @@ -200,6 +217,10 @@ namespace Babylon::Polyfills::Blob
}

const auto* blob = Internal::Blob::Unwrap(object);
if (blob == nullptr)
{
return std::nullopt;
}
return BlobData{blob->Data(), blob->Type()};
}
}
16 changes: 12 additions & 4 deletions Polyfills/URL/Source/URL.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,11 @@ namespace

namespace Babylon::Polyfills::Internal
{
namespace
{
constexpr auto WebIdlOperationAttributes = static_cast<napi_property_attributes>(napi_writable | napi_enumerable | napi_configurable);
}

static constexpr auto JS_URL_CONSTRUCTOR_NAME = "URL";

void URL::Initialize(Napi::Env env)
Expand Down Expand Up @@ -485,10 +490,13 @@ namespace Babylon::Polyfills::Internal
InstanceMethod("toString", &URL::ToString),
InstanceMethod("toJSON", &URL::ToJSON),
// Static methods
StaticMethod("canParse", &URL::CanParse),
StaticMethod("parse", &URL::Parse),
StaticMethod("createObjectURL", &URL::CreateObjectURL),
StaticMethod("revokeObjectURL", &URL::RevokeObjectURL),
// WebIDL static operations are writable, enumerable and configurable (a page can
// replace URL.createObjectURL); napi_default would make them read-only and let
// such an assignment fail silently.
StaticMethod("canParse", &URL::CanParse, WebIdlOperationAttributes),
StaticMethod("parse", &URL::Parse, WebIdlOperationAttributes),
StaticMethod("createObjectURL", &URL::CreateObjectURL, WebIdlOperationAttributes),
StaticMethod("revokeObjectURL", &URL::RevokeObjectURL, WebIdlOperationAttributes),
});

env.Global().Set(JS_URL_CONSTRUCTOR_NAME, func);
Expand Down
37 changes: 37 additions & 0 deletions Tests/UnitTests/Source/Scripts/tests.url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,43 @@ describe("URL.createObjectURL", function () {
expect(() => URL.createObjectURL("not a blob" as any)).to.throw();
});

it("identifies real Blobs, and rejects look-alikes, after a script replaces the global Blob", function () {
// A page or test harness may install its own Blob class. Identity must come from the
// polyfill's own constructor: a real Blob still works and a foreign instance is a clean
// TypeError, not a bare "Invalid argument" from unwrapping an object that wraps nothing.
// ChakraCore has no globalThis; the sloppy-mode Function trick reaches the global object everywhere.
const globalObject: any = Function("return this")();
const NativeBlob = Blob;
const real = new NativeBlob(["hello"], { type: "text/plain" });
class LookAlikeBlob {
size = 5;
type = "text/plain";
}
globalObject.Blob = LookAlikeBlob;
try {
const url = URL.createObjectURL(real);
expect(url.indexOf("blob:")).to.equal(0);
URL.revokeObjectURL(url);
// Message only: the JSI adapter still surfaces native throws as plain Errors.
expect(() => URL.createObjectURL(new LookAlikeBlob() as any)).to.throw(/not a Blob/);
} finally {
globalObject.Blob = NativeBlob;
}
});

it("exposes createObjectURL as a writable, configurable static (WebIDL operation)", function () {
const descriptor = Object.getOwnPropertyDescriptor(URL, "createObjectURL")!;
expect(descriptor.writable, "writable").to.equal(true);
expect(descriptor.configurable, "configurable").to.equal(true);
const original = URL.createObjectURL;
try {
(URL as any).createObjectURL = () => "blob:replaced";
expect(URL.createObjectURL(new Blob([]))).to.equal("blob:replaced");
} finally {
(URL as any).createObjectURL = original;
}
});

it("resolves a blob: URL through fetch (text + content-type)", async function () {
const url = URL.createObjectURL(new Blob(["hello blob"], { type: "text/plain" }));
const response = await fetch(url);
Expand Down