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
+65 -8

No files matched your search

+9
View File
@@ -1,4 +1,5 @@
"use strict"; "use strict";
(function () {
// Pre-declare all internal globals as non-enumerable so they are invisible // Pre-declare all internal globals as non-enumerable so they are invisible
// to Object.keys(window) / for-in enumeration. Must run before any var // 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. // when SPAs dynamically insert multiple <script module> tags at once.
let __dynScriptQueue = []; let __dynScriptQueue = [];
let __dynScriptBusy = false; let __dynScriptBusy = false;
Object.defineProperty(globalThis, '__obscura_hasPendingDynamicScripts', {
value: function() { return __dynScriptBusy || __dynScriptQueue.length > 0; },
writable: false,
enumerable: false,
configurable: false,
});
function _decodeDataScriptUrl(url) { function _decodeDataScriptUrl(url) {
const comma = url.indexOf(','); const comma = url.indexOf(',');
if (!url.startsWith('data:') || comma < 5) { if (!url.startsWith('data:') || comma < 5) {
@@ -8959,3 +8966,5 @@ if (typeof Response !== 'undefined' && Response.prototype && !Response.prototype
if (typeof val === 'function') { walk(val); } if (typeof val === 'function') { walk(val); }
} }
})(); })();
})();
+56 -8
View File
@@ -860,15 +860,14 @@ impl ObscuraJsRuntime {
} }
/// Whether the serialized dynamic-script queue is still fetching or /// Whether the serialized dynamic-script queue is still fetching or
/// evaluating a script. The queue variables are global lexicals rather /// evaluating a script. The queue stays private to the bootstrap closure;
/// than window properties, so page code cannot overwrite this state. /// 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 { pub fn has_pending_dynamic_scripts(&mut self) -> bool {
self.evaluate( self.evaluate("globalThis.__obscura_hasPendingDynamicScripts?.() === true")
"typeof __dynScriptBusy !== 'undefined' && (__dynScriptBusy || __dynScriptQueue.length > 0)", .ok()
) .and_then(|value| value.as_bool())
.ok() .unwrap_or(false)
.and_then(|value| value.as_bool())
.unwrap_or(false)
} }
/// Arm a hard wall-clock backstop on synchronous V8 work. A page stuck in a /// Arm a hard wall-clock backstop on synchronous V8 work. A page stuck in a
@@ -2500,6 +2499,55 @@ mod tests {
assert_eq!(result, serde_json::json!(["A", "B"])); 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 /// Regression test for #147: a TypeError in one script must not poison
/// the runtime so that subsequent scripts (or DOM queries) collapse to /// the runtime so that subsequent scripts (or DOM queries) collapse to
/// empty. The reporter saw `--dump text` return 1 byte after offside.js /// empty. The reporter saw `--dump text` return 1 byte after offside.js