fix(js): isolate bootstrap declarations

This commit is contained in:
SGavrl committed 2026-08-03 16:31:30 +00:00
1 parent 23a8a3111b
commit 6a55b63fa6
2 files changed
+62 -5

No files matched your search

+9
View File
@@ -1,4 +1,5 @@
"use strict";
(function () {
// Pre-declare all internal globals as non-enumerable so they are invisible
// to Object.keys(window) / for-in enumeration. Must run before any var
@@ -175,6 +176,12 @@ let _fpSeed = 0;
// when SPAs dynamically insert multiple <script module> tags at once.
let __dynScriptQueue = [];
let __dynScriptBusy = false;
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
value: function() { return __dynScriptBusy || __dynScriptQueue.length > 0; },
writable: false,
enumerable: false,
configurable: false,
});
function _decodeDataScriptUrl(url) {
const comma = url.indexOf(',');
if (!url.startsWith('data:') || comma < 5) {
@@ -8959,3 +8966,5 @@ if (typeof Response !== 'undefined' && Response.prototype && !Response.prototype
if (typeof val === 'function') { walk(val); }
}
})();
})();
+53 -5
View File
@@ -860,12 +860,11 @@ impl ObscuraJsRuntime {
}
/// Whether the serialized dynamic-script queue is still fetching or
/// evaluating a script. The queue variables are global lexicals rather
/// than window properties, so page code cannot overwrite this state.
/// evaluating a script. The queue stays private to the bootstrap closure;
/// Rust reads it through a hidden status function so page declarations
/// cannot collide with or overwrite the queue itself.
pub fn has_pending_dynamic_scripts(&mut self) -> bool {
self.evaluate(
"typeof __dynScriptBusy !== 'undefined' && (__dynScriptBusy || __dynScriptQueue.length > 0)",
)
self.evaluate("globalThis.__obscura_hasPendingDynamicScripts?.() === true")
.ok()
.and_then(|value| value.as_bool())
.unwrap_or(false)
@@ -2500,6 +2499,55 @@ mod tests {
assert_eq!(result, serde_json::json!(["A", "B"]));
}
#[test]
fn page_var_declarations_do_not_collide_with_dom_interfaces() {
let mut rt = setup_runtime("<html><body></body></html>");
rt.execute_script(
"legacy-node-guard",
"if (!window.Node) { var Node = {}; } globalThis.__legacyNodeRan = true;",
)
.unwrap();
assert_eq!(
rt.evaluate("globalThis.__legacyNodeRan").unwrap(),
serde_json::json!(true)
);
rt.execute_script(
"page-element",
"var Element = function PageElement() {}; globalThis.__createdTag = document.createElement('div').tagName;",
)
.unwrap();
assert_eq!(
rt.evaluate("globalThis.__createdTag").unwrap(),
serde_json::json!("DIV")
);
}
#[test]
fn dynamic_script_status_bridge_is_hidden_and_idle() {
let mut rt = setup_runtime("<html><body></body></html>");
assert!(!rt.has_pending_dynamic_scripts());
assert_eq!(
rt.evaluate("typeof __dynScriptBusy").unwrap(),
serde_json::json!("undefined")
);
assert_eq!(
rt.evaluate(
"Object.getOwnPropertyNames(globalThis).includes('__obscura_hasPendingDynamicScripts')"
)
.unwrap(),
serde_json::json!(false)
);
assert_eq!(
rt.evaluate(
"Reflect.ownKeys(globalThis).includes('__obscura_hasPendingDynamicScripts')"
)
.unwrap(),
serde_json::json!(false)
);
}
/// Regression test for #147: a TypeError in one script must not poison
/// the runtime so that subsequent scripts (or DOM queries) collapse to
/// empty. The reporter saw `--dump text` return 1 byte after offside.js