diff --git a/eslint.config.mjs b/eslint.config.mjs index 655409db7..9c042cef0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -13,15 +13,20 @@ import globals from 'globals'; // review rule. const capturedStatics = [ ['Array', 'isArray', 'ArrayIsArray'], + ['ArrayBuffer', 'isView', 'ArrayBufferIsView'], ['JSON', 'stringify', 'JSONStringify'], ['Object', 'create', 'ObjectCreate'], ['Object', 'defineProperty', 'ObjectDefineProperty'], + ['Object', 'getOwnPropertyDescriptor', 'ObjectGetOwnPropertyDescriptor'], + ['Object', 'getOwnPropertySymbols', 'ObjectGetOwnPropertySymbols'], + ['Object', 'getPrototypeOf', 'ObjectGetPrototypeOf'], + ['Object', 'is', 'ObjectIs'], ['Object', 'keys', 'ObjectKeys'], ]; // Captured constructors. A destructure from `primordials` shadows the global, // so these only fire on the unguarded reference. -const restrictedGlobals = ['Date', 'Map', 'Proxy', 'String', 'TypeError'].map((name) => ({ +const restrictedGlobals = ['Date', 'Map', 'Proxy', 'Set', 'String', 'TypeError'].map((name) => ({ name, message: `Destructure ${name} from primordials — builtins must not read intrinsics off globals user code can replace.`, })); diff --git a/test-app/app/src/main/assets/app/mainpage.js b/test-app/app/src/main/assets/app/mainpage.js index ad71a72af..7e37e0869 100644 --- a/test-app/app/src/main/assets/app/mainpage.js +++ b/test-app/app/src/main/assets/app/mainpage.js @@ -80,6 +80,7 @@ require('./tests/testEscapeException'); require('./tests/testUncaughtErrorPolicy'); // Runtime builtins keep working when app code replaces the intrinsics they use require('./tests/testPrimordials'); +require('./tests/testInspect'); require("./tests/testConcurrentAccess"); require("./tests/testESModules.mjs"); diff --git a/test-app/app/src/main/assets/app/tests/testInspect.js b/test-app/app/src/main/assets/app/tests/testInspect.js new file mode 100644 index 000000000..4f7dfd7ac --- /dev/null +++ b/test-app/app/src/main/assets/app/tests/testInspect.js @@ -0,0 +1,158 @@ +describe("inspect", function () { + it("formats primitives and plain objects", function () { + expect(__inspect(42)).toBe("42"); + expect(__inspect("hi")).toBe('"hi"'); + expect(__inspect({ a: 1, b: "x" })).toBe('{ a: 1, b: "x" }'); + expect(__inspect([1, [2, 3]])).toBe("[ 1, [ 2, 3 ] ]"); + }); + + it("limits depth", function () { + expect(__inspect({ a: { b: { c: { d: 1 } } } })).toBe("{ a: { b: { c: [Object] } } }"); + expect(__inspect({ a: { b: { c: { d: 1 } } } }, { depth: 3 })).toBe("{ a: { b: { c: { d: 1 } } } }"); + }); + + it("reports true cycles and only true cycles", function () { + var cyc = {}; + cyc.self = cyc; + expect(__inspect(cyc)).toBe("{ self: [Circular] }"); + + var shared = { x: 1 }; + expect(__inspect({ a: shared, b: shared })).toBe("{ a: { x: 1 }, b: { x: 1 } }"); + }); + + it("caps arrays and total output", function () { + var big = []; + for (var i = 0; i < 250; i++) { + big[i] = i; + } + expect(__inspect(big).indexOf("... 150 more items")).toBeGreaterThan(-1); + + var huge = {}; + for (var k = 0; k < 100000; k++) { + huge["key" + k] = k; + } + var start = Date.now(); + var out = __inspect(huge); + var elapsed = Date.now() - start; + expect(out.length).toBeLessThan(20000); + // The old JSON path would serialize all 100k keys; the budgeted + // formatter must not take anywhere near a second. + expect(elapsed).toBeLessThan(1000); + }); + + it("caps long strings", function () { + var long = new Array(12001).join("a"); + var out = __inspect(long); + expect(out.indexOf("... 2000 more characters")).toBeGreaterThan(-1); + }); + + it("never invokes getters", function () { + var invoked = false; + var obj = {}; + Object.defineProperty(obj, "x", { + enumerable: true, + get: function () { + invoked = true; + return 1; + } + }); + expect(__inspect(obj)).toBe("{ x: [Getter] }"); + expect(invoked).toBe(false); + }); + + it("formats collections, dates, regexes, errors and functions", function () { + expect(__inspect(new Map([["k", 1]]))).toBe('Map(1) { "k" => 1 }'); + expect(__inspect(new Set([1, 2]))).toBe("Set(2) { 1, 2 }"); + expect(__inspect(new Date(0))).toBe("1970-01-01T00:00:00.000Z"); + expect(__inspect(/ab+c/gi)).toBe("/ab+c/gi"); + expect(__inspect(function foo() {})).toBe("[Function: foo]"); + expect(__inspect(class Foo {})).toBe("[class Foo]"); + expect(__inspect(new Uint8Array(3))).toBe("Uint8Array(3)"); + expect(__inspect(10n)).toBe("10n"); + + var errOut = __inspect(new Error("boom")); + expect(errOut.indexOf("Error: boom")).toBe(0); + }); + + it("identifies java objects without walking them", function () { + var out = __inspect(new java.lang.Object()); + expect(out.indexOf("[")).toBe(0); + expect(out.indexOf("java.lang.Object")).toBeGreaterThan(-1); + + var listOut = __inspect(new java.util.ArrayList()); + expect(listOut.indexOf("java.util.ArrayList")).toBeGreaterThan(-1); + + // The wrapper hint replaces the whole graph, so a java object nested in + // a plain object stays a single token. + var nested = __inspect({ v: new java.lang.Object() }); + expect(nested.indexOf("{ v: [")).toBe(0); + }); + + it("does not materialize java packages", function () { + // Package children are native data properties, so reading descriptors + // off one would build every class it contains. + var start = Date.now(); + var out = __inspect(java); + expect(out.indexOf("[package java")).toBe(0); + expect(__inspect(java.lang).indexOf("[package java.lang")).toBe(0); + expect(Date.now() - start).toBeLessThan(1000); + }); + + it("renders java classes as callables, not as graphs", function () { + // Class wrappers are constructor functions, so they take the callable + // branch before the native-wrapper hint is consulted. + var out = __inspect(java.lang.Object); + expect(/^\[(Function|class)\b/.test(out)).toBe(true); + }); + + it("leaves plain javascript objects to structural rendering", function () { + expect(__inspect({ a: 1 })).toBe("{ a: 1 }"); + expect(__inspect([1])).toBe("[ 1 ]"); + }); + + it("console.log of a huge cyclic object completes quickly", function () { + var huge = { name: "root" }; + var cursor = huge; + for (var i = 0; i < 5000; i++) { + cursor = cursor.child = { i: i, parent: huge }; + } + huge.self = huge; + var start = Date.now(); + console.log(huge); + expect(Date.now() - start).toBeLessThan(1000); + }); + + it("honors custom toString overrides (NativeScript core convention)", function () { + function ViewLike() { this.id = 42; } + ViewLike.prototype.toString = function () { return "Button(42)"; }; + expect(__inspect(new ViewLike())).toBe("Button(42)"); + expect(__inspect({ v: new ViewLike() })).toBe("{ v: Button(42) }"); + expect(__inspect({ toString: function () { return "custom!"; } })).toBe("custom!"); + // A broken override degrades to structural rendering instead of hiding data. + var broken = { a: 1, toString: function () { throw new Error("x"); } }; + expect(__inspect(broken).indexOf("a: 1")).toBeGreaterThan(-1); + }); + + it("formats under tampered prototypes", function () { + var savedSlice = Array.prototype.slice; + var savedIndexOf = Array.prototype.indexOf; + var savedKeys = Object.keys; + var savedStringify = JSON.stringify; + var boom = function () { throw new Error("tampered"); }; + var out; + try { + Array.prototype.slice = boom; + Array.prototype.indexOf = boom; + Object.keys = boom; + JSON.stringify = boom; + out = __inspect({ a: [1, 2], m: new Map([[1, 2]]) }); + } finally { + Array.prototype.slice = savedSlice; + Array.prototype.indexOf = savedIndexOf; + Object.keys = savedKeys; + JSON.stringify = savedStringify; + } + expect(out).toBe("{ a: [ 1, 2 ], m: Map(1) { 1 => 2 } }"); + }); +}); + diff --git a/test-app/app/src/main/assets/app/tests/testPrimordials.js b/test-app/app/src/main/assets/app/tests/testPrimordials.js index 48b62fffa..f2a91dd62 100644 --- a/test-app/app/src/main/assets/app/tests/testPrimordials.js +++ b/test-app/app/src/main/assets/app/tests/testPrimordials.js @@ -118,11 +118,10 @@ describe("primordials", function () { }); it("console.log of a circular object neither throws nor crashes with JSON.stringify tampered", function () { - // The smart-stringify builtin both calls JSON.stringify and tracks - // already-visited objects with Array.prototype.indexOf/push. Its output - // is not reachable from JS and JsonStringifyObject swallows a throwing - // stringify, so this only pins down that the tampered path stays - // non-fatal; the primordial routing itself is covered by review. + // The inspect builtin quotes strings through JSON.stringify and builds + // its parts list with Array.prototype.push. Its logcat output is not + // reachable from JS, so this only pins down that the tampered path + // stays non-fatal; testInspect covers the formatting itself. const circular = { name: "primordials" }; circular.self = circular; diff --git a/test-app/runtime/CMakeLists.txt b/test-app/runtime/CMakeLists.txt index 31797cb97..e45a338e8 100644 --- a/test-app/runtime/CMakeLists.txt +++ b/test-app/runtime/CMakeLists.txt @@ -67,11 +67,11 @@ set(RUNTIME_BUILTIN_JS ${RUNTIME_BUILTIN_JS_DIR}/blob-url.js ${RUNTIME_BUILTIN_JS_DIR}/error-events.js ${RUNTIME_BUILTIN_JS_DIR}/events.js + ${RUNTIME_BUILTIN_JS_DIR}/inspect.js ${RUNTIME_BUILTIN_JS_DIR}/json-helper.js ${RUNTIME_BUILTIN_JS_DIR}/message-loop-timer.js ${RUNTIME_BUILTIN_JS_DIR}/primordials.js ${RUNTIME_BUILTIN_JS_DIR}/require-factory.js - ${RUNTIME_BUILTIN_JS_DIR}/smart-stringify.js ${RUNTIME_BUILTIN_JS_DIR}/weak-ref.js ) set(RUNTIME_BUILTINS_GENERATED_DIR ${PROJECT_SOURCE_DIR}/src/main/cpp/generated) diff --git a/test-app/runtime/src/main/cpp/BuiltinLoader.cpp b/test-app/runtime/src/main/cpp/BuiltinLoader.cpp index 81e81cebf..c276303bf 100644 --- a/test-app/runtime/src/main/cpp/BuiltinLoader.cpp +++ b/test-app/runtime/src/main/cpp/BuiltinLoader.cpp @@ -125,9 +125,9 @@ MaybeLocal CallBuiltin(Local context, BuiltinId id, Local /* * Snapshot of the intrinsics, taken the first time any builtin runs in this - * isolate — during runtime init, before user code can replace a global. Later - * builtins (smart-stringify compiles lazily, on the first object logged) get - * the same pristine snapshot. + * isolate — during runtime init, before user code can replace a global. + * Builtins compiled later in the isolate's life get the same pristine + * snapshot. */ MaybeLocal GetPrimordials(Local context) { Isolate* isolate = v8::Isolate::GetCurrent(); diff --git a/test-app/runtime/src/main/cpp/IsolateDisposer.cpp b/test-app/runtime/src/main/cpp/IsolateDisposer.cpp index 1896b64f8..8a2aa776b 100644 --- a/test-app/runtime/src/main/cpp/IsolateDisposer.cpp +++ b/test-app/runtime/src/main/cpp/IsolateDisposer.cpp @@ -16,7 +16,6 @@ namespace tns { void disposeIsolate(v8::Isolate *isolate) { tns::ArgConverter::onDisposeIsolate(isolate); tns::MetadataNode::onDisposeIsolate(isolate); - tns::V8GlobalHelpers::onDisposeIsolate(isolate); tns::Console::onDisposeIsolate(isolate); tns::JSONObjectHelper::onDisposeIsolate(isolate); tns::BuiltinLoader::onDisposeIsolate(isolate); diff --git a/test-app/runtime/src/main/cpp/MetadataNode.cpp b/test-app/runtime/src/main/cpp/MetadataNode.cpp index eeeab0e56..4888ea866 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.cpp +++ b/test-app/runtime/src/main/cpp/MetadataNode.cpp @@ -30,6 +30,52 @@ void MetadataNode::Init(Isolate* isolate) { auto key = ArgConverter::ConvertToV8String(isolate, "tns::MetadataKey"); auto cache = GetMetadataNodeCache(isolate); cache->MetadataKey = new Persistent(isolate, key); + cache->PackageKey = new Persistent( + isolate, ArgConverter::ConvertToV8String(isolate, "tns::PackageKey")); +} + +// Deliberately not V8GetPrivateValue: that one resolves the creation context, +// which hard-crashes on values that have none (a revoked proxy), and throws +// when the lookup comes back Nothing. Callers here take arbitrary values. +static void* TryReadPrivateExternal(Isolate* isolate, const Local& value, Persistent* key) { + if (key == nullptr) { + return nullptr; + } + + auto context = isolate->GetCurrentContext(); + if (context.IsEmpty()) { + return nullptr; + } + + Local hidden; + auto privateKey = Private::ForApi(isolate, Local::New(isolate, *key)); + if (!value->GetPrivate(context, privateKey).ToLocal(&hidden) || !hidden->IsExternal()) { + return nullptr; + } + + return hidden.As()->Value(v8::kExternalPointerTypeTagDefault); +} + +bool MetadataNode::TryGetInstanceTypeName(Isolate* isolate, const Local& value, std::string& out) { + auto cache = GetMetadataNodeCache(isolate); + auto node = static_cast(TryReadPrivateExternal(isolate, value, cache->MetadataKey)); + if (node == nullptr) { + return false; + } + + out = node->m_name; + return true; +} + +bool MetadataNode::TryGetPackageName(Isolate* isolate, const Local& value, std::string& out) { + auto cache = GetMetadataNodeCache(isolate); + auto node = static_cast(TryReadPrivateExternal(isolate, value, cache->PackageKey)); + if (node == nullptr) { + return false; + } + + out = node->m_name; + return true; } Local MetadataNode::GetOrCreateArrayObjectTemplate(Isolate* isolate) { @@ -234,10 +280,20 @@ Local MetadataNode::CreateArrayWrapper(Isolate* isolate) { Local MetadataNode::CreatePackageObject(Isolate* isolate) { auto packageObj = Object::New(isolate); + auto ctx = isolate->GetCurrentContext(); + auto extData = External::New(isolate, this, v8::kExternalPointerTypeTagDefault); + + // Every child is a native data property, so anything that reads a property + // descriptor off a package object - console formatting, most notably - + // would materialize the whole subtree. The marker lets such callers + // recognize a package and stop. + auto cache = GetMetadataNodeCache(isolate); + packageObj->SetPrivate(ctx, + Private::ForApi(isolate, Local::New(isolate, *cache->PackageKey)), + extData).ToChecked(); + auto ptrChildren = this->m_treeNode->children; if (ptrChildren != nullptr) { - auto ctx = isolate->GetCurrentContext(); - auto extData = External::New(isolate, this, v8::kExternalPointerTypeTagDefault); const auto& children = *ptrChildren; for (auto childNode: children) { packageObj->SetNativeDataProperty(ctx, diff --git a/test-app/runtime/src/main/cpp/MetadataNode.h b/test-app/runtime/src/main/cpp/MetadataNode.h index caa813bdf..2b36fa92e 100644 --- a/test-app/runtime/src/main/cpp/MetadataNode.h +++ b/test-app/runtime/src/main/cpp/MetadataNode.h @@ -59,6 +59,17 @@ class MetadataNode { static std::string GetTypeMetadataName(v8::Isolate* isolate, v8::Local& value); + /* + * Non-throwing metadata lookups for the console formatter. They read + * private symbols only, so they never re-enter JS or trip an + * interceptor, and they tolerate values carrying no metadata at all + * (unlike GetNodeFromHandle/GetTypeMetadataName, which assume it is + * there). Names come back in the metadata's slashed JNI form. + */ + static bool TryGetInstanceTypeName(v8::Isolate* isolate, const v8::Local& value, std::string& out); + + static bool TryGetPackageName(v8::Isolate* isolate, const v8::Local& value, std::string& out); + static void onDisposeIsolate(v8::Isolate* isolate); static MetadataReader* getMetadataReader(); @@ -267,6 +278,8 @@ class MetadataNode { struct MetadataNodeCache { v8::Persistent* MetadataKey; + v8::Persistent* PackageKey; + robin_hood::unordered_map CtorFuncCache; robin_hood::unordered_map ExtendedCtorFuncCache; diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp index 4be82634c..adc523884 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.cpp @@ -1,79 +1,67 @@ #include "V8GlobalHelpers.h" #include "ArgConverter.h" -#include "BuiltinLoader.h" #include "CallbackHandlers.h" #include "include/v8.h" #include "JEnv.h" +#include "MetadataNode.h" #include "NativeScriptException.h" +#include "ObjectManager.h" +#include "Runtime.h" +#include #include -#include "robin_hood.h" using namespace v8; using namespace std; -static robin_hood::unordered_map*> isolateToPersistentSmartJSONStringify = robin_hood::unordered_map*>(); - -Local GetSmartJSONStringifyFunction(Isolate* isolate) { - auto it = isolateToPersistentSmartJSONStringify.find(isolate); - if (it != isolateToPersistentSmartJSONStringify.end()) { - auto smartStringifyPersistentFunction = it->second; +// Metadata carries slashed JNI names; logs read better dotted. +static std::string ToDottedName(std::string name) { + std::replace(name.begin(), name.end(), '/', '.'); + return name; +} - return smartStringifyPersistentFunction->Get(isolate); +// A JS object linked to a Java counterpart holds a JSInstanceInfo External in +// its first internal field. The field count alone does not identify one: V8 +// hands typed arrays and array buffers the same number of embedder fields. +static bool HasJavaCounterpart(Isolate* isolate, const Local& object) { + auto objectManager = tns::Runtime::GetObjectManager(isolate); + if (objectManager == nullptr || !objectManager->IsJsRuntimeObject(object)) { + return false; } - auto context = isolate->GetCurrentContext(); + const int jsInfoIdx = static_cast(tns::ObjectManager::MetadataNodeKeys::JsInfo); + return object->GetInternalField(jsInfoIdx).As()->IsExternal(); +} - Local result; - if (!tns::BuiltinLoader::RunBuiltin(context, tns::BuiltinId::kSmartStringify).ToLocal(&result) || - !result->IsFunction()) { - return Local(); +std::string tns::GetNativeWrapperHint(Isolate* isolate, const Local& value) { + if (isolate == nullptr || value.IsEmpty() || !value->IsObject() || value->IsFunction()) { + return ""; } - auto smartStringifyFunction = result.As(); - - auto smartStringifyPersistentFunction = new Persistent(isolate, smartStringifyFunction); - - isolateToPersistentSmartJSONStringify.emplace(isolate, smartStringifyPersistentFunction); + auto object = value.As(); + std::string name; - return smartStringifyPersistentFunction->Get(isolate); -} - -std::string tns::JsonStringifyObject(Isolate* isolate, v8::Local value, bool handleCircularReferences) { - if (value.IsEmpty()) { - return ""; + if (MetadataNode::TryGetPackageName(isolate, object, name)) { + return "package " + ToDottedName(name); } - auto context = isolate->GetCurrentContext(); - if (handleCircularReferences) { - Local smartJSONStringifyFunction = GetSmartJSONStringifyFunction(isolate); - - if (!smartJSONStringifyFunction.IsEmpty()) { - if (value->IsObject()) { - v8::Local resultValue; - v8::TryCatch tc(isolate); - - Local args[] = { value }; - auto success = smartJSONStringifyFunction - ->Call(context, Undefined(isolate), 1, args) - .ToLocal(&resultValue); - - if (success && !tc.HasCaught()) { - auto res = resultValue.As(); - return ArgConverter::ConvertToString(res); - } - } - } + if (MetadataNode::TryGetInstanceTypeName(isolate, object, name)) { + return ToDottedName(name); } - v8::Local resultString; - v8::TryCatch tc(isolate); - auto success = v8::JSON::Stringify(context, value).ToLocal(&resultString); + if (!HasJavaCounterpart(isolate, object)) { + return ""; + } - if (!success && tc.HasCaught()) { - throw NativeScriptException(tc); + // TypeScript instances sit one level below the registered instance, which + // is where the metadata lives (see the object layout notes in + // MetadataNode.h). + auto prototype = object->GetPrototype(); + if (prototype->IsObject() && + MetadataNode::TryGetInstanceTypeName(isolate, prototype.As(), name)) { + return ToDottedName(name); } - return ArgConverter::ConvertToString(resultString); + return "JavaObject"; } bool tns::V8GetPrivateValue(Isolate* isolate, const Local& obj, const Local& propName, Local& out) { @@ -116,7 +104,3 @@ bool tns::V8SetPrivateValue(Isolate* isolate, const Local& obj, const Lo return res.FromMaybe(false); } - -void tns::V8GlobalHelpers::onDisposeIsolate(Isolate* isolate) { - isolateToPersistentSmartJSONStringify.erase(isolate); -} diff --git a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h index 0dc71fc8e..4f5e165c8 100644 --- a/test-app/runtime/src/main/cpp/V8GlobalHelpers.h +++ b/test-app/runtime/src/main/cpp/V8GlobalHelpers.h @@ -8,15 +8,13 @@ #include namespace tns { -std::string JsonStringifyObject(v8::Isolate* isolate, v8::Handle value, bool handleCircularReferences = true); +// Short identification for objects backed by a native wrapper (the Java class +// name etc.); empty when the value is a plain JS object. Never runs JS. +std::string GetNativeWrapperHint(v8::Isolate* isolate, const v8::Local& value); bool V8GetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, v8::Local& out); bool V8SetPrivateValue(v8::Isolate* isolate, const v8::Local& obj, const v8::Local& propName, const v8::Local& value); - -namespace V8GlobalHelpers { - void onDisposeIsolate(v8::Isolate* isolate); -} } #endif /* V8GLOBALHELPERS_H_ */ diff --git a/test-app/runtime/src/main/cpp/console/Console.cpp b/test-app/runtime/src/main/cpp/console/Console.cpp index 2e2431da9..f260e36ab 100644 --- a/test-app/runtime/src/main/cpp/console/Console.cpp +++ b/test-app/runtime/src/main/cpp/console/Console.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -15,10 +16,26 @@ #include #include "ArgConverter.h" +#include "BuiltinLoader.h" #include "Console.h" +#include "robin_hood.h" namespace tns { +// internal/inspect.js, one compiled instance per isolate. Worker runtimes +// initialize on their own threads, so every access is under the mutex. +static std::mutex inspectMutex; +static robin_hood::unordered_map*> isolateToInspect; + +static v8::Local getInspectFunction(v8::Isolate* isolate) { + std::lock_guard lock(inspectMutex); + auto it = isolateToInspect.find(isolate); + if (it == isolateToInspect.end()) { + return v8::Local(); + } + return it->second->Get(isolate); +} + v8::Local Console::createConsole(v8::Local context, ConsoleCallback callback, const int maxLogcatObjectSize, const bool forceLog) { m_callback = callback; m_maxLogcatObjectSize = maxLogcatObjectSize; @@ -44,9 +61,89 @@ v8::Local Console::createConsole(v8::Local context, Con bindFunctionProperty(context, console, "time", timeCallback); bindFunctionProperty(context, console, "timeEnd", timeEndCallback); + initInspect(context); + return console; } +static void getNativeWrapperHintCallback(const v8::FunctionCallbackInfo& info) { + auto isolate = info.GetIsolate(); + if (info.Length() < 1) { + return; + } + + std::string hint = GetNativeWrapperHint(isolate, info[0]); + if (!hint.empty()) { + info.GetReturnValue().Set(ArgConverter::ConvertToV8String(isolate, hint)); + } +} + +void Console::initInspect(v8::Local context) { + auto isolate = v8::Isolate::GetCurrent(); + + v8::Local hintFunc; + if (!v8::Function::New(context, getNativeWrapperHintCallback).ToLocal(&hintFunc)) { + __android_log_write(ANDROID_LOG_WARN, LOG_TAG, + "Warning: Console failed to create the native-hint binding"); + return; + } + + auto binding = v8::Object::New(isolate); + if (!binding->Set(context, ArgConverter::ConvertToV8String(isolate, "getNativeWrapperHint"), + hintFunc).FromMaybe(false)) { + __android_log_write(ANDROID_LOG_WARN, LOG_TAG, + "Warning: Console failed to populate the inspect binding"); + return; + } + + v8::TryCatch tc(isolate); + v8::Local result; + if (!BuiltinLoader::RunBuiltin(context, BuiltinId::kInspect, binding).ToLocal(&result) || + !result->IsFunction()) { + __android_log_write(ANDROID_LOG_WARN, LOG_TAG, + "Warning: Console failed to initialize the inspect builtin"); + return; + } + + std::lock_guard lock(inspectMutex); + isolateToInspect.emplace(isolate, new v8::Persistent(isolate, result.As())); +} + +// depth < 0 leaves the builtin's own default (2); console.dir passes 4. +static std::string inspectValue(v8::Isolate* isolate, const v8::Local& val, int depth = -1) { + auto context = isolate->GetCurrentContext(); + + auto inspect = getInspectFunction(isolate); + if (!inspect.IsEmpty()) { + v8::Local args[2]; + int argc = 1; + args[0] = val; + if (depth >= 0) { + auto options = v8::Object::New(isolate); + if (options->Set(context, ArgConverter::ConvertToV8String(isolate, "depth"), + v8::Number::New(isolate, depth)).FromMaybe(false)) { + args[1] = options; + argc = 2; + } + } + + v8::TryCatch tc(isolate); + v8::Local result; + if (inspect->Call(context, v8::Undefined(isolate), argc, args).ToLocal(&result) && + result->IsString()) { + return ArgConverter::ConvertToString(result.As()); + } + } + + // Init failed or the formatter threw: degrade to V8's own short description. + v8::TryCatch tc(isolate); + v8::Local fallback; + if (val->ToDetailString(context).ToLocal(&fallback)) { + return ArgConverter::ConvertToString(fallback); + } + return ""; +} + void Console::sendToADBLogcat(const std::string& message, android_LogPriority logPriority) { // limit the size of the message that we send to logcat using the predefined value in package.json auto messageToLog = message; @@ -94,59 +191,22 @@ void Console::sendToDevToolsFrontEnd(v8::Isolate* isolate, ConsoleAPIType method m_callback(isolate, method, args); } -std::string transformJSObject(v8::Isolate* isolate, v8::Local object) { - auto context = isolate->GetCurrentContext(); - auto objToString = object->ToString(context).ToLocalChecked(); - auto objToCppString = ArgConverter::ConvertToString(objToString); - - auto hasCustomToStringImplementation = objToCppString.find("[object Object]") == std::string::npos; - - if (hasCustomToStringImplementation) { - return objToCppString; - } else { - return JsonStringifyObject(isolate, object); - } -} - std::string buildStringFromArg(v8::Isolate* isolate, const v8::Local& val) { - if (val->IsFunction()) { - auto v8FunctionString = val->ToDetailString(isolate->GetCurrentContext()).ToLocalChecked(); - return ArgConverter::ConvertToString(v8FunctionString); - } else if (val->IsArray()) { - auto context = isolate->GetCurrentContext(); - auto cachedSelf = val; - auto array = val->ToObject(context).ToLocalChecked(); - auto arrayEntryKeys = array->GetPropertyNames(isolate->GetCurrentContext()).ToLocalChecked(); - - auto arrayLength = arrayEntryKeys->Length(); - std::string argString = "["; - - for (int i = 0; i < arrayLength; i++) { - auto propertyName = arrayEntryKeys->Get(context, i).ToLocalChecked(); - auto propertyValue = array->Get(context, propertyName).ToLocalChecked(); - - // avoid bottomless recursion with cyclic reference to the same array - if (propertyValue->StrictEquals(cachedSelf)) { - argString.append("[Circular]"); - continue; - } - - auto objectString = buildStringFromArg(isolate, propertyValue); - argString.append(objectString); - - if (i != arrayLength - 1) { - argString.append(", "); - } - } + // Top-level strings print raw (console.log("hi") -> hi); everything else + // that can carry structure goes through the inspect builtin. + if (val->IsString()) { + return ArgConverter::ConvertToString(val.As()); + } + if (val->IsObject()) { + return inspectValue(isolate, val); + } - return argString.append("]"); - } else if (val->IsObject()) { - v8::Local obj = val.As(); - return transformJSObject(isolate, obj); - } else { - auto v8DefaultToString = val->ToDetailString(isolate->GetCurrentContext()).ToLocalChecked(); - return ArgConverter::ConvertToString(v8DefaultToString); + v8::TryCatch tc(isolate); + v8::Local detail; + if (val->ToDetailString(isolate->GetCurrentContext()).ToLocal(&detail)) { + return ArgConverter::ConvertToString(detail); } + return ""; } std::string buildLogString(const v8::FunctionCallbackInfo& info, int startingIndex = 0) { @@ -290,7 +350,6 @@ void Console::warnCallback(const v8::FunctionCallbackInfo& info) { void Console::dirCallback(const v8::FunctionCallbackInfo& info) { try { auto isolate = info.GetIsolate(); - auto context = isolate->GetCurrentContext(); v8::HandleScope scope(isolate); @@ -300,40 +359,7 @@ void Console::dirCallback(const v8::FunctionCallbackInfo& info) { if (argLen) { if (info[0]->IsObject()) { ss << "==== object dump start ====" << std::endl; - v8::Local argObject = info[0].As(); - - v8::Local propNames; - argObject->GetPropertyNames(context).ToLocal(&propNames); - - auto propertiesLen = propNames->Length(); - for (int i = 0; i < propertiesLen; i++) { - auto propertyName = propNames->Get(context, i).ToLocalChecked(); - auto propertyValue = argObject->Get(context, propertyName).ToLocalChecked(); - - auto propIsFunction = propertyValue->IsFunction(); - - ss << ArgConverter::ConvertToString(propertyName->ToString(context).ToLocalChecked()); - - if (propIsFunction) { - ss << "()"; - } else if (propertyValue->IsArray()) { - std::string jsonStringifiedArray = buildStringFromArg(isolate, propertyValue); - ss << ": " << jsonStringifiedArray; - } else if (propertyValue->IsObject()) { - auto obj = propertyValue->ToObject(context).ToLocalChecked(); - auto jsonStringifiedObject = transformJSObject(isolate, obj); - // if object prints out as the error string for circular references, replace with #CR instead for brevity - if (jsonStringifiedObject.find("circular structure") != std::string::npos) { - jsonStringifiedObject = "#CR"; - } - ss << ": " << jsonStringifiedObject; - } else { - ss << ": \"" << ArgConverter::ConvertToString(propertyValue->ToDetailString(context).ToLocalChecked()) << "\""; - } - - ss << std::endl; - } - + ss << inspectValue(isolate, info[0], 4) << std::endl; ss << "==== object dump end ====" << std::endl; } else { std::string logString = buildLogString(info); @@ -537,6 +563,13 @@ void Console::timeEndCallback(const v8::FunctionCallbackInfo& info) { void Console::onDisposeIsolate(v8::Isolate* isolate) { s_isolateToConsoleTimersMap.erase(isolate); + + std::lock_guard lock(inspectMutex); + auto it = isolateToInspect.find(isolate); + if (it != isolateToInspect.end()) { + delete it->second; + isolateToInspect.erase(it); + } } const char* Console::LOG_TAG = "JS"; diff --git a/test-app/runtime/src/main/cpp/console/Console.h b/test-app/runtime/src/main/cpp/console/Console.h index d5a4b94f3..8cd160e3b 100644 --- a/test-app/runtime/src/main/cpp/console/Console.h +++ b/test-app/runtime/src/main/cpp/console/Console.h @@ -41,6 +41,8 @@ class Console { static const char* LOG_TAG; static std::map> s_isolateToConsoleTimersMap; + static void initInspect(v8::Local context); + // heavily inspired by 'createBoundFunctionProperty' of V8's v8-console.h static void bindFunctionProperty(v8::Local context, v8::Local consoleInstance, diff --git a/test-app/runtime/src/main/cpp/js/README.md b/test-app/runtime/src/main/cpp/js/README.md index bf23d23a0..7025849a5 100644 --- a/test-app/runtime/src/main/cpp/js/README.md +++ b/test-app/runtime/src/main/cpp/js/README.md @@ -31,6 +31,10 @@ module.exports = somethingTheCallSiteNeeds; every tool that isn't reading this repo's ESLint config (editors' TS server, prettier, review bots) rejects the file as invalid JavaScript. - Strict mode is per-file: start the file with `"use strict";` to opt in. +- `inspect.js` is the console formatter (util.inspect-lite, exposed as the + internal `__inspect` global): budgeted output, no getter invocation, + tamper-immune via primordials. Console routes all object formatting + through it. - Destructure `binding` and `primordials` once, at the top of the file, so the file's dependencies are visible and greppable. @@ -60,9 +64,9 @@ module.exports = somethingTheCallSiteNeeds; `primordials.js` runs first in every isolate — lazily, on the first `RunBuiltin` call, which happens during runtime init — and its frozen, null-prototype export is cached per isolate (`BuiltinLoader`, released from -`disposeIsolate`) and handed to every other builtin. Builtins that compile -late (`smart-stringify` is compiled on the first object logged) therefore -still see intrinsics as they were before user code ran. +`disposeIsolate`) and handed to every other builtin, so a builtin that +compiles later in the isolate's life still sees intrinsics as they were before +user code ran. Naming follows Node: statics keep their path (`JSONStringify`, `ObjectDefineProperty`), instance methods are **uncurried** so the receiver diff --git a/test-app/runtime/src/main/cpp/js/inspect.js b/test-app/runtime/src/main/cpp/js/inspect.js new file mode 100644 index 000000000..772798e99 --- /dev/null +++ b/test-app/runtime/src/main/cpp/js/inspect.js @@ -0,0 +1,411 @@ +"use strict"; + +// Terminal formatter for console.* (util.inspect-lite). Everything here runs +// while user code may have tampered any prototype, so all intrinsic access +// goes through primordials, brands come from Object.prototype.toString rather +// than constructors, and getters are NEVER invoked. Output is budgeted: depth, +// per-collection entry caps, string caps, and a hard total-size cap make it +// impossible for a single console.log to hang the app on a huge graph. + +const { getNativeWrapperHint } = binding; +const { + ArrayBufferIsView, + ArrayBufferPrototypeGetByteLength, + ArrayIsArray, + ArrayPrototypePush, + DataViewPrototypeGetByteLength, + DatePrototypeGetTime, + DatePrototypeToISOString, + FunctionPrototypeToString, + JSONStringify, + MapIteratorPrototypeNext, + MapPrototypeEntries, + ObjectDefineProperty, + ObjectGetOwnPropertyDescriptor, + ObjectGetOwnPropertySymbols, + ObjectGetPrototypeOf, + ObjectIs, + ObjectKeys, + FunctionPrototypeCall, + ObjectPrototype, + ObjectPrototypePropertyIsEnumerable, + ObjectPrototypeToString, + RegExpPrototypeTest, + RegExpPrototypeToString, + Set, + SetIteratorPrototypeNext, + SetPrototypeAdd, + SetPrototypeDelete, + SetPrototypeHas, + SetPrototypeValues, + String, + StringPrototypeIndexOf, + StringPrototypeSlice, + SymbolPrototypeToString, + TypedArrayPrototypeGetLength, +} = primordials; + +const DEFAULT_DEPTH = 2; +const MAX_ENTRIES = 100; // array elements / object keys / map+set entries +const MAX_STRING = 10000; // characters of a single string value +const MAX_TOTAL = 16384; // hard cap for one inspect() result + +// Thrown to unwind out of a deep graph the moment the total budget is spent; +// never escapes inspect(). +const BUDGET_EXHAUSTED = { budget: true }; + +function inspect(value, options) { + const ctx = { + depth: options && typeof options.depth === "number" ? options.depth : DEFAULT_DEPTH, + remaining: MAX_TOTAL, + // In-progress ancestors only (entries are removed on exit), so shared + // acyclic references print normally and only true cycles say [Circular]. + ancestors: new Set(), + truncated: false, + }; + let result; + try { + result = formatValue(ctx, value, 0); + } catch (e) { + if (e === BUDGET_EXHAUSTED) { + ctx.truncated = true; + result = ctx.out !== undefined ? ctx.out : ""; + } else { + // A formatter bug must never take the log call down with it. + try { + result = "[inspect failed: " + safeString(e) + "]"; + } catch (ignored) { + result = "[inspect failed]"; + } + } + } + if (ctx.truncated) { + result = result + "... (output truncated)"; + } + return result; +} + +function spend(ctx, text) { + ctx.remaining -= text.length; + if (ctx.remaining < 0) { + ctx.out = StringPrototypeSlice(text, 0, text.length + ctx.remaining); + throw BUDGET_EXHAUSTED; + } + return text; +} + +function safeString(value) { + try { + return String(value); + } catch (ignored) { + return "?"; + } +} + +function quoteString(ctx, str) { + let clipped = str; + let suffix = ""; + if (clipped.length > MAX_STRING) { + clipped = StringPrototypeSlice(clipped, 0, MAX_STRING); + suffix = "... " + (str.length - MAX_STRING) + " more characters"; + } + // JSONStringify escapes quotes/control characters; a string exotic enough to + // defeat it (lone surrogates are fine) just falls back to raw. + let quoted; + try { + quoted = JSONStringify(clipped); + } catch (ignored) { + quoted = "'" + clipped + "'"; + } + return quoted + suffix; +} + +function formatValue(ctx, value, depth) { + // Primitives. + if (value === null) return "null"; + const type = typeof value; + if (type === "undefined") return "undefined"; + if (type === "string") return quoteString(ctx, value); + if (type === "number") return ObjectIs(value, -0) ? "-0" : String(value); + if (type === "boolean") return value ? "true" : "false"; + if (type === "bigint") return String(value) + "n"; + if (type === "symbol") return SymbolPrototypeToString(value); + + if (type === "function") return formatFunction(value); + + // Objects. Native wrappers first: never walk into a native-backed graph. + const hint = getNativeWrapperHint(value); + if (hint !== undefined) { + return "[" + hint + "]"; + } + + if (SetPrototypeHas(ctx.ancestors, value)) { + return "[Circular]"; + } + if (depth > ctx.depth) { + const brand = ObjectPrototypeToString(value); + return brand === "[object Object]" ? "[Object]" : "[" + StringPrototypeSlice(brand, 8, -1) + "]"; + } + + SetPrototypeAdd(ctx.ancestors, value); + try { + return formatObject(ctx, value, depth); + } finally { + SetPrototypeDelete(ctx.ancestors, value); + } +} + +function formatFunction(fn) { + let name = ""; + const desc = ObjectGetOwnPropertyDescriptor(fn, "name"); + if (desc !== undefined && typeof desc.value === "string") { + name = desc.value; + } + let isClass = false; + try { + const src = FunctionPrototypeToString(fn); + isClass = StringPrototypeIndexOf(src, "class") === 0; + } catch (ignored) { + // Some callables (bound functions render fine; revoked proxies do not) + // refuse toString; treat them as plain functions. + } + if (isClass) { + return name === "" ? "[class (anonymous)]" : "[class " + name + "]"; + } + return name === "" ? "[Function (anonymous)]" : "[Function: " + name + "]"; +} + +function formatObject(ctx, value, depth) { + const brand = ObjectPrototypeToString(value); + + if (ArrayIsArray(value)) { + return formatArray(ctx, value, depth); + } + if (brand === "[object Map]") { + return formatMapLike(ctx, value, depth, true); + } + if (brand === "[object Set]") { + return formatMapLike(ctx, value, depth, false); + } + if (brand === "[object Date]") { + const time = DatePrototypeGetTime(value); + return time !== time ? "Invalid Date" : DatePrototypeToISOString(value); + } + if (brand === "[object RegExp]") { + return RegExpPrototypeToString(value); + } + if (brand === "[object Error]") { + return formatError(ctx, value); + } + if (brand === "[object Promise]") { + return "Promise {}"; + } + if (ArrayBufferIsView(value)) { + // TypedArray/DataView: brand carries the concrete type name; sizes come + // from the captured accessor getters, not the (tamperable) prototype. + const name = StringPrototypeSlice(brand, 8, -1); + let size; + try { + size = brand === "[object DataView]" + ? DataViewPrototypeGetByteLength(value) + : TypedArrayPrototypeGetLength(value); + } catch (ignored) { + return name; + } + return name + "(" + size + ")"; + } + if (brand === "[object ArrayBuffer]" || brand === "[object SharedArrayBuffer]") { + let size; + try { + size = ArrayBufferPrototypeGetByteLength(value); + } catch (ignored) { + return StringPrototypeSlice(brand, 8, -1); + } + return StringPrototypeSlice(brand, 8, -1) + "(" + size + ")"; + } + + // NativeScript core (ViewBase, Observable, ...) and app classes override + // toString for short debug forms; the old console honored that, so a custom + // toString wins over structural rendering. This is the second deliberate + // exception to the no-user-code rule (with error.stack), guarded and capped. + const customToString = findCustomToString(value); + if (customToString !== undefined) { + try { + const str = FunctionPrototypeCall(customToString, value); + if (typeof str === "string" && str !== "") { + return spend(ctx, str.length > MAX_STRING ? StringPrototypeSlice(str, 0, MAX_STRING) : str); + } + } catch (ignored) { + } + } + + return formatPlainObject(ctx, value, depth); +} + +// A toString override that is NOT Object.prototype's: own property first, then +// up the chain, stopping at Object.prototype. Data properties only — a +// toString defined as an accessor is not worth invoking a getter for. +function findCustomToString(value) { + let target = value; + for (let i = 0; target !== null && target !== ObjectPrototype && i < 8; i++) { + const desc = ObjectGetOwnPropertyDescriptor(target, "toString"); + if (desc !== undefined) { + return typeof desc.value === "function" ? desc.value : undefined; + } + target = ObjectGetPrototypeOf(target); + } + return undefined; +} + +function formatError(ctx, err) { + // Exception to the no-getters rule: V8 materializes `stack` through a lazy + // own accessor, and the stack is the whole point of printing an error. The + // guarded read carries the same exposure as every other error-reporting + // path in the runtime that touches `.stack`. + try { + const stack = err.stack; + if (typeof stack === "string" && stack !== "") { + return spend(ctx, stack); + } + } catch (ignored) { + } + const messageDesc = ObjectGetOwnPropertyDescriptor(err, "message"); + const message = messageDesc !== undefined && typeof messageDesc.value === "string" + ? messageDesc.value : ""; + let name = "Error"; + const nameDesc = ObjectGetOwnPropertyDescriptor(err, "name"); + if (nameDesc !== undefined && typeof nameDesc.value === "string") { + name = nameDesc.value; + } + return message === "" ? name : name + ": " + message; +} + +function formatArray(ctx, arr, depth) { + const parts = []; + const len = arr.length; + const shown = len > MAX_ENTRIES ? MAX_ENTRIES : len; + let emptyRun = 0; + for (let i = 0; i < shown; i++) { + const desc = ObjectGetOwnPropertyDescriptor(arr, i); + if (desc === undefined) { + emptyRun++; + continue; + } + if (emptyRun > 0) { + ArrayPrototypePush(parts, "<" + emptyRun + " empty items>"); + emptyRun = 0; + } + ArrayPrototypePush(parts, formatProperty(ctx, desc, depth)); + } + if (emptyRun > 0) { + ArrayPrototypePush(parts, "<" + emptyRun + " empty items>"); + } + if (len > shown) { + ArrayPrototypePush(parts, "... " + (len - shown) + " more items"); + } + return spend(ctx, parts.length === 0 ? "[]" : "[ " + join(parts) + " ]"); +} + +function formatMapLike(ctx, coll, depth, isMap) { + const parts = []; + let count = 0; + let total = 0; + const iter = isMap ? MapPrototypeEntries(coll) : SetPrototypeValues(coll); + const next = isMap ? MapIteratorPrototypeNext : SetIteratorPrototypeNext; + for (;;) { + const step = next(iter); + if (step.done) break; + total++; + if (count < MAX_ENTRIES) { + count++; + if (isMap) { + const entry = step.value; + ArrayPrototypePush(parts, + formatValue(ctx, entry[0], depth + 1) + " => " + formatValue(ctx, entry[1], depth + 1)); + } else { + ArrayPrototypePush(parts, formatValue(ctx, step.value, depth + 1)); + } + } + } + if (total > count) { + ArrayPrototypePush(parts, "... " + (total - count) + " more items"); + } + const label = (isMap ? "Map(" : "Set(") + total + ")"; + return spend(ctx, parts.length === 0 ? label + " {}" : label + " { " + join(parts) + " }"); +} + +function formatPlainObject(ctx, value, depth) { + const parts = []; + const keys = ObjectKeys(value); + const shown = keys.length > MAX_ENTRIES ? MAX_ENTRIES : keys.length; + for (let i = 0; i < shown; i++) { + const key = keys[i]; + const desc = ObjectGetOwnPropertyDescriptor(value, key); + if (desc === undefined) continue; + ArrayPrototypePush(parts, formatKey(key) + ": " + formatProperty(ctx, desc, depth)); + } + if (keys.length > shown) { + ArrayPrototypePush(parts, "... " + (keys.length - shown) + " more properties"); + } + const symbols = ObjectGetOwnPropertySymbols(value); + for (let i = 0; i < symbols.length && i < MAX_ENTRIES; i++) { + if (ObjectPrototypePropertyIsEnumerable(value, symbols[i])) { + const desc = ObjectGetOwnPropertyDescriptor(value, symbols[i]); + ArrayPrototypePush(parts, + "[" + SymbolPrototypeToString(symbols[i]) + "]: " + formatProperty(ctx, desc, depth)); + } + } + + let prefix = ""; + const proto = ObjectGetPrototypeOf(value); + if (proto === null) { + prefix = "[Object: null prototype] "; + } else { + const ctorDesc = ObjectGetOwnPropertyDescriptor(proto, "constructor"); + const ctor = ctorDesc !== undefined ? ctorDesc.value : undefined; + if (typeof ctor === "function") { + const nameDesc = ObjectGetOwnPropertyDescriptor(ctor, "name"); + const name = nameDesc !== undefined ? nameDesc.value : undefined; + if (typeof name === "string" && name !== "" && name !== "Object") { + prefix = name + " "; + } + } + } + + return spend(ctx, parts.length === 0 ? prefix + "{}" : prefix + "{ " + join(parts) + " }"); +} + +// A log call must never execute user code: accessors render as tags. +function formatProperty(ctx, desc, depth) { + if (desc.get !== undefined) { + return desc.set !== undefined ? "[Getter/Setter]" : "[Getter]"; + } + if (desc.set !== undefined) { + return "[Setter]"; + } + return formatValue(ctx, desc.value, depth + 1); +} + +const IDENT = /^[A-Za-z_$][A-Za-z0-9_$]*$/; +function formatKey(key) { + return RegExpPrototypeTest(IDENT, key) ? key : "'" + key + "'"; +} + +function join(parts) { + let out = ""; + for (let i = 0; i < parts.length; i++) { + out += (i === 0 ? "" : ", ") + parts[i]; + } + return out; +} + +// Testability + app-level escape hatch (util.inspect-lite); installed eagerly +// by Console::Init running this builtin. +ObjectDefineProperty(global, "__inspect", { + writable: false, + enumerable: false, + configurable: false, + value: inspect, +}); + +module.exports = inspect; diff --git a/test-app/runtime/src/main/cpp/js/primordials.js b/test-app/runtime/src/main/cpp/js/primordials.js index 442e9488c..7685ec7f4 100644 --- a/test-app/runtime/src/main/cpp/js/primordials.js +++ b/test-app/runtime/src/main/cpp/js/primordials.js @@ -24,14 +24,23 @@ const intrinsics = { Date, Map, Proxy, + Set, String, TypeError, + // Namespaces / prototypes. + ObjectPrototype: Object.prototype, + // Statics. + ArrayBufferIsView: ArrayBuffer.isView, ArrayIsArray: Array.isArray, JSONStringify: JSON.stringify, ObjectCreate: Object.create, ObjectDefineProperty: Object.defineProperty, + ObjectGetOwnPropertyDescriptor: Object.getOwnPropertyDescriptor, + ObjectGetOwnPropertySymbols: Object.getOwnPropertySymbols, + ObjectGetPrototypeOf: Object.getPrototypeOf, + ObjectIs: Object.is, ObjectKeys: Object.keys, // Instance methods, uncurried. @@ -40,14 +49,47 @@ const intrinsics = { ArrayPrototypePush: uncurryThis(Array.prototype.push), ArrayPrototypeSlice: uncurryThis(Array.prototype.slice), ArrayPrototypeSplice: uncurryThis(Array.prototype.splice), + DatePrototypeGetTime: uncurryThis(Date.prototype.getTime), + DatePrototypeToISOString: uncurryThis(Date.prototype.toISOString), DatePrototypeToJSON: uncurryThis(Date.prototype.toJSON), FunctionPrototypeApply: uncurryThis(FunctionPrototypeApply), FunctionPrototypeCall: uncurryThis(FunctionPrototypeCall), + FunctionPrototypeToString: uncurryThis(Function.prototype.toString), MapPrototypeDelete: uncurryThis(Map.prototype.delete), + MapPrototypeEntries: uncurryThis(Map.prototype.entries), MapPrototypeGet: uncurryThis(Map.prototype.get), MapPrototypeSet: uncurryThis(Map.prototype.set), + ObjectPrototypePropertyIsEnumerable: uncurryThis(Object.prototype.propertyIsEnumerable), + ObjectPrototypeToString: uncurryThis(Object.prototype.toString), PromisePrototypeCatch: uncurryThis(Promise.prototype.catch), PromisePrototypeThen: uncurryThis(Promise.prototype.then), + RegExpPrototypeTest: uncurryThis(RegExp.prototype.test), + RegExpPrototypeToString: uncurryThis(RegExp.prototype.toString), + SetPrototypeAdd: uncurryThis(Set.prototype.add), + SetPrototypeDelete: uncurryThis(Set.prototype.delete), + SetPrototypeHas: uncurryThis(Set.prototype.has), + SetPrototypeValues: uncurryThis(Set.prototype.values), + StringPrototypeIndexOf: uncurryThis(String.prototype.indexOf), + StringPrototypeSlice: uncurryThis(String.prototype.slice), + SymbolPrototypeToString: uncurryThis(Symbol.prototype.toString), + + // Iterator-protocol escape hatches: the captured `next` of the live map/set + // iterator prototypes, so entries can be walked with early exit even after + // user code tampers %MapIteratorPrototype%/%SetIteratorPrototype%. + MapIteratorPrototypeNext: uncurryThis( + Object.getPrototypeOf(new Map()[Symbol.iterator]()).next), + SetIteratorPrototypeNext: uncurryThis( + Object.getPrototypeOf(new Set()[Symbol.iterator]()).next), + + // Captured accessor getters: reading .length/.byteLength off a view via the + // prototype would invoke whatever getter user code installed there. + TypedArrayPrototypeGetLength: uncurryThis( + Object.getOwnPropertyDescriptor( + Object.getPrototypeOf(Uint8Array.prototype), "length").get), + ArrayBufferPrototypeGetByteLength: uncurryThis( + Object.getOwnPropertyDescriptor(ArrayBuffer.prototype, "byteLength").get), + DataViewPrototypeGetByteLength: uncurryThis( + Object.getOwnPropertyDescriptor(DataView.prototype, "byteLength").get), }; Object.setPrototypeOf(intrinsics, null); diff --git a/test-app/runtime/src/main/cpp/js/smart-stringify.js b/test-app/runtime/src/main/cpp/js/smart-stringify.js deleted file mode 100644 index 5824f9d1c..000000000 --- a/test-app/runtime/src/main/cpp/js/smart-stringify.js +++ /dev/null @@ -1,19 +0,0 @@ -const { ArrayPrototypeIndexOf, ArrayPrototypePush, JSONStringify } = primordials; - -function smartStringify(object) { - const seen = []; - var replacer = function (key, value) { - if (value != null && typeof value == "object") { - if (ArrayPrototypeIndexOf(seen, value) >= 0) { - if (key) { - return "[Circular]"; - } - return; - } - ArrayPrototypePush(seen, value); - } - return value; - }; - return JSONStringify(object, replacer, 2); -} -module.exports = smartStringify;