///|
/// Cross-frame DOM mirror generalization (#182 slice 5).
///
/// The synthetic iframe handler at try_handle_synthetic_iframe_creation
/// previously hardcoded ONE shape for cross-frame property access:
///
/// window.baz = iframe.contentWindow.foo
///
/// Real pages assign to arbitrary LHS targets and read property chains
/// through any iframe variable. This module extracts the (lhs, chain)
/// tuple from the expression so the handler can mirror read-only
/// property/index access.
///
/// Scope and limits:
///
/// - Only assignments of the literal form
/// ` = .contentWindow.` are recognized. Compound
/// expressions (function calls, arithmetic on the RHS) fall through
/// to the broader synthetic flow without mirror.
/// - The read source is always `window.` in the child realm.
/// - Single mirror per expression (the first match is used). The
/// hardcoded one-liner this replaces had the same constraint.
///|
/// Return true if `c` is a valid JavaScript identifier character.
fn is_iframe_mirror_identifier_char(c : Char) -> Bool {
(c >= 'a' && c <= 'z') ||
(c >= 'A' && c <= 'Z') ||
(c >= '0' && c <= '9') ||
c == '_' ||
c == '$'
}
///|
fn is_iframe_mirror_whitespace(c : Char) -> Bool {
c == ' ' || c == '\t' || c == '\n' || c == '\r'
}
///|
fn read_iframe_mirror_identifier_end(chars : Array[Char], start : Int) -> Int {
let mut end = start
while end < chars.length() && is_iframe_mirror_identifier_char(chars[end]) {
end += 1
}
end
}
///|
fn read_iframe_mirror_bracket_end(chars : Array[Char], start : Int) -> Int? {
let mut cursor = start + 1
let mut quote : Char? = None
let mut escaped = false
while cursor < chars.length() {
let c = chars[cursor]
match quote {
Some(q) =>
if escaped {
escaped = false
} else if c == '\\' {
escaped = true
} else if c == q {
quote = None
}
None =>
if c == '"' || c == '\'' {
quote = Some(c)
} else if c == ']' {
return Some(cursor + 1)
} else if c == '(' || c == ')' || c == ';' {
return None
}
}
cursor += 1
}
None
}
///|
fn skip_iframe_mirror_whitespace(chars : Array[Char], start : Int) -> Int {
let mut cursor = start
while cursor < chars.length() && is_iframe_mirror_whitespace(chars[cursor]) {
cursor += 1
}
cursor
}
///|
fn is_iframe_mirror_rhs_terminator(c : Char) -> Bool {
c == ';' || c == '}'
}
///|
/// Extract the (lhs, chain) tuple from a cross-frame mirror expression
/// like ` = .contentWindow.`. Returns None if no
/// matching pattern is present.
///
/// Examples:
///
/// - `window.baz = iframe.contentWindow.foo` → Some(("window.baz", "foo"))
/// - `globalThis.captured = f.contentWindow.theProp;` → Some(("globalThis.captured", "theProp"))
/// - `window.key = iframe.contentWindow.config.apiKey` → Some(("window.key", "config.apiKey"))
/// - `iframe.src = '/x'` (no .contentWindow.) → None
fn extract_iframe_content_window_mirror(source : String) -> (String, String)? {
let marker = ".contentWindow."
let marker_idx = match find_substring(source, marker, 0) {
Some(idx) => idx
None => return None
}
let chars = source.to_array()
let len = chars.length()
// Extract chain: identifier after the marker, followed by zero or
// more `.identifier` or `[index]` / `["key"]` accesses.
let chain_start = marker_idx + marker.length()
let mut chain_end = read_iframe_mirror_identifier_end(chars, chain_start)
if chain_end == chain_start {
return None
}
let mut done = false
while !done && chain_end < len {
if chars[chain_end] == '.' {
let next_start = chain_end + 1
let next_end = read_iframe_mirror_identifier_end(chars, next_start)
if next_end == next_start {
return None
}
chain_end = next_end
} else if chars[chain_end] == '[' {
match read_iframe_mirror_bracket_end(chars, chain_end) {
Some(next_end) => chain_end = next_end
None => return None
}
} else {
done = true
}
}
let rhs_tail = skip_iframe_mirror_whitespace(chars, chain_end)
if rhs_tail < len && !is_iframe_mirror_rhs_terminator(chars[rhs_tail]) {
return None
}
let chain = source.unsafe_substring(start=chain_start, end=chain_end)
// Walk backward past the iframe var name.
let mut var_start = marker_idx
while var_start > 0 && is_iframe_mirror_identifier_char(chars[var_start - 1]) {
var_start -= 1
}
if var_start == marker_idx {
return None
}
// Walk backward past whitespace, expect `=`.
let mut eq_pos = var_start
while eq_pos > 0 && is_iframe_mirror_whitespace(chars[eq_pos - 1]) {
eq_pos -= 1
}
if eq_pos == 0 || chars[eq_pos - 1] != '=' {
return None
}
eq_pos -= 1
// Walk backward past whitespace before the `=`.
let mut lhs_end = eq_pos
while lhs_end > 0 && is_iframe_mirror_whitespace(chars[lhs_end - 1]) {
lhs_end -= 1
}
// LHS chars: identifiers + dots. Stop at any other char.
let mut lhs_start = lhs_end
while lhs_start > 0 {
let c = chars[lhs_start - 1]
if is_iframe_mirror_identifier_char(c) || c == '.' {
lhs_start -= 1
} else {
break
}
}
if lhs_start == lhs_end {
return None
}
let lhs = source.unsafe_substring(start=lhs_start, end=lhs_end)
// Sanity: LHS must contain at least one identifier char; not just dots.
let lhs_chars = lhs.to_array()
let mut has_ident = false
for i = 0; i < lhs_chars.length(); i = i + 1 {
if is_iframe_mirror_identifier_char(lhs_chars[i]) {
has_ident = true
break
}
}
if !has_ident {
return None
}
Some((lhs, chain))
}