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
7 changes: 6 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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.`,
}));
Expand Down
1 change: 1 addition & 0 deletions test-app/app/src/main/assets/app/mainpage.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
158 changes: 158 additions & 0 deletions test-app/app/src/main/assets/app/tests/testInspect.js
Original file line number Diff line number Diff line change
@@ -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 } }");
});
});

9 changes: 4 additions & 5 deletions test-app/app/src/main/assets/app/tests/testPrimordials.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
2 changes: 1 addition & 1 deletion test-app/runtime/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 3 additions & 3 deletions test-app/runtime/src/main/cpp/BuiltinLoader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,9 @@ MaybeLocal<Value> CallBuiltin(Local<Context> context, BuiltinId id, Local<Value>

/*
* 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<Object> GetPrimordials(Local<Context> context) {
Isolate* isolate = v8::Isolate::GetCurrent();
Expand Down
1 change: 0 additions & 1 deletion test-app/runtime/src/main/cpp/IsolateDisposer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
60 changes: 58 additions & 2 deletions test-app/runtime/src/main/cpp/MetadataNode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,52 @@ void MetadataNode::Init(Isolate* isolate) {
auto key = ArgConverter::ConvertToV8String(isolate, "tns::MetadataKey");
auto cache = GetMetadataNodeCache(isolate);
cache->MetadataKey = new Persistent<String>(isolate, key);
cache->PackageKey = new Persistent<String>(
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<Object>& value, Persistent<String>* key) {
if (key == nullptr) {
return nullptr;
}

auto context = isolate->GetCurrentContext();
if (context.IsEmpty()) {
return nullptr;
}

Local<Value> hidden;
auto privateKey = Private::ForApi(isolate, Local<String>::New(isolate, *key));
if (!value->GetPrivate(context, privateKey).ToLocal(&hidden) || !hidden->IsExternal()) {
return nullptr;
}

return hidden.As<External>()->Value(v8::kExternalPointerTypeTagDefault);
}

bool MetadataNode::TryGetInstanceTypeName(Isolate* isolate, const Local<Object>& value, std::string& out) {
auto cache = GetMetadataNodeCache(isolate);
auto node = static_cast<MetadataNode*>(TryReadPrivateExternal(isolate, value, cache->MetadataKey));
if (node == nullptr) {
return false;
}

out = node->m_name;
return true;
}

bool MetadataNode::TryGetPackageName(Isolate* isolate, const Local<Object>& value, std::string& out) {
auto cache = GetMetadataNodeCache(isolate);
auto node = static_cast<MetadataNode*>(TryReadPrivateExternal(isolate, value, cache->PackageKey));
if (node == nullptr) {
return false;
}

out = node->m_name;
return true;
}

Local<ObjectTemplate> MetadataNode::GetOrCreateArrayObjectTemplate(Isolate* isolate) {
Expand Down Expand Up @@ -234,10 +280,20 @@ Local<Object> MetadataNode::CreateArrayWrapper(Isolate* isolate) {

Local<Object> 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<String>::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,
Expand Down
13 changes: 13 additions & 0 deletions test-app/runtime/src/main/cpp/MetadataNode.h
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,17 @@ class MetadataNode {

static std::string GetTypeMetadataName(v8::Isolate* isolate, v8::Local<v8::Value>& 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<v8::Object>& value, std::string& out);

static bool TryGetPackageName(v8::Isolate* isolate, const v8::Local<v8::Object>& value, std::string& out);

static void onDisposeIsolate(v8::Isolate* isolate);

static MetadataReader* getMetadataReader();
Expand Down Expand Up @@ -267,6 +278,8 @@ class MetadataNode {
struct MetadataNodeCache {
v8::Persistent<v8::String>* MetadataKey;

v8::Persistent<v8::String>* PackageKey;

robin_hood::unordered_map<MetadataTreeNode*, CtorCacheData> CtorFuncCache;

robin_hood::unordered_map<std::string, MetadataNode::ExtendedClassCacheData> ExtendedCtorFuncCache;
Expand Down
Loading