Skip to content
Merged
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
84 changes: 70 additions & 14 deletions NativeScript/runtime/js/blob-url.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const {
ArrayPrototypePush,
FunctionPrototypeCall,
Map,
MapPrototypeDelete,
MapPrototypeGet,
Expand Down Expand Up @@ -39,35 +41,89 @@ InternalAccessor.getData = function (url) {
return MapPrototypeGet(BLOB_STORE, url);
};
URL.InternalAccessor = InternalAccessor;

// Pushes the params object's serialization onto its URL and records it as the
// query string the params object is in sync with.
function writeBack(params) {
const url = params._url;
url.search = params.toString();
url._searchParamsSource = url.search;
}

// Brings `params` back in line with its URL's current query string by mutating
// it in place: url.searchParams is a same-object accessor, so the one params
// object handed out for a URL has to survive every reparse of that URL. Only
// the raw methods captured at construction are used here — the public ones are
// wrapped to write back into the URL, which would clobber the very string being
// applied.
function resync(params) {
const url = params._url;
const search = url.search;
if (url._searchParamsSource === search) {
return;
}
const names = [];
FunctionPrototypeCall(params._forEach, params, function (value, name) {
ArrayPrototypePush(names, name);
});
for (let i = 0; i < names.length; i++) {
FunctionPrototypeCall(params._delete, params, names[i]);
}
const parsed = new URLSearchParamsCtor(search);
FunctionPrototypeCall(params._forEach, parsed, function (value, name) {
FunctionPrototypeCall(params._append, params, name, value);
});
url._searchParamsSource = search;
}

ObjectDefineProperty(URL.prototype, 'searchParams', {
get() {
if (this._searchParams == null) {
this._searchParams = new URLSearchParamsCtor(this.search);
ObjectDefineProperty(this._searchParams, '_url', {
const params = new URLSearchParamsCtor(this.search);
ObjectDefineProperty(this, '_searchParams', {
enumerable: false,
writable: true,
configurable: true,
value: params,
});
ObjectDefineProperty(this, '_searchParamsSource', {
enumerable: false,
writable: true,
configurable: true,
value: this.search,
});
ObjectDefineProperty(params, '_url', {
enumerable: false,
writable: false,
value: this,
});
this._searchParams._append = this._searchParams.append;
this._searchParams.append = function (name, value) {
params._forEach = params.forEach;
params._append = params.append;
params.append = function (name, value) {
resync(this);
this._append(name, value);
this._url.search = this.toString();
writeBack(this);
};
this._searchParams._delete = this._searchParams.delete;
this._searchParams.delete = function (name) {
params._delete = params.delete;
params.delete = function (name) {
resync(this);
this._delete(name);
this._url.search = this.toString();
writeBack(this);
};
this._searchParams._set = this._searchParams.set;
this._searchParams.set = function (name, value) {
params._set = params.set;
params.set = function (name, value) {
resync(this);
this._set(name, value);
this._url.search = this.toString();
writeBack(this);
};
this._searchParams._sort = this._searchParams.sort;
this._searchParams.sort = function () {
params._sort = params.sort;
params.sort = function () {
resync(this);
this._sort();
this._url.search = this.toString();
writeBack(this);
};
Comment on lines +100 to 124

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
fd -a 'blob-url\.js$|UrlSearchParamsTests\.js$' . || true

echo "== git status/stat =="
git diff --stat || true

echo "== blob-url lines 1-160 =="
blob=$(fd 'blob-url\.js$' . | head -n1)
[ -n "${blob:-}" ] && cat -n "$blob" | sed -n '1,180p'

echo "== tests around 130-170 =="
testfile=$(fd 'UrlSearchParamsTests\.js$' . | head -n1)
[ -n "${testfile:-}" ] && cat -n "$testfile" | sed -n '110,170p'

echo "== search ObjectDefineProperty and own keys tests =="
rg -n "ObjectDefineProperty|Object\.keys|OwnPropertyKeys|for\.\\(\\w+ in|ownKeys|searchParams|_url|_searchParams|_searchParamsSource" -S .

Repository: NativeScript/ios

Length of output: 50372


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Object.defineProperty enumerable probe =="
node - <<'JS'
const obj = {};
obj._raw = Object.prototype.forEach.call;
Reflect.defineProperty(obj, '_bound', { enumerable: false, writable: true, configurable: true, value: Object.prototype.forEach.call });

console.log(objectKeysContains, forInContains);

function objectKeysContains(prop) {
  return Object.keys(obj).includes(prop);
}

function forInContains(prop) {
  let found = false;
  for (const key in obj) {
    if (key === prop) found = true;
  }
  return found;
}

console.log("_raw in Object.keys:", objectKeysContains("_raw"));
console.log("_raw in for...in:", forInContains("_raw"));
console.log("_bound in Object.keys:", objectKeysContains("_bound"));
console.log("_bound in for...in:", forInContains("_bound"));
console.log("descriptor:", Object.getOwnPropertyDescriptor(obj, "_bound"));

const keys = Object.keys(obj);
for (const key in obj) {
  console.log("enumerable key:", key);
}
console.log("has own non-enumerable:", Object.prototype.hasOwnProperty.call(obj, "_bound"));
console.log("hasOwnProperty on enumerable cache key:", Object.prototype.hasOwnProperty.call(obj, "_raw"));
JS

echo "== focused tests =="
cat -n TestRunner/app/tests/UrlSearchParamsTests.js | sed -n '1,220p'

Repository: NativeScript/ios

Length of output: 860


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Object.prototype assignment/defineProperty enumerable probe =="
node - <<'JS'
const obj = {};
obj._raw = Function.prototype.call.bind(Object.prototype.forEach);

Reflect.defineProperty(obj, '_bound', { enumerable: false, writable: true, configurable: true, value: Function.prototype.call.bind(Object.prototype.forEach) });

function objectKeysContains(prop) {
  return Object.keys(obj).includes(prop);
}

function forInContains(prop) {
  let found = false;
  for (const key in obj) {
    if (key === prop) found = true;
  }
  return found;
}

console.log("_raw in Object.keys:", objectKeysContains("_raw"));
console.log("_raw in for...in:", forInContains("_raw"));
console.log("_bound in Object.keys:", objectKeysContains("_bound"));
console.log("_bound in for...in:", forInContains("_bound"));
console.log("descriptor:", Object.getOwnPropertyDescriptor(obj, "_bound"));

const keys = Object.keys(obj);
for (const key in obj) {
  console.log("enumerable key:", key);
}
console.log("has own enumerable cache property:", Object.prototype.hasOwnProperty.call(obj, "_raw"));
JS

echo "== focused UrlSearchParamsTests =="
cat -n TestRunner/app/tests/UrlSearchParamsTests.js | sed -n '1,220p'

Repository: NativeScript/ios

Length of output: 6127


Make the params raw method caches non-enumerable.

obj._for = obj.forEach; on url.searchParams creates a writable, enumerable own property, so Object.keys(url.searchParams) and for...in expose _forEach, _append, _delete, _set, and _sort. Use ObjectDefineProperty(params, '_<name>', { enumerable: false, ... }) for each cached method, and cover searchParams enumeration in the cache test with _searchParams/_searchParamsSource.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NativeScript/runtime/js/blob-url.js` around lines 100 - 124, Update the raw
method caches in the searchParams setup to define _forEach, _append, _delete,
_set, and _sort via ObjectDefineProperty with enumerable set to false while
preserving their writable callable values. Extend the cache test using
_searchParams and _searchParamsSource to verify these internal properties are
absent from Object.keys and for...in enumeration.

} else {
resync(this._searchParams);
}
return this._searchParams;
Comment on lines 79 to 128

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== repo files matching blob-url/tests =="
git ls-files | rg '(^|/)blob-url\.js$|UrlSearchParamsTests\.js$|blob|searchParams|URLSearchParams' || true

echo
echo "== blob-url outline/size =="
if [ -f NativeScript/runtime/js/blob-url.js ]; then
  wc -l NativeScript/runtime/js/blob-url.js
  ast-grep outline NativeScript/runtime/js/blob-url.js --view compact || true
  echo "== relevant blob-url lines =="
  sed -n '1,180p' NativeScript/runtime/js/blob-url.js | cat -n
fi

echo
echo "== UrlSearchParamsTests relevant sections =="
if [ -f NativeScript/runtime/js/UrlSearchParamsTests.js ]; then
  wc -l NativeScript/runtime/js/UrlSearchParamsTests.js
  git diff -- NativeScript/runtime/js/UrlSearchParamsTests.js | sed -n '1,240p'
  rg -n "searchParams|search|href|append|get\\(|getAll\\(|has\\(|keys\\(|values\\(|entries\\(|forEach\\(|toString\\(|sort\\(|Symbol|iterator|size" NativeScript/runtime/js/UrlSearchParamsTests.js NativeScript/runtime/js/blob-url.js || true
fi

Repository: NativeScript/ios

Length of output: 6055


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test file relevant lines =="
wc -l TestRunner/app/tests/UrlSearchParamsTests.js
sed -n '1,260p' TestRunner/app/tests/UrlSearchParamsTests.js | cat -n

echo
echo "== URLSearchParams.js test list around searchParams =="
awk '
/URLSearchParams|searchParams|function \(title\)/{start=NR}
NR>=start && NR<=start+220 || NR>=1 && NR<=220 {print NR": "$0}
' TestRunner/app/tests/URLSearchParams.js | cat -n

echo
echo "== static probe for wrapped and unwrapped URLSearchParams methods in blob-url.js =="
python3 - <<'PY'
from pathlib import Path
p=Path("NativeScript/runtime/js/blob-url.js")
s=p.read_text()
start=s.index("ObjectDefineProperty(URL.prototype, 'searchParams', {")
wrap=s.index("params.append = function", start)
end=None
try:
    end=s.index("});\n);", wrap)
except ValueError:
    end=s.find("});", wrap + 1)
if wrap==-1:
    raise SystemExit("wrapper section not found")
section=s[start:end]
unwrapped_methods="get getAll has keys values entries forEach toString size".split()
for name in unwrapped_methods:
    needle=f"params.{name}"
    if name != "size":
        needle=f"{name}:"
    print(f"{name}: wrapper_assign={'params._'+name in section and 'params.'+name+' = function' in section} wrapper_calls_resync={'params._'+name+" in section and "resync(this)\n                return this._"+name in section)
print("size" in section)
PY

Repository: NativeScript/ios

Length of output: 35078


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate and inspect URLSearchParams implementation =="
fd -a 'URLSearchParamsImpl\.(h|cpp)$' . | while read -r f; do
  echo "--- $f ---"
  wc -l "$f"
  sed -n '1,260p' "$f" | cat -n
done

echo
echo "== check whether searchParams tests cover direct method stale path =="
python3 - <<'PY'
from pathlib import Path
p = Path("TestRunner/app/tests/UrlSearchParamsTests.js")
s = p.read_text()
cases = s.split("it(")
held = []
stale_read_after_url_change = []
for i, case in enumerate(cases[1:], 1):
    # crude brace/expect boundary
    code = case[:case.find(";)")]
    if "url.searchParams" in code and "url.search" in code:
        all_sp = code.count("url.searchParams")
    else:
        all_sp = 0
    if "const sp = url.searchParams" in code and "url.search = " in code:
        held.append((i, "has held sp after search reassign"))
        # Check direct read methods on sp after url.search reassign without reading url.searchParams immediately.
        after_assign = code.index("url.search = ")
        segment = code[after_assign:]
        direct_sp_reads = ["sp get", "sp getAll", "sp has", "sp keys", "sp values", "sp entries", "sp forEach", "sp toString", "sp [Symbol", "sp.iterator", "sp size"]
        if "sp.set" in segment or "sp.append" in segment or "sp.delete" in segment or "sp.sort" in segment:
            stale_read_after_url_change.append((i, "mutator on sp before direct read"))
        else:
            found = []
            for term in direct_sp_reads:
                if term in segment:
                    found.append(term)
            stale_read_after_url_change.append((i, f"has held sp after search reassign; direct reads not present: {found or 'none'}"))
print("held-after-search tests:", len(held))
print("held-after-search expectations involving sp (not url.searchParams):")
for idx, text in held:
    print(f"  {idx}: {text}")
print("held-after-search stale direct-read status:")
for idx, text in stale_read_after_url_change:
    print(f"  {idx}: {text}")
PY

echo
echo "== deterministic JS semantics probe for current wrapper coverage =="
node - <<'JS'
const methods = ["get","getAll","has","keys","values","entries","forEach","toString","size"];
const source = require('fs').readFileSync('NativeScript/runtime/js/blob-url.js','utf8');
const section = source.substring(600); // after relevant constants
for (const name of methods) {
  const pattern = new RegExp(`params\\.${name}\\s*=\\s*function`)
  console.log(name, "has wrapper", pattern.test(source), section.includes(`resync(this)`), section.includes(`return this._${name}`), section.includes(`${name}:`));
}
JS

Repository: NativeScript/ios

Length of output: 16590


Make searchParams read methods resync before returning data.

get, getAll, has, keys, values, entries, forEach, toString, size, and Symbol.iterator are currently read directly. Keep a held url.searchParams reference, assign a new search/hash, and call one of these methods; it can return the old query state because only append, delete, set, sort, and the searchParams getter itself resync. Wrap these access points with the same resync(this)/original-delegate pattern, or use a Proxy that resyncs on every access.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@NativeScript/runtime/js/blob-url.js` around lines 79 - 128, Update the
searchParams initialization logic in the URL.prototype.searchParams getter so
read methods—get, getAll, has, keys, values, entries, forEach, toString, size,
and Symbol.iterator—resync against the owning URL before returning data.
Preserve the existing original-method delegation pattern used by append, delete,
set, and sort, or apply an equivalent proxy covering every listed access point.

},
Expand Down
9 changes: 6 additions & 3 deletions TestRunner/app/tests/ErrorEventsTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,11 @@ describe("WHATWG error events", function () {
expect(received.error).toBe(err);
expect(received.message).toBe("x");
// preventDefault() in the listener must suppress the __onUncaughtError shim.
// The hook is process-wide, so assert on this error rather than on the
// spy being empty: async failures from earlier suites can still drain
// into it while this test waits.
afterQuietTurns(function () {
expect(uncaught.length).toBe(0);
expect(uncaught.indexOf(err)).toBe(-1);
done();
});
});
Expand Down Expand Up @@ -154,7 +157,7 @@ describe("WHATWG error events", function () {
expect(typeof received.reason.stackTrace).toBe("string");
expect(received.reason.stackTrace.length).toBeGreaterThan(0);
afterQuietTurns(function () {
expect(uncaught.length).toBe(0);
expect(uncaught.indexOf(reason)).toBe(-1);
done();
});
});
Expand Down Expand Up @@ -326,7 +329,7 @@ describe("WHATWG error events", function () {
expect(received).not.toBeNull();
expect(received.error).toBe(err);
afterQuietTurns(function () {
expect(uncaught.length).toBe(0);
expect(uncaught.indexOf(err)).toBe(-1);
done();
});
});
Expand Down
149 changes: 149 additions & 0 deletions TestRunner/app/tests/UrlSearchParamsTests.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
describe("URL.searchParams caching", function () {
it("returns the same object across reads", function () {
const url = new URL("https://example.com/?a=1");
expect(url.searchParams).toBe(url.searchParams);
});

it("parses the query string on the first read", function () {
const url = new URL("https://example.com/?a=1&b=2&b=3");
const sp = url.searchParams;
expect(sp.get("a")).toBe("1");
expect(sp.getAll("b")).toEqual(["2", "3"]);
expect(sp.size).toBe(3);
});

it("keeps the same object after assigning to search", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;
url.search = "?x=1";
expect(url.searchParams).toBe(sp);
});

it("refreshes after assigning to search", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;
expect(sp.get("a")).toBe("1");

url.search = "?b=2&c=3";

expect(url.searchParams.get("a")).toBe(null);
expect(url.searchParams.get("b")).toBe("2");
expect(url.searchParams.get("c")).toBe("3");
expect(url.searchParams.size).toBe(2);
expect(sp.get("b")).toBe("2");
});

it("refreshes after assigning to href", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;
expect(sp.get("a")).toBe("1");

url.href = "https://example.com/other?q=hello&lang=en";

expect(url.searchParams).toBe(sp);
expect(url.searchParams.get("a")).toBe(null);
expect(url.searchParams.get("q")).toBe("hello");
expect(url.searchParams.get("lang")).toBe("en");
});

it("refreshes to empty when the query is cleared", function () {
const url = new URL("https://example.com/?a=1&b=2");
const sp = url.searchParams;
expect(sp.size).toBe(2);

url.search = "";

expect(url.searchParams).toBe(sp);
expect(url.searchParams.size).toBe(0);
expect(url.searchParams.toString()).toBe("");
});

it("preserves duplicate keys when refreshing", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;

url.search = "?a=1&a=2&a=3";

expect(url.searchParams.getAll("a")).toEqual(["1", "2", "3"]);
expect(sp.getAll("a")).toEqual(["1", "2", "3"]);
});

it("writes mutations back to the url", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;

sp.set("a", "9");
expect(url.search).toBe("?a=9");

sp.append("b", "2");
expect(url.search).toBe("?a=9&b=2");
expect(url.href).toBe("https://example.com/?a=9&b=2");

sp.delete("a");
expect(url.search).toBe("?b=2");

sp.append("a", "0");
sp.sort();
expect(url.search).toBe("?a=0&b=2");
});

it("does not drop state on the read following a mutation", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;

sp.append("b", "2");
sp.append("b", "3");

const spAfter = url.searchParams;
expect(spAfter).toBe(sp);
expect(spAfter.getAll("b")).toEqual(["2", "3"]);
expect(spAfter.get("a")).toBe("1");
expect(spAfter.size).toBe(3);
});

it("interleaves mutations and direct assignments", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;

sp.set("a", "2");
url.search = "?b=1";

expect(url.searchParams.get("a")).toBe(null);
expect(url.searchParams.get("b")).toBe("1");

sp.set("c", "3");
expect(url.search).toBe("?b=1&c=3");
expect(url.searchParams.get("c")).toBe("3");
});

it("does not clobber a reassigned query when mutating a held reference", function () {
const url = new URL("https://example.com/?a=1");
const sp = url.searchParams;

url.search = "?b=1";
sp.set("c", "3");

expect(url.search).toBe("?b=1&c=3");
expect(sp.get("a")).toBe(null);
});

it("does not expose its cache as an enumerable property", function () {
const url = new URL("https://example.com/?a=1");
UNUSED(url.searchParams);
url.search = "?b=2";
UNUSED(url.searchParams);

const keys = Object.keys(url);
expect(keys.indexOf("_searchParams")).toBe(-1);
expect(keys.indexOf("_searchParamsSource")).toBe(-1);

const serialized = JSON.stringify(url);
expect(serialized.indexOf("_searchParams")).toBe(-1);
expect(serialized.indexOf("_searchParamsSource")).toBe(-1);

for (const key in url) {
expect(key).not.toBe("_searchParams");
expect(key).not.toBe("_searchParamsSource");
}
});
});
1 change: 1 addition & 0 deletions TestRunner/app/tests/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ require("./Timers");

require("./URL");
require("./URLSearchParams");
require("./UrlSearchParamsTests");
require("./URLPattern");

// HTTP ESM Loader tests
Expand Down
Loading