Skip to content
Merged
8 changes: 8 additions & 0 deletions Polyfills/Blob/Include/Babylon/Polyfills/Blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,15 @@
#include <napi/env.h>
#include <Babylon/Api.h>

#include <cstddef>
#include <string>

namespace Babylon::Polyfills::Blob
{
void BABYLON_API Initialize(Napi::Env env);

// Synchronously reads the bytes and MIME type of a Blob JS object created by this
// polyfill. Returns false if `object` is not a Blob. The returned pointer remains
// valid only while `object` is alive, so it must be consumed synchronously.
bool BABYLON_API TryGetData(const Napi::Object& object, const std::byte*& outData, size_t& outSize, std::string& outType);
}
71 changes: 71 additions & 0 deletions Polyfills/Blob/Source/Blob.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -139,4 +139,75 @@ namespace Babylon::Polyfills::Blob
{
Internal::Blob::Initialize(env);
}

bool BABYLON_API TryGetData(const Napi::Object& object, const std::byte*& outData, size_t& outSize, std::string& outType)
{
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.
//
// Two engine-adapter quirks drive this implementation:
// * The Blob constructor is created via node-addon-api's DefineClass. On the JavaScriptCore
// adapter such constructors are callable-as-constructor but are NOT reported as functions
// by napi_typeof (JSObjectIsFunction returns false), so we must probe the constructor with
Comment thread
bghgary marked this conversation as resolved.
Outdated
// IsObject() rather than IsFunction() and read `prototype` as a plain object property.
// * 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.
// Together 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 and is likewise gated on the
// constructor being typed as a function.
const auto blobConstructor = global.Get("Blob");
if (!blobConstructor.IsObject())
{
return false;
}

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

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

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

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 false;
}

const auto* blob = Internal::Blob::Unwrap(object);
const auto& data = blob->Data();
outData = data.data();
outSize = data.size();
outType = blob->Type();
return true;
}
}
4 changes: 4 additions & 0 deletions Polyfills/Blob/Source/Blob.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ namespace Babylon::Polyfills::Internal

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

// Synchronous accessors for internal cross-polyfill use (e.g. URL.createObjectURL).
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 Down
3 changes: 2 additions & 1 deletion Polyfills/Fetch/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ target_include_directories(Fetch PUBLIC "Include")
target_link_libraries(Fetch
PUBLIC JsRuntime
PRIVATE arcana
PRIVATE UrlLib)
PRIVATE UrlLib
PRIVATE URL)

set_property(TARGET Fetch PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
29 changes: 29 additions & 0 deletions Polyfills/Fetch/Source/Fetch.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include <Babylon/JsRuntime.h>
#include <Babylon/JsRuntimeScheduler.h>
#include <Babylon/Polyfills/Fetch.h>
#include <Babylon/Polyfills/URL.h>

#include <UrlLib/UrlLib.h>

Expand Down Expand Up @@ -404,6 +405,34 @@ namespace Babylon::Polyfills::Internal
signal = init.Get("signal");
}

// blob: URLs (URL.createObjectURL) resolve against the in-memory object-URL store
// instead of the UrlLib transport, which only understands app/file/http(s). Serve
// the buffered bytes synchronously as a 200 response, or reject with a network
// error (TypeError) when the URL was never registered or has been revoked.
if (url.rfind("blob:", 0) == 0)
Comment thread
bghgary marked this conversation as resolved.
Outdated
{
std::vector<std::byte> blobData;
std::string blobType;
if (!Babylon::Polyfills::URL::TryResolveObjectURL(env, url, blobData, blobType))
{
deferred.Reject(Napi::TypeError::New(env, "Failed to fetch: blob URL is not registered").Value());
return deferred.Promise();
}

auto data = std::make_shared<ResponseData>();
data->statusCode = 200;
data->statusText = "OK";
data->url = url;
if (!blobType.empty())
{
data->headers.emplace_back("content-type", blobType);
}
data->body = std::move(blobData);

deferred.Resolve(BuildResponse(env, data));
return deferred.Promise();
}

auto request = std::make_shared<UrlLib::UrlRequest>();
request->Open(method, url);
request->ResponseType(UrlLib::UrlResponseType::Buffer);
Expand Down
1 change: 1 addition & 0 deletions Polyfills/URL/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ add_library(URL ${SOURCES})
target_include_directories(URL PUBLIC "Include")

target_link_libraries(URL
PRIVATE Blob
PUBLIC JsRuntime)

set_property(TARGET URL PROPERTY FOLDER Polyfills)
Expand Down
22 changes: 22 additions & 0 deletions Polyfills/URL/Include/Babylon/Polyfills/URL.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,29 @@
#include <napi/env.h>
#include <Babylon/Api.h>

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

namespace Babylon::Polyfills::URL
{
void BABYLON_API Initialize(Napi::Env env);

// Blob URL store backing URL.createObjectURL / URL.revokeObjectURL. Native has no browser
// blob: URL store, so the URL polyfill keeps an in-memory registry keyed by a minted
// blob: URL. The store is process-global (shared across all JS environments in the process);
// the minted URLs embed a UUID so entries never collide between environments. The
// XMLHttpRequest and fetch polyfills call TryResolveObjectURL to serve those URLs from memory
// instead of handing them to the (scheme-unaware) transport.

// Copies `size` bytes into the process-global store and returns a freshly minted blob: URL.
// The `env` parameter is currently unused but kept for API symmetry and future per-env scoping.
std::string BABYLON_API RegisterObjectURL(Napi::Env env, const std::byte* data, size_t size, std::string type);

// Releases the entry for `url`, if any. Unknown URLs are ignored (matching the web platform).
void BABYLON_API RevokeObjectURL(Napi::Env env, const std::string& url);

// Copies the bytes and MIME type registered for `url` into the out-parameters. Returns false
// if `url` is not a live blob: URL in the process-global store.
bool BABYLON_API TryResolveObjectURL(Napi::Env env, const std::string& url, std::vector<std::byte>& outData, std::string& outType);
}
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
154 changes: 154 additions & 0 deletions Polyfills/URL/Source/URL.cpp
Original file line number Diff line number Diff line change
@@ -1,11 +1,84 @@
#include "URL.h"
#include <Babylon/Polyfills/URL.h>
#include <Babylon/Polyfills/Blob.h>
#include <sstream>
#include <regex>
#include <optional>
#include <cstdint>
#include <cstddef>
#include <cstdio>
#include <memory>
#include <mutex>
#include <random>
#include <unordered_map>
#include <vector>

// NOTE: This is a platform agnostic implementation created with a lot of help from AI :)
// In the future, we may want to consider using platform-specific URL parsing APIs instead.

namespace
{
// ---- Blob URL registry -------------------------------------------------------------------
// URL.createObjectURL/revokeObjectURL are backed by an in-memory store. The XMLHttpRequest and
// fetch polyfills resolve minted blob: URLs against this store (see the public
// Register/Revoke/TryResolveObjectURL functions below), since the underlying transport has no
// notion of the blob: scheme.
//
// The store is process-global rather than per-environment: the Node-API engine adapters this
Comment thread
bghgary marked this conversation as resolved.
Outdated
// library targets do not all expose napi_add_env_cleanup_hook (e.g. QuickJS), so there is no
// portable hook on which to free per-environment state. Keys are unguessable v4 UUIDs, so
// sharing one store across environments is safe (a blob: URL minted in one environment is never
// produced in another). Entries are released by revokeObjectURL; any not revoked before the
// process exits are reclaimed at exit, mirroring how browsers retain blob URLs until unload.
struct BlobUrlEntry
{
std::vector<std::byte> data;
std::string type;
};

struct BlobUrlStore
{
std::mutex mutex;
std::unordered_map<std::string, BlobUrlEntry> entries;
};

BlobUrlStore& GetBlobUrlStore()
{
static BlobUrlStore store;
return store;
}

// Mints a URL of the form blob:<origin>/<uuid>. Native has no origin, so the opaque "null"
// origin (as used by the web platform for e.g. data:-document contexts) is used. The uuid is a
// random RFC 4122 version 4 identifier -- unique enough to key the store, not security bearing.
std::string GenerateObjectURL()
{
static std::mutex generatorMutex;
static std::mt19937_64 generator{std::random_device{}()};

uint64_t hi{};
uint64_t lo{};
{
const std::lock_guard<std::mutex> lock{generatorMutex};
hi = generator();
lo = generator();
}

hi = (hi & 0xFFFFFFFFFFFF0FFFull) | 0x0000000000004000ull; // version 4
lo = (lo & 0x3FFFFFFFFFFFFFFFull) | 0x8000000000000000ull; // variant 1

char buffer[64];
std::snprintf(buffer, sizeof(buffer),
"blob:null/%08x-%04x-%04x-%04x-%012llx",
static_cast<uint32_t>(hi >> 32),
static_cast<uint32_t>((hi >> 16) & 0xFFFFull),
static_cast<uint32_t>(hi & 0xFFFFull),
static_cast<uint32_t>(lo >> 48),
static_cast<unsigned long long>(lo & 0xFFFFFFFFFFFFull));
return std::string{buffer};
}
}

namespace
{
// Parsed URL components
Expand Down Expand Up @@ -346,6 +419,8 @@ namespace Babylon::Polyfills::Internal
// Static methods
StaticMethod("canParse", &URL::CanParse),
StaticMethod("parse", &URL::Parse),
StaticMethod("createObjectURL", &URL::CreateObjectURL),
StaticMethod("revokeObjectURL", &URL::RevokeObjectURL),
});

env.Global().Set(JS_URL_CONSTRUCTOR_NAME, func);
Expand Down Expand Up @@ -712,6 +787,48 @@ namespace Babylon::Polyfills::Internal
return info.Env().Null();
}
}

// URL.createObjectURL(blob) copies the Blob's bytes into the in-memory blob URL store and
// returns a minted blob: URL. The XMLHttpRequest and fetch polyfills resolve that URL against
// the store. Only Blob objects are supported (not MediaSource/MediaStream). revokeObjectURL
// releases the entry.
Napi::Value URL::CreateObjectURL(const Napi::CallbackInfo& info)
{
auto env = info.Env();

if (!info.Length() || !info[0].IsObject())
{
throw Napi::TypeError::New(env, "URL.createObjectURL: expected a Blob argument");
}

const std::byte* data{};
size_t size{};
std::string type;
if (!Polyfills::Blob::TryGetData(info[0].As<Napi::Object>(), data, size, type))
{
throw Napi::TypeError::New(env, "URL.createObjectURL: argument is not a Blob");
}

if (type.empty())
{
type = "application/octet-stream";
}

return Napi::String::New(env, Babylon::Polyfills::URL::RegisterObjectURL(env, data, size, std::move(type)));
}

// Releases the store entry for the given blob: URL. Unknown or non-string arguments are ignored.
Napi::Value URL::RevokeObjectURL(const Napi::CallbackInfo& info)
{
auto env = info.Env();

if (info.Length() && info[0].IsString())
{
Babylon::Polyfills::URL::RevokeObjectURL(env, info[0].As<Napi::String>().Utf8Value());
}

return env.Undefined();
}
}

namespace Babylon::Polyfills::URL
Expand All @@ -721,4 +838,41 @@ namespace Babylon::Polyfills::URL
Internal::URL::Initialize(env);
Internal::URLSearchParams::Initialize(env);
}

std::string BABYLON_API RegisterObjectURL(Napi::Env, const std::byte* data, size_t size, std::string type)
{
BlobUrlEntry entry;
entry.data.assign(data, data + size);
entry.type = std::move(type);

std::string url = GenerateObjectURL();

auto& store = GetBlobUrlStore();
const std::lock_guard<std::mutex> lock{store.mutex};
store.entries.emplace(url, std::move(entry));
return url;
}

void BABYLON_API RevokeObjectURL(Napi::Env, const std::string& url)
{
auto& store = GetBlobUrlStore();
const std::lock_guard<std::mutex> lock{store.mutex};
store.entries.erase(url);
}

bool BABYLON_API TryResolveObjectURL(Napi::Env, const std::string& url, std::vector<std::byte>& outData, std::string& outType)
{
auto& store = GetBlobUrlStore();
const std::lock_guard<std::mutex> lock{store.mutex};

const auto it = store.entries.find(url);
if (it == store.entries.end())
{
return false;
}

outData = it->second.data;
outType = it->second.type;
return true;
}
}
Loading
Loading