1use std::collections::BTreeMap;
8use std::collections::BTreeSet;
9
10use std::sync::{Arc, Mutex};
11
12const MAX_NATIVE_FRAMES: usize = 256;
13const MAX_NATIVE_FRAME_ID_BYTES: usize = 512;
14
15#[derive(Debug, Default)]
16struct NativeFrameState {
17 browser_id: Option<i32>,
18 generation: u64,
19 exhausted: bool,
20 loading: bool,
21 observed_load_state: bool,
22 main_frame: Option<String>,
23 frames: BTreeMap<String, bool>,
24}
25
26#[derive(Clone, Debug)]
30pub struct FrameNavigationState {
31 state: Arc<Mutex<NativeFrameState>>,
32}
33
34#[derive(Clone)]
38pub struct NativeDocumentToken {
39 state: Arc<Mutex<NativeFrameState>>,
40 generation: u64,
41}
42
43impl PartialEq for NativeDocumentToken {
44 fn eq(&self, other: &Self) -> bool {
45 Arc::ptr_eq(&self.state, &other.state) && self.generation == other.generation
46 }
47}
48impl Eq for NativeDocumentToken {}
49
50impl std::fmt::Debug for NativeDocumentToken {
51 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
52 formatter
53 .debug_struct("NativeDocumentToken")
54 .finish_non_exhaustive()
55 }
56}
57
58impl FrameNavigationState {
59 pub(crate) fn new() -> Self {
60 Self {
61 state: Arc::default(),
62 }
63 }
64
65 pub fn is_same_browser(&self, other: &Self) -> bool {
68 Arc::ptr_eq(&self.state, &other.state)
69 }
70
71 pub(crate) fn has_browser_id(&self, browser_id: i32) -> bool {
72 self
73 .state
74 .lock()
75 .is_ok_and(|state| state.browser_id == Some(browser_id))
76 }
77
78 pub(crate) fn close(&self) {
79 if let Ok(mut state) = self.state.lock() {
80 state.exhausted = true;
81 }
82 }
83
84 pub(crate) fn observe_document(&self, browser: &cef::Browser) -> Option<NativeDocumentToken> {
85 use cef::{ImplBrowser, ImplFrame};
86 let generation = self.ready_generation()?;
87 if browser.is_valid() == 0 || browser.frame_count() > MAX_NATIVE_FRAMES {
88 return None;
89 }
90 let mut identifiers = cef::CefStringList::new();
91 browser.frame_identifiers(Some(&mut identifiers));
92 let attached = identifiers
97 .into_iter()
98 .filter(|id| {
99 browser
100 .frame_by_identifier(Some(&cef::CefString::from(id.as_str())))
101 .is_some_and(|frame| frame.is_valid() != 0)
102 })
103 .collect();
104 self
105 .admits_native_frames(
106 generation,
107 browser.identifier(),
108 browser.is_loading() != 0,
109 &attached,
110 )
111 .then(|| NativeDocumentToken {
112 state: Arc::clone(&self.state),
113 generation,
114 })
115 }
116
117 pub fn document(&self) -> Option<NativeDocumentToken> {
121 self
122 .ready_generation()
123 .map(|generation| NativeDocumentToken {
124 state: Arc::clone(&self.state),
125 generation,
126 })
127 }
128
129 fn ready_generation(&self) -> Option<u64> {
130 let state = self.state.lock().ok()?;
131 let ready = Self::ready(&state);
132 if !ready {
133 log::debug!(
134 "native document unavailable: exhausted={}, load_observed={}, loading={}, frames={}, attached={}, main_attached={}",
135 state.exhausted,
136 state.observed_load_state,
137 state.loading,
138 state.frames.len(),
139 state.frames.values().filter(|attached| **attached).count(),
140 state
141 .main_frame
142 .as_ref()
143 .is_some_and(|id| state.frames.get(id) == Some(&true)),
144 );
145 }
146 ready.then_some(state.generation)
147 }
148
149 fn ready(state: &NativeFrameState) -> bool {
150 !state.exhausted
151 && state.observed_load_state
152 && !state.loading
153 && !state.frames.is_empty()
154 && state.frames.len() <= MAX_NATIVE_FRAMES
158 && state.frames.values().all(|attached| *attached)
159 && state
160 .main_frame
161 .as_ref()
162 .is_some_and(|id| state.frames.get(id) == Some(&true))
163 }
164
165 pub(crate) fn on_frame_event(&self, event: &crate::FrameEvent) {
166 self.apply(
167 event.browser_id,
168 &event.frame_id,
169 event.is_main,
170 &event.kind,
171 );
172 }
173
174 fn apply(&self, browser_id: i32, frame_id: &str, is_main: bool, kind: &crate::FrameEventKind) {
175 use crate::FrameEventKind;
176
177 let Ok(mut state) = self.state.lock() else {
178 return;
179 };
180 if state.exhausted {
181 return;
182 }
183 if state
186 .browser_id
187 .is_some_and(|expected| expected != browser_id)
188 {
189 return;
190 }
191 state.browser_id = Some(browser_id);
195 let browser_wide = matches!(
196 kind,
197 FrameEventKind::MainFrameChanged
198 | FrameEventKind::LoadingStateChanged { .. }
199 | FrameEventKind::RendererTerminated
200 );
201 if frame_id.is_empty() && !browser_wide || frame_id.len() > MAX_NATIVE_FRAME_ID_BYTES {
202 log::warn!(
203 "native frame tracking exhausted: invalid identifier, empty={}, length={}, main={}, teardown={}",
204 frame_id.is_empty(),
205 frame_id.len(),
206 is_main,
207 matches!(kind, FrameEventKind::Detached | FrameEventKind::Destroyed),
208 );
209 state.exhausted = true;
210 return;
211 }
212 let Some(generation) = state.generation.checked_add(1) else {
213 state.exhausted = true;
214 return;
215 };
216 state.generation = generation;
217 match kind {
218 FrameEventKind::Created => {
219 state.frames.insert(frame_id.to_string(), false);
220 }
221 FrameEventKind::Attached => {
222 state.frames.insert(frame_id.to_string(), true);
223 }
224 FrameEventKind::Detached | FrameEventKind::Destroyed => {
225 state.frames.remove(frame_id);
226 if state.main_frame.as_deref() == Some(frame_id) {
227 state.main_frame = None;
228 }
229 }
230 FrameEventKind::MainFrameChanged => {
231 state.main_frame = is_main.then(|| frame_id.to_string());
232 }
233 FrameEventKind::NavigationStarted { .. } => {
234 state.loading = true;
235 }
236 FrameEventKind::LoadingStateChanged { is_loading } => {
237 state.observed_load_state = true;
238 state.loading = *is_loading;
239 }
240 FrameEventKind::DocumentCommitted { .. }
241 | FrameEventKind::NavigationFailed { .. }
242 | FrameEventKind::AddressChanged { .. } => {}
243 FrameEventKind::RendererTerminated => {
244 state.frames.clear();
245 state.main_frame = None;
246 state.loading = true;
247 state.observed_load_state = false;
248 }
249 }
250 }
251
252 pub(crate) fn admits_native_frames(
255 &self,
256 expected: u64,
257 browser_id: i32,
258 is_loading: bool,
259 frame_ids: &BTreeSet<String>,
260 ) -> bool {
261 let Ok(state) = self.state.lock() else {
262 return false;
263 };
264 Self::ready(&state)
265 && state.generation == expected
266 && state.browser_id == Some(browser_id)
267 && !is_loading
268 && state.frames.keys().eq(frame_ids.iter())
269 }
270}
271
272#[cfg(test)]
273mod tests {
274 use crate::FrameEventKind as Event;
275
276 use super::*;
277
278 fn apply(barrier: &FrameNavigationState, frame: &str, kind: Event) {
279 barrier.apply(1, frame, frame == "main", &kind);
280 }
281
282 fn ready_main() -> FrameNavigationState {
283 let barrier = FrameNavigationState::new();
284 apply(&barrier, "main", Event::Created);
285 apply(&barrier, "main", Event::MainFrameChanged);
286 apply(&barrier, "main", Event::Attached);
287 apply(
288 &barrier,
289 "main",
290 Event::LoadingStateChanged { is_loading: false },
291 );
292 barrier
293 }
294
295 fn admits(barrier: &FrameNavigationState, generation: u64, frames: &[&str]) -> bool {
296 barrier.admits_native_frames(
297 generation,
298 1,
299 false,
300 &frames.iter().map(|id| id.to_string()).collect(),
301 )
302 }
303
304 #[test]
305 fn child_navigation_replacement_and_detach_revoke_prior_documents() {
306 let barrier = ready_main();
307 let main_generation = barrier.ready_generation().unwrap();
308 assert!(admits(&barrier, main_generation, &["main"]));
309 apply(&barrier, "child-a", Event::Created);
310 assert!(barrier.ready_generation().is_none());
311 apply(&barrier, "child-a", Event::Attached);
312 let child_generation = barrier.ready_generation().unwrap();
313 assert!(!admits(&barrier, main_generation, &["main", "child-a"]));
314 assert!(admits(&barrier, child_generation, &["main", "child-a"]));
315 let url = url::Url::parse("https://example.test/frame").unwrap();
316 apply(
317 &barrier,
318 "child-a",
319 Event::NavigationStarted { url: url.clone() },
320 );
321 assert!(barrier.ready_generation().is_none());
322 apply(&barrier, "child-a", Event::DocumentCommitted { url });
323 assert!(barrier.ready_generation().is_none());
324 apply(
325 &barrier,
326 "main",
327 Event::LoadingStateChanged { is_loading: false },
328 );
329 assert!(!admits(&barrier, child_generation, &["main", "child-a"]));
330 let loaded_generation = barrier.ready_generation().unwrap();
331 assert!(admits(&barrier, loaded_generation, &["main", "child-a"]));
332 assert!(!admits(&barrier, loaded_generation, &["main", "child-b"]));
334 apply(&barrier, "child-a", Event::Detached);
335 apply(&barrier, "child-b", Event::Created);
336 apply(&barrier, "child-b", Event::Attached);
337 assert!(!admits(&barrier, loaded_generation, &["main", "child-b"]));
338 let replacement_generation = barrier.ready_generation().unwrap();
339 assert!(admits(
340 &barrier,
341 replacement_generation,
342 &["main", "child-b"]
343 ));
344 apply(&barrier, "child-b", Event::Detached);
345 assert!(!admits(&barrier, replacement_generation, &["main"]));
346 assert!(admits(
347 &barrier,
348 barrier.ready_generation().unwrap(),
349 &["main"]
350 ));
351 }
352
353 #[test]
354 fn pending_cancelled_navigation_waits_for_native_loading_completion() {
355 let barrier = ready_main();
356 let url = url::Url::parse("https://example.test/repeated").unwrap();
357 apply(
358 &barrier,
359 "main",
360 Event::NavigationStarted { url: url.clone() },
361 );
362 apply(
363 &barrier,
364 "main",
365 Event::NavigationStarted { url: url.clone() },
366 );
367 apply(&barrier, "main", Event::NavigationFailed { url });
368 assert!(barrier.ready_generation().is_none());
369 apply(
370 &barrier,
371 "main",
372 Event::LoadingStateChanged { is_loading: false },
373 );
374 assert!(barrier.ready_generation().is_some());
375 }
376
377 #[test]
378 fn foreign_browser_unobserved_frames_and_exhaustion_fail_closed() {
379 let barrier = ready_main();
380 let generation = barrier.ready_generation().unwrap();
381 barrier.apply(2, "popup", true, &Event::Created);
382 assert_eq!(barrier.ready_generation(), Some(generation));
383 assert!(!barrier.admits_native_frames(
384 generation,
385 2,
386 false,
387 &BTreeSet::from(["main".to_string()])
388 ));
389 assert!(!admits(&barrier, generation, &["main", "unobserved"]));
390 barrier.state.lock().unwrap().generation = u64::MAX;
391 apply(&barrier, "main", Event::Attached);
392 assert!(barrier.ready_generation().is_none());
393 apply(
394 &barrier,
395 "main",
396 Event::LoadingStateChanged { is_loading: false },
397 );
398 assert!(barrier.ready_generation().is_none());
399 }
400 #[test]
401 fn missing_main_frame_and_renderer_termination_revoke_prior_generations() {
402 let barrier = ready_main();
403 let generation = barrier.ready_generation().unwrap();
404 barrier.apply(1, "", false, &Event::MainFrameChanged);
405 assert!(barrier.ready_generation().is_none());
406 apply(&barrier, "main", Event::MainFrameChanged);
407 assert_ne!(barrier.ready_generation().unwrap(), generation);
408 barrier.apply(1, "", false, &Event::RendererTerminated);
409 assert!(barrier.ready_generation().is_none());
410 barrier.apply(
411 1,
412 "",
413 false,
414 &Event::LoadingStateChanged { is_loading: false },
415 );
416 assert!(barrier.ready_generation().is_none());
417 apply(&barrier, "replacement", Event::Created);
418 apply(&barrier, "replacement", Event::Attached);
419 barrier.apply(1, "replacement", true, &Event::MainFrameChanged);
420 assert_ne!(barrier.ready_generation().unwrap(), generation);
421 }
422 #[test]
423 fn document_tokens_cannot_cross_native_lifetimes_or_navigation() {
424 let first = ready_main();
425 let second = ready_main();
426 assert_eq!(first.ready_generation(), second.ready_generation());
427 assert!(first.is_same_browser(&first.clone()));
428 assert!(!first.is_same_browser(&second));
429 let before = first.document().unwrap();
430 assert_eq!(before, first.clone().document().unwrap());
431 assert_ne!(before, second.document().unwrap());
432 apply(&first, "child", Event::Created);
433 assert!(first.document().is_none());
434 apply(&first, "child", Event::Attached);
435 assert_ne!(before, first.document().unwrap());
436 }
437 #[test]
438 fn a_rejected_first_event_still_records_the_native_browser_identity() {
439 let url = url::Url::parse("https://example.test/frameless").unwrap();
442 let barrier = FrameNavigationState::new();
443 barrier.apply(1, "", false, &Event::AddressChanged { url });
444 assert!(barrier.document().is_none());
446 assert!(barrier.has_browser_id(1));
448 assert!(!barrier.has_browser_id(2));
449 }
450 #[test]
451 fn a_transient_frame_count_overflow_recovers_once_frames_drain() {
452 let barrier = ready_main();
453 for index in 0..MAX_NATIVE_FRAMES {
454 apply(&barrier, &format!("child-{index}"), Event::Created);
455 apply(&barrier, &format!("child-{index}"), Event::Attached);
456 }
457 assert!(barrier.ready_generation().is_none());
458 apply(&barrier, "child-0", Event::Detached);
459 assert!(barrier.ready_generation().is_some());
460 assert!(barrier.has_browser_id(1));
461 }
462}