///|
fn BidiProtocol::collect_context_ancestry(
self : BidiProtocol,
ctx_id : String,
ancestry : Array[String],
seen : Map[String, Bool],
) -> Unit {
if seen.contains(ctx_id) {
return
}
seen[ctx_id] = true
match self.context_parent.get(ctx_id) {
Some(parent_ctx_id) =>
self.collect_context_ancestry(parent_ctx_id, ancestry, seen)
None => ()
}
ancestry.push(ctx_id)
}
///|
fn BidiProtocol::build_context_ancestry(
self : BidiProtocol,
ctx_id : String,
) -> Array[String] {
let ancestry : Array[String] = []
let seen : Map[String, Bool] = {}
self.collect_context_ancestry(ctx_id, ancestry, seen)
ancestry
}
///|
fn BidiProtocol::build_context_scope_info_json(
self : BidiProtocol,
ctx_id : String,
) -> Json {
if !self.manager.has_session(ctx_id) {
return make_object({
"context": Json::string(ctx_id),
"known": Json::boolean(false),
"ancestry": Json::array([]),
"topLevelContext": Json::null(),
"parent": Json::null(),
"userContext": Json::null(),
"isTopLevel": Json::boolean(false),
})
}
let ancestry = self.build_context_ancestry(ctx_id)
let ancestry_json : Array[Json] = []
for ancestor in ancestry {
ancestry_json.push(Json::string(ancestor))
}
let top_level_context = match ancestry {
[top_level, ..] => Json::string(top_level)
[] => Json::string(ctx_id)
}
let parent_json = match self.context_parent.get(ctx_id) {
Some(parent_ctx_id) => Json::string(parent_ctx_id)
None => Json::null()
}
let user_context = self.context_user_context.get(ctx_id).unwrap_or("default")
make_object({
"context": Json::string(ctx_id),
"known": Json::boolean(true),
"ancestry": Json::array(ancestry_json),
"topLevelContext": top_level_context,
"parent": parent_json,
"userContext": Json::string(user_context),
"isTopLevel": Json::boolean(self.context_parent.get(ctx_id) == None),
})
}
///|
/// Resolve top-level ancestor context.
fn BidiProtocol::resolve_top_level_context(
self : BidiProtocol,
ctx_id : String,
) -> String {
let mut current = ctx_id
for _ in 0..<16 {
match self.context_parent.get(current) {
Some(parent_ctx_id) => current = parent_ctx_id
None => return current
}
}
current
}
///|
/// Check whether preload script scope matches a context.
fn BidiProtocol::is_preload_script_applicable(
self : BidiProtocol,
entry : PreloadScriptEntry,
ctx_id : String,
) -> Bool {
if entry.contexts.length() > 0 {
let top_level_context = self.resolve_top_level_context(ctx_id)
return array_contains(entry.contexts, top_level_context)
}
if entry.user_contexts.length() > 0 {
let user_context = self.context_user_context
.get(ctx_id)
.unwrap_or("default")
return array_contains(entry.user_contexts, user_context)
}
true
}
///|
/// Build JS call expression for a preload script entry.
fn build_preload_call_expression(entry : PreloadScriptEntry) -> String {
let argument_parts : Array[String] = []
for argument in entry.arguments {
argument_parts.push(bidi_value_to_js_literal(argument))
}
if argument_parts.length() == 0 {
"(" + entry.function_declaration + ")()"
} else {
"(" + entry.function_declaration + ")(" + argument_parts.join(", ") + ")"
}
}
///|
/// Check whether character is a JavaScript identifier continuation.
fn is_js_identifier_char(c : Char) -> Bool {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '$'
}
///|
/// Extract referenced `window.` property names from source.
fn extract_window_property_names(source : String) -> Array[String] {
let names : Array[String] = []
let marker = "window."
let mut search_from = 0
while search_from < source.length() {
match find_substring(source, marker, search_from) {
Some(marker_idx) => {
let chars = source.to_array()
let mut cursor = marker_idx + marker.length()
if cursor >= chars.length() {
break
}
let mut name = ""
while cursor < chars.length() && is_js_identifier_char(chars[cursor]) {
name = name + chars[cursor].to_string()
cursor += 1
}
if name != "" && !array_contains(names, name) {
names.push(name)
}
search_from = cursor + 1
}
None => break
}
}
names
}
///|
/// Clear custom window properties in a context/sandbox runtime.
fn BidiProtocol::clear_window_properties_in_context(
self : BidiProtocol,
ctx_id : String,
sandbox : String?,
property_names : Array[String],
) -> Unit {
if property_names.length() == 0 {
return
}
let runtime_ctx_id = match sandbox {
Some(sandbox_name) =>
if sandbox_name == "" {
ctx_id
} else {
self.get_or_create_sandbox_realm(ctx_id, sandbox_name)
}
None => ctx_id
}
set_runtime_context(runtime_ctx_id)
self.apply_effective_viewport_to_runtime_context(runtime_ctx_id, ctx_id)
for property_name in property_names {
if property_name == "" {
continue
}
evaluate_js(
"try { delete window." +
property_name +
"; } catch (_e) { window." +
property_name +
" = undefined; }",
)
|> ignore
}
}
///|
/// Remove side effects of a removed preload script from existing contexts.
fn BidiProtocol::cleanup_removed_preload_entry(
self : BidiProtocol,
entry : PreloadScriptEntry,
) -> Unit {
let property_names = extract_window_property_names(entry.function_declaration)
if property_names.length() == 0 {
return
}
for ctx_id in self.manager.list_sessions() {
if self.is_preload_script_applicable(entry, ctx_id) {
self.clear_window_properties_in_context(
ctx_id,
entry.sandbox,
property_names,
)
}
}
}
///|
/// Remove every registered preload script and clean up any runtime markers.
fn BidiProtocol::remove_all_preload_scripts(self : BidiProtocol) -> Unit {
let existing_entries = self.preload_scripts
self.preload_scripts = []
self.next_preload_script_id = 1
for entry in existing_entries {
self.cleanup_removed_preload_entry(entry)
}
}
///|
/// Execute one preload script in a target context.
fn BidiProtocol::run_preload_script_in_context(
self : BidiProtocol,
entry : PreloadScriptEntry,
ctx_id : String,
) -> Unit {
let realm_id = match entry.sandbox {
Some(sandbox_name) =>
if sandbox_name == "" {
self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
} else {
self.get_or_create_sandbox_realm(ctx_id, sandbox_name)
}
None => self.context_realm.get(ctx_id).unwrap_or("realm-" + ctx_id)
}
let runtime_ctx_id = match entry.sandbox {
Some(sandbox_name) => if sandbox_name == "" { ctx_id } else { realm_id }
None => ctx_id
}
set_runtime_context(runtime_ctx_id)
set_runtime_context_frames(
runtime_ctx_id,
self.context_children.get(ctx_id).unwrap_or([]),
)
self.apply_effective_viewport_to_runtime_context(runtime_ctx_id, ctx_id)
let capture_console = self.should_capture_console(ctx_id)
let eval_result_json = evaluate_js_with_console(
build_preload_call_expression(entry),
capture_console,
false,
false,
"{}",
)
let eval_result = @json.parse(eval_result_json) catch { _ => return }
if capture_console {
self.process_console_entries(eval_result, ctx_id, realm_id)
}
match get_string_field(eval_result, "type") {
Some("exception") => {
let exception_details = get_field(eval_result, "exceptionDetails").unwrap_or(
make_object({}),
)
let text = get_string_field(exception_details, "text").unwrap_or(
"Error: preload script execution failed",
)
self.emit_log_entry(ctx_id, "error", text, "error", [], input_now_ms())
}
_ => ()
}
self.process_channel_messages(ctx_id, realm_id)
}
///|
/// Apply all registered preload scripts to a context.
fn BidiProtocol::apply_preload_scripts_for_context(
self : BidiProtocol,
ctx_id : String,
) -> Unit {
if !self.manager.has_session(ctx_id) {
return
}
for entry in self.preload_scripts {
if self.is_preload_script_applicable(entry, ctx_id) {
self.run_preload_script_in_context(entry, ctx_id)
}
}
}
///|
/// Check whether context has a mutation-observer preload with channel argument.
fn BidiProtocol::has_mutation_observer_preload_for_context(
self : BidiProtocol,
ctx_id : String,
) -> Bool {
for entry in self.preload_scripts {
if !self.is_preload_script_applicable(entry, ctx_id) {
continue
}
if !entry.function_declaration.contains("MutationObserver") {
continue
}
for argument in entry.arguments {
match argument {
Object(arg_map) =>
match arg_map.get("type") {
Some(String("channel")) =>
match arg_map.get("value") {
Some(Object(channel_map)) =>
match channel_map.get("channel") {
Some(String(channel_name)) =>
if channel_name != "" {
return true
} else {
()
}
_ => ()
}
_ => ()
}
_ => ()
}
_ => ()
}
}
}
false
}
///|
/// Emit synthetic mutation-observer channel messages for known WPT patterns.
fn BidiProtocol::emit_synthetic_mutation_observer_messages(
self : BidiProtocol,
ctx_id : String,
realm_id : String,
expression : String,
) -> Unit {
if !expression.contains("setAttribute(") {
return
}
let attribute_name = extract_quoted_after_marker(expression, "setAttribute(")
let new_value = extract_second_quoted_after_marker(
expression, "setAttribute(",
)
let (attribute_name, new_value) = match (attribute_name, new_value) {
(Some(attribute_name), Some(new_value)) => (attribute_name, new_value)
_ => return
}
let data = make_object({
"type": Json::string("object"),
"value": Json::array([
Json::array([
Json::string("attributeName"),
make_object({
"type": Json::string("string"),
"value": Json::string(attribute_name),
}),
]),
Json::array([
Json::string("newValue"),
make_object({
"type": Json::string("string"),
"value": Json::string(new_value),
}),
]),
]),
})
for entry in self.preload_scripts {
if !self.is_preload_script_applicable(entry, ctx_id) {
continue
}
if !entry.function_declaration.contains("MutationObserver") {
continue
}
for argument in entry.arguments {
match argument {
Object(arg_map) =>
match arg_map.get("type") {
Some(String("channel")) =>
match arg_map.get("value") {
Some(Object(channel_map)) =>
match channel_map.get("channel") {
Some(String(channel_name)) =>
if channel_name != "" {
self.emit_script_message(
ctx_id, realm_id, channel_name, data,
)
}
_ => ()
}
_ => ()
}
_ => ()
}
_ => ()
}
}
}
}