1#![cfg_attr(
2 not(any(
3 target_os = "android",
4 target_os = "ios",
5 target_os = "macos",
6 target_os = "windows",
7 all(target_os = "linux", target_env = "ohos")
8 )),
9 allow(dead_code)
10)]
11
12use serde::{Deserialize, Serialize};
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::future::Future;
15use std::panic::{AssertUnwindSafe, catch_unwind};
16use std::pin::Pin;
17use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
18use std::sync::mpsc::{Sender, SyncSender, channel, sync_channel};
19use std::sync::{Arc, Mutex, OnceLock, RwLock};
20use std::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
21use tokio::sync::watch;
22
23#[cfg(target_os = "android")]
24use crate::android::WebViewInner;
25
26#[cfg(any(target_os = "ios", target_os = "macos"))]
27use crate::apple::WebViewInner;
28
29#[cfg(all(target_os = "linux", target_env = "ohos"))]
30use crate::harmony::WebViewInner;
31
32#[cfg(target_os = "windows")]
33use crate::windows::WebViewInner;
34
35#[cfg(not(any(
36 target_os = "android",
37 target_os = "ios",
38 target_os = "macos",
39 target_os = "windows",
40 all(target_os = "linux", target_env = "ohos")
41)))]
42pub(crate) struct WebViewInner {
43 webtag: WebTag,
44}
45
46use crate::traits::{
47 AsyncSchemeHandler, ClickOptions, ContextualSchemeRequest, DownloadHandler, DownloadRequest,
48 FileChooserRequest, FileChooserResponse, FillOptions, NativeWebViewId, NavigationHandler,
49 NavigationPolicy, NavigationRequest, NewWindowHandler, NewWindowPolicy, PressOptions,
50 SchemeOutcome, SchemeRequestFrame, ScrollOptions, TypeOptions, UserAgentOverride,
51 WebMessageContext, WebMessageFrame, WebMessageSource, WebMessageTransport,
52 WebViewInputController,
53};
54use crate::{
55 ClearSiteDataOptions, ClearSiteDataResult, IncomingWebMessage, LoadDataRequest,
56 NetworkCaptureSnapshot, TrustedLoadIntent, WebResourceResponse, WebViewController,
57 WebViewCookie, WebViewCookieSetRequest, WebViewDelegate, WebViewError, WebViewInputError,
58 WebViewScriptError,
59};
60use async_trait::async_trait;
61
62const APPLE_INTERNAL_SCHEME: &str = "lx-apple";
63pub(crate) const MAX_WEB_MESSAGE_BYTES: usize = 64 * 1024;
64const MAX_PENDING_WEB_MESSAGES: usize = 1024;
65const MAX_PENDING_WEB_MESSAGE_BYTES: usize = 1024 * 1024;
66const WEB_MESSAGE_WORKER_COUNT: usize = 4;
67
68static NEXT_NATIVE_WEBVIEW_ID: AtomicU64 = AtomicU64::new(1);
69
70fn next_native_webview_id() -> NativeWebViewId {
71 let raw = NEXT_NATIVE_WEBVIEW_ID
75 .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |current| {
76 current.checked_add(1)
77 })
78 .expect("native WebView identity space exhausted");
79 NativeWebViewId::new(raw)
80}
81
82#[derive(Default)]
83struct WebMessageIngress {
84 state: Mutex<WebMessageIngressState>,
85}
86
87#[derive(Default)]
88struct WebMessageIngressState {
89 queue: VecDeque<IncomingWebMessage>,
90 queued_bytes: usize,
91 scheduled: bool,
92 closed: bool,
93 in_flight: bool,
94 rejection_counts: [u64; WebMessageRejectReason::COUNT],
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
98enum WebMessageEnqueue {
99 Queued,
100 Schedule,
101 Rejected {
102 reason: WebMessageRejectReason,
103 count: u64,
104 },
105}
106
107#[derive(Clone, Copy, Debug, PartialEq, Eq)]
108enum WebMessageRejectReason {
109 MessageTooLarge,
110 QueueCountLimit,
111 QueueByteLimit,
112 Closed,
113}
114
115impl WebMessageRejectReason {
116 const COUNT: usize = 4;
117
118 const fn index(self) -> usize {
119 match self {
120 Self::MessageTooLarge => 0,
121 Self::QueueCountLimit => 1,
122 Self::QueueByteLimit => 2,
123 Self::Closed => 3,
124 }
125 }
126
127 const fn as_str(self) -> &'static str {
128 match self {
129 Self::MessageTooLarge => "message_too_large",
130 Self::QueueCountLimit => "queue_count_limit",
131 Self::QueueByteLimit => "queue_byte_limit",
132 Self::Closed => "ingress_closed",
133 }
134 }
135}
136
137fn should_sample_rejection(count: u64) -> bool {
138 count == 1 || count.is_power_of_two()
139}
140
141pub(crate) const fn web_message_bytes_within_limit(bytes: usize) -> bool {
142 bytes <= MAX_WEB_MESSAGE_BYTES
143}
144
145#[cfg(any(test, target_os = "windows"))]
150pub(crate) const fn web_message_utf16_units_within_limit(units: usize) -> bool {
151 units <= MAX_WEB_MESSAGE_BYTES
152}
153
154impl WebMessageIngress {
155 fn reject_locked(
156 state: &mut WebMessageIngressState,
157 reason: WebMessageRejectReason,
158 ) -> WebMessageEnqueue {
159 let count = &mut state.rejection_counts[reason.index()];
160 *count = count.saturating_add(1);
161 WebMessageEnqueue::Rejected {
162 reason,
163 count: *count,
164 }
165 }
166
167 #[cfg(any(
168 test,
169 target_os = "android",
170 target_os = "ios",
171 target_os = "macos",
172 target_os = "windows",
173 all(target_os = "linux", target_env = "ohos")
174 ))]
175 fn reject(&self, reason: WebMessageRejectReason) -> WebMessageEnqueue {
176 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
177 Self::reject_locked(&mut state, reason)
178 }
179
180 fn enqueue(&self, message: IncomingWebMessage) -> WebMessageEnqueue {
184 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
185 let message_bytes = message.body().len();
186 if !web_message_bytes_within_limit(message_bytes) {
187 return Self::reject_locked(&mut state, WebMessageRejectReason::MessageTooLarge);
188 }
189 if state.closed {
190 return Self::reject_locked(&mut state, WebMessageRejectReason::Closed);
191 }
192 if state.queue.len() >= MAX_PENDING_WEB_MESSAGES {
193 return Self::reject_locked(&mut state, WebMessageRejectReason::QueueCountLimit);
194 }
195 let Some(next_bytes) = state.queued_bytes.checked_add(message_bytes) else {
196 return Self::reject_locked(&mut state, WebMessageRejectReason::QueueByteLimit);
197 };
198 if next_bytes > MAX_PENDING_WEB_MESSAGE_BYTES {
199 return Self::reject_locked(&mut state, WebMessageRejectReason::QueueByteLimit);
200 }
201 state.queued_bytes = next_bytes;
202 state.queue.push_back(message);
203 if state.scheduled {
204 WebMessageEnqueue::Queued
205 } else {
206 state.scheduled = true;
207 WebMessageEnqueue::Schedule
208 }
209 }
210
211 fn close(&self) {
218 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
219 state.closed = true;
220 state.queue.clear();
221 state.queued_bytes = 0;
222 if !state.in_flight {
223 state.scheduled = false;
224 }
225 }
226
227 fn begin_delivery(&self) -> Option<IncomingWebMessage> {
231 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
232 if state.closed {
233 state.scheduled = false;
234 return None;
235 }
236 match state.queue.pop_front() {
237 Some(message) => {
238 state.queued_bytes = state
239 .queued_bytes
240 .checked_sub(message.body().len())
241 .expect("queued WebView message byte ledger underflow");
242 state.in_flight = true;
243 Some(message)
244 }
245 None => {
246 state.scheduled = false;
247 None
248 }
249 }
250 }
251
252 fn finish_delivery(&self) {
253 let mut state = self.state.lock().unwrap_or_else(|error| error.into_inner());
254 debug_assert!(state.in_flight);
255 state.in_flight = false;
256 }
257
258 #[cfg(test)]
259 fn queued_bytes(&self) -> usize {
260 self.state
261 .lock()
262 .unwrap_or_else(|error| error.into_inner())
263 .queued_bytes
264 }
265
266 #[cfg(test)]
267 fn rejection_count(&self, reason: WebMessageRejectReason) -> u64 {
268 self.state
269 .lock()
270 .unwrap_or_else(|error| error.into_inner())
271 .rejection_counts[reason.index()]
272 }
273
274 fn drain<F>(&self, mut deliver: F)
275 where
276 F: FnMut(IncomingWebMessage),
277 {
278 while let Some(message) = self.begin_delivery() {
279 let result = catch_unwind(AssertUnwindSafe(|| deliver(message)));
280 self.finish_delivery();
281 if result.is_err() {
282 log::error!("WebView message delegate panicked; continuing ingress drain");
283 }
284 }
285 }
286}
287
288struct WebMessageJob {
289 ingress: Arc<WebMessageIngress>,
290 webview: std::sync::Weak<WebView>,
291}
292
293struct WebMessageExecutor {
299 senders: Vec<Sender<WebMessageJob>>,
300 next_worker: AtomicUsize,
301}
302
303impl WebMessageExecutor {
304 fn global() -> &'static Self {
305 static EXECUTOR: OnceLock<WebMessageExecutor> = OnceLock::new();
306 EXECUTOR.get_or_init(Self::new)
307 }
308
309 fn new() -> Self {
310 let mut senders = Vec::with_capacity(WEB_MESSAGE_WORKER_COUNT);
311 for worker_index in 0..WEB_MESSAGE_WORKER_COUNT {
312 let (sender, receiver) = channel::<WebMessageJob>();
313 std::thread::Builder::new()
314 .name(format!("lingxia-web-message-worker-{worker_index}"))
315 .spawn(move || {
316 while let Ok(job) = receiver.recv() {
317 let ingress = Arc::clone(&job.ingress);
318 job.ingress.drain(|message| {
319 let Some(webview) = job.webview.upgrade() else {
320 ingress.close();
321 return;
322 };
323 let document = message.context().document();
324 let mut message = Some(message);
325 let deliver = &mut || {
326 if let Some(delegate) = webview.get_delegate() {
327 delegate.handle_post_message(
328 message
329 .take()
330 .expect("message delivery closure runs at most once"),
331 );
332 } else {
333 log::debug!(
334 "Dropping WebView message before delegate installation ({})",
335 webview.webtag()
336 );
337 }
338 };
339 match document {
340 crate::DocumentBinding::Bound(generation) => {
341 if crate::events::normalizer::document_binding_is_current(
349 webview.native_view_id(),
350 generation,
351 ) {
352 deliver();
353 } else {
354 log::debug!(
355 "Dropping WebView message after its document generation was revoked ({})",
356 webview.webtag()
357 );
358 }
359 }
360 crate::DocumentBinding::Unbound => deliver(),
361 }
362 });
363 }
364 })
365 .expect("failed to start fixed WebView message worker");
366 senders.push(sender);
367 }
368 Self {
369 senders,
370 next_worker: AtomicUsize::new(0),
371 }
372 }
373
374 fn schedule(&self, job: WebMessageJob) -> Result<(), WebMessageJob> {
375 let worker = self.next_worker.fetch_add(1, Ordering::Relaxed) % self.senders.len();
376 self.senders[worker].send(job).map_err(|error| error.0)
377 }
378}
379
380#[cfg(not(any(
381 target_os = "android",
382 target_os = "ios",
383 target_os = "macos",
384 target_os = "windows",
385 all(target_os = "linux", target_env = "ohos")
386)))]
387fn unsupported_webview_error(action: &str) -> WebViewError {
388 WebViewError::Unsupported(action.to_string())
389}
390
391#[cfg(not(any(
392 target_os = "android",
393 target_os = "ios",
394 target_os = "macos",
395 target_os = "windows",
396 all(target_os = "linux", target_env = "ohos")
397)))]
398impl WebViewInner {
399 pub(crate) fn create(
400 appid: &str,
401 path: &str,
402 session_id: Option<u64>,
403 _effective_options: EffectiveWebViewCreateOptions,
404 sender: WebViewCreateSender,
405 ) {
406 let _webtag = WebTag::new(appid, path, session_id);
407 sender.fail(
408 WebViewCreateStage::Requested,
409 unsupported_webview_error("webview creation"),
410 );
411 }
412}
413
414#[cfg(not(any(
415 target_os = "android",
416 target_os = "ios",
417 target_os = "macos",
418 target_os = "windows",
419 all(target_os = "linux", target_env = "ohos")
420)))]
421#[async_trait]
422impl WebViewController for WebViewInner {
423 fn load_url(&self, _url: &str) -> Result<(), WebViewError> {
424 Err(unsupported_webview_error("load_url"))
425 }
426
427 fn load_data(&self, _request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
428 Err(unsupported_webview_error("load_data"))
429 }
430
431 fn exec_js(&self, _js: &str) -> Result<(), WebViewError> {
432 Err(unsupported_webview_error("exec_js"))
433 }
434
435 async fn eval_js(&self, _js: &str) -> Result<serde_json::Value, WebViewScriptError> {
436 Err(WebViewScriptError::Unsupported(
437 "JavaScript evaluation is not supported on this platform",
438 ))
439 }
440
441 fn post_message(&self, _message: &str) -> Result<(), WebViewError> {
442 Err(unsupported_webview_error("post_message"))
443 }
444
445 fn clear_browsing_data(&self) -> Result<(), WebViewError> {
446 Err(unsupported_webview_error("clear_browsing_data"))
447 }
448
449 fn set_user_agent_override(&self, _user_agent: UserAgentOverride) -> Result<(), WebViewError> {
450 Err(unsupported_webview_error("set_user_agent_override"))
451 }
452}
453
454fn lock_or_recover<'a, T>(mutex: &'a Mutex<T>, name: &str) -> std::sync::MutexGuard<'a, T> {
455 match mutex.lock() {
456 Ok(guard) => guard,
457 Err(poisoned) => {
458 log::error!("Mutex poisoned at {}, recovering inner value", name);
459 poisoned.into_inner()
460 }
461 }
462}
463
464fn scheme_waker_from_sender(sender: SyncSender<()>) -> Waker {
465 unsafe { Waker::from_raw(scheme_raw_waker(Arc::new(sender))) }
467}
468
469fn scheme_raw_waker(sender: Arc<SyncSender<()>>) -> RawWaker {
470 RawWaker::new(Arc::into_raw(sender) as *const (), &SCHEME_WAKER_VTABLE)
471}
472
473unsafe fn scheme_waker_clone(data: *const ()) -> RawWaker {
474 let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
476 let cloned = Arc::clone(&arc);
477 let _ = Arc::into_raw(arc);
478 scheme_raw_waker(cloned)
479}
480
481unsafe fn scheme_waker_wake(data: *const ()) {
482 let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
484 let _ = arc.try_send(());
485}
486
487unsafe fn scheme_waker_wake_by_ref(data: *const ()) {
488 let arc = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
490 let _ = arc.try_send(());
491 let _ = Arc::into_raw(arc);
492}
493
494unsafe fn scheme_waker_drop(data: *const ()) {
495 let _ = unsafe { Arc::<SyncSender<()>>::from_raw(data as *const SyncSender<()>) };
497}
498
499static SCHEME_WAKER_VTABLE: RawWakerVTable = RawWakerVTable::new(
500 scheme_waker_clone,
501 scheme_waker_wake,
502 scheme_waker_wake_by_ref,
503 scheme_waker_drop,
504);
505
506fn block_on_scheme_future<F>(future: F) -> F::Output
507where
508 F: Future,
509{
510 let (tx, rx) = sync_channel::<()>(1);
511 let waker = scheme_waker_from_sender(tx);
512 let mut context = Context::from_waker(&waker);
513 let mut future = Box::pin(future);
514
515 loop {
516 match Pin::as_mut(&mut future).poll(&mut context) {
517 Poll::Ready(value) => return value,
518 Poll::Pending => {
519 if rx.recv().is_err() {
520 std::thread::yield_now();
521 }
522 }
523 }
524 }
525}
526
527#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
529#[serde(rename_all = "snake_case")]
530pub(crate) enum SecurityProfile {
531 StrictDefault,
532 BrowserRelaxed,
533}
534
535#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub(crate) enum PlatformConsoleBackend {
540 #[cfg(any(target_os = "ios", target_os = "macos", test))]
541 Apple,
542 #[cfg(any(target_os = "android", test))]
543 Android,
544 #[cfg(any(all(target_os = "linux", target_env = "ohos"), test))]
545 Harmony,
546 #[cfg(any(target_os = "windows", test))]
547 Windows,
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
551pub(crate) enum PlatformConsoleDelivery {
552 DirectDelegate,
553 RequiredV3Envelope,
554}
555
556pub(crate) const fn platform_console_delivery(
557 profile: SecurityProfile,
558 _backend: PlatformConsoleBackend,
559) -> PlatformConsoleDelivery {
560 match profile {
561 SecurityProfile::StrictDefault => PlatformConsoleDelivery::DirectDelegate,
562 SecurityProfile::BrowserRelaxed => PlatformConsoleDelivery::RequiredV3Envelope,
563 }
564}
565
566#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
571#[serde(rename_all = "snake_case")]
572pub enum WebViewDataMode {
573 #[default]
576 ProfileDefault,
577 Ephemeral,
580}
581
582pub(crate) type FileChooserFuture =
583 Pin<Box<dyn Future<Output = FileChooserResponse> + Send + 'static>>;
584pub(crate) type FileChooserHandler =
585 Box<dyn Fn(FileChooserRequest) -> FileChooserFuture + Send + Sync>;
586
587pub(crate) struct WebViewCreateOptions {
589 pub(crate) profile: SecurityProfile,
590 pub(crate) data_mode: WebViewDataMode,
591 pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
592 pub(crate) navigation_handler: Option<NavigationHandler>,
593 pub(crate) new_window_handler: Option<NewWindowHandler>,
594 pub(crate) download_handler: Option<DownloadHandler>,
595 pub(crate) file_chooser_handler: Option<FileChooserHandler>,
596 pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
597 pub(crate) surface_owned: bool,
600}
601
602impl std::fmt::Debug for WebViewCreateOptions {
603 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604 f.debug_struct("WebViewCreateOptions")
605 .field("profile", &self.profile)
606 .field("data_mode", &self.data_mode)
607 .field(
608 "scheme_handlers",
609 &self.scheme_handlers.keys().collect::<Vec<_>>(),
610 )
611 .field("has_navigation_handler", &self.navigation_handler.is_some())
612 .field("has_new_window_handler", &self.new_window_handler.is_some())
613 .field("has_download_handler", &self.download_handler.is_some())
614 .field(
615 "has_file_chooser_handler",
616 &self.file_chooser_handler.is_some(),
617 )
618 .field("has_delegate", &self.delegate.is_some())
619 .finish()
620 }
621}
622
623#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
625pub struct ProxyConfig {
626 pub host: String,
627 pub port: u16,
628 #[serde(default)]
629 pub bypass: Vec<String>,
630}
631
632impl ProxyConfig {
633 pub fn new(host: impl Into<String>, port: u16) -> Result<Self, WebViewError> {
634 let cfg = Self {
635 host: host.into(),
636 port,
637 bypass: Vec::new(),
638 };
639 cfg.validate()
640 }
641
642 pub fn with_bypass<I, S>(mut self, bypass: I) -> Self
643 where
644 I: IntoIterator<Item = S>,
645 S: Into<String>,
646 {
647 self.bypass = bypass.into_iter().map(Into::into).collect();
648 self
649 }
650
651 fn validate(self) -> Result<Self, WebViewError> {
652 let host = self.host.trim().to_string();
653 if host.is_empty() {
654 return Err(WebViewError::InvalidCreateOptions(
655 "proxy host cannot be empty".to_string(),
656 ));
657 }
658 if host.contains(char::is_whitespace) {
659 return Err(WebViewError::InvalidCreateOptions(
660 "proxy host cannot contain whitespace".to_string(),
661 ));
662 }
663 if self.port == 0 {
664 return Err(WebViewError::InvalidCreateOptions(
665 "proxy port must be greater than 0".to_string(),
666 ));
667 }
668
669 let mut seen = HashSet::new();
670 let mut bypass = Vec::new();
671 for raw in self.bypass {
672 let rule = raw.trim();
673 if rule.is_empty() {
674 continue;
675 }
676 let key = rule.to_ascii_lowercase();
677 if seen.insert(key) {
678 bypass.push(rule.to_string());
679 }
680 }
681
682 Ok(Self {
683 host,
684 bypass,
685 ..self
686 })
687 }
688}
689
690#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
691#[serde(rename_all = "snake_case")]
692pub enum ProxyApplyStatus {
693 Applied,
694 Cleared,
695 Unsupported,
696}
697
698#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
699#[serde(rename_all = "snake_case")]
700pub enum ProxyActivation {
701 EffectiveNow,
702 NewWebViewsOnly,
703 EngineRecreateRequired,
704 NotApplied,
705}
706
707#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
708pub struct ProxyApplyReport {
709 pub status: ProxyApplyStatus,
710 pub activation: ProxyActivation,
711 #[serde(default, skip_serializing_if = "Option::is_none")]
712 pub detail: Option<String>,
713}
714
715impl ProxyApplyReport {
716 pub fn applied(activation: ProxyActivation) -> Self {
717 Self {
718 status: ProxyApplyStatus::Applied,
719 activation,
720 detail: None,
721 }
722 }
723
724 pub fn cleared(activation: ProxyActivation) -> Self {
725 Self {
726 status: ProxyApplyStatus::Cleared,
727 activation,
728 detail: None,
729 }
730 }
731
732 pub fn unsupported(detail: impl Into<String>) -> Self {
733 Self {
734 status: ProxyApplyStatus::Unsupported,
735 activation: ProxyActivation::NotApplied,
736 detail: Some(detail.into()),
737 }
738 }
739}
740
741#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
743pub(crate) struct EffectiveWebViewCreateOptions {
744 pub(crate) profile: SecurityProfile,
745 #[serde(default)]
747 pub(crate) data_mode: WebViewDataMode,
748 #[serde(default)]
750 pub(crate) registered_schemes: Vec<String>,
751 #[serde(default)]
752 pub(crate) has_navigation_handler: bool,
753 #[serde(default)]
754 pub(crate) has_new_window_handler: bool,
755 #[serde(default)]
756 pub(crate) has_download_handler: bool,
757 #[serde(default)]
758 pub(crate) has_file_chooser_handler: bool,
759 #[serde(default)]
760 pub(crate) has_delegate: bool,
761 #[serde(default)]
762 pub(crate) surface_owned: bool,
763}
764
765impl Default for WebViewCreateOptions {
766 fn default() -> Self {
767 Self::strict()
768 }
769}
770
771impl WebViewCreateOptions {
772 fn strict() -> Self {
773 Self {
774 profile: SecurityProfile::StrictDefault,
775 data_mode: WebViewDataMode::ProfileDefault,
776 scheme_handlers: HashMap::new(),
777 navigation_handler: None,
778 new_window_handler: None,
779 download_handler: None,
780 file_chooser_handler: None,
781 delegate: None,
782 surface_owned: false,
783 }
784 }
785
786 fn browser() -> Self {
787 Self {
788 profile: SecurityProfile::BrowserRelaxed,
789 data_mode: WebViewDataMode::ProfileDefault,
790 scheme_handlers: HashMap::new(),
791 navigation_handler: None,
792 new_window_handler: None,
793 download_handler: None,
794 file_chooser_handler: None,
795 delegate: None,
796 surface_owned: false,
797 }
798 }
799
800 fn on_contextual_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
810 where
811 F: Fn(ContextualSchemeRequest) -> Fut + Send + Sync + 'static,
812 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
813 {
814 let normalized = scheme.trim().to_ascii_lowercase();
815 if !normalized.is_empty() {
816 self.scheme_handlers.insert(
817 normalized,
818 Arc::new(move |req| {
819 let fut = handler(req);
820 Box::pin(fut)
821 }),
822 );
823 }
824 self
825 }
826
827 fn on_scheme<F, Fut>(self, scheme: &str, handler: F) -> Self
828 where
829 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
830 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
831 {
832 self.on_contextual_scheme(scheme, move |request| handler(request.into_request()))
833 }
834
835 fn on_navigation<F>(mut self, handler: F) -> Self
838 where
839 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
840 {
841 self.navigation_handler = Some(Box::new(handler));
842 self
843 }
844
845 fn on_new_window<F>(mut self, handler: F) -> Self
848 where
849 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
850 {
851 self.new_window_handler = Some(Box::new(handler));
852 self
853 }
854
855 fn on_download<F>(mut self, handler: F) -> Self
865 where
866 F: Fn(DownloadRequest) + Send + Sync + 'static,
867 {
868 self.download_handler = Some(Box::new(handler));
869 self
870 }
871
872 fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
873 where
874 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
875 Fut: Future<Output = FileChooserResponse> + Send + 'static,
876 {
877 self.file_chooser_handler = Some(Box::new(move |request| Box::pin(handler(request))));
878 self
879 }
880
881 fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
882 self.delegate = Some(delegate);
883 self
884 }
885
886 fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
887 self.data_mode = data_mode;
888 self
889 }
890
891 fn surface_owned(mut self, surface_owned: bool) -> Self {
892 self.surface_owned = surface_owned;
893 self
894 }
895
896 pub(crate) fn normalize(
897 self,
898 ) -> Result<(EffectiveWebViewCreateOptions, PendingCallbacks), WebViewError> {
899 if self.profile != SecurityProfile::BrowserRelaxed && self.download_handler.is_some() {
900 return Err(WebViewError::InvalidCreateOptions(
901 "download callback is only supported in browser profile; use WebViewBuilder::browser(webtag).on_download(...).create()".to_string(),
902 ));
903 }
904 if self.scheme_handlers.contains_key(APPLE_INTERNAL_SCHEME) {
905 return Err(WebViewError::InvalidCreateOptions(format!(
906 "scheme '{APPLE_INTERNAL_SCHEME}' is reserved for LingXia Apple bridge transport"
907 )));
908 }
909 let mut registered_schemes: Vec<String> = self.scheme_handlers.keys().cloned().collect();
910 registered_schemes.sort_unstable();
911 registered_schemes.dedup();
912 let effective = EffectiveWebViewCreateOptions {
913 profile: self.profile,
914 data_mode: self.data_mode,
915 registered_schemes,
916 has_navigation_handler: self.navigation_handler.is_some(),
917 has_new_window_handler: self.new_window_handler.is_some(),
918 has_download_handler: self.download_handler.is_some(),
919 has_file_chooser_handler: self.file_chooser_handler.is_some(),
920 has_delegate: self.delegate.is_some(),
921 surface_owned: self.surface_owned,
922 };
923 let pending = PendingCallbacks {
924 scheme_handlers: self.scheme_handlers,
925 navigation_handler: self.navigation_handler,
926 new_window_handler: self.new_window_handler,
927 download_handler: self.download_handler,
928 file_chooser_handler: self.file_chooser_handler,
929 delegate: self.delegate,
930 };
931 Ok((effective, pending))
932 }
933}
934
935pub struct WebViewBuilder;
943
944#[must_use = "call .create() to start WebView creation"]
945pub struct StrictWebViewBuilder {
946 webtag: WebTag,
947 options: WebViewCreateOptions,
948}
949
950#[must_use = "call .create() to start WebView creation"]
951pub struct BrowserWebViewBuilder {
952 webtag: WebTag,
953 options: WebViewCreateOptions,
954}
955
956impl WebViewBuilder {
957 #[must_use = "call .create() to start WebView creation"]
959 pub fn strict(webtag: WebTag) -> StrictWebViewBuilder {
960 StrictWebViewBuilder {
961 webtag,
962 options: WebViewCreateOptions::strict(),
963 }
964 }
965
966 #[must_use = "call .create() to start WebView creation"]
968 pub fn browser(webtag: WebTag) -> BrowserWebViewBuilder {
969 BrowserWebViewBuilder {
970 webtag,
971 options: WebViewCreateOptions::browser(),
972 }
973 }
974}
975
976impl StrictWebViewBuilder {
977 pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
981 self.options = self.options.delegate(delegate);
982 self
983 }
984
985 pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
987 self.options = self.options.data_mode(data_mode);
988 self
989 }
990
991 pub fn surface_owned(mut self, surface_owned: bool) -> Self {
994 self.options = self.options.surface_owned(surface_owned);
995 self
996 }
997
998 pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
999 where
1000 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
1001 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
1002 {
1003 self.options = self.options.on_scheme(scheme, handler);
1004 self
1005 }
1006
1007 pub fn on_contextual_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
1010 where
1011 F: Fn(ContextualSchemeRequest) -> Fut + Send + Sync + 'static,
1012 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
1013 {
1014 self.options = self.options.on_contextual_scheme(scheme, handler);
1015 self
1016 }
1017
1018 pub fn on_navigation<F>(mut self, handler: F) -> Self
1019 where
1020 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
1021 {
1022 self.options = self.options.on_navigation(handler);
1023 self
1024 }
1025
1026 pub fn on_new_window<F>(mut self, handler: F) -> Self
1027 where
1028 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
1029 {
1030 self.options = self.options.on_new_window(handler);
1031 self
1032 }
1033
1034 pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
1035 where
1036 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
1037 Fut: Future<Output = FileChooserResponse> + Send + 'static,
1038 {
1039 self.options = self.options.on_file_chooser(handler);
1040 self
1041 }
1042
1043 pub fn create(self) -> WebViewSession {
1050 create_webview_session(self.webtag, self.options)
1051 }
1052}
1053
1054impl BrowserWebViewBuilder {
1055 pub fn delegate(mut self, delegate: Arc<dyn WebViewDelegate>) -> Self {
1059 self.options = self.options.delegate(delegate);
1060 self
1061 }
1062
1063 pub fn on_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
1064 where
1065 F: Fn(http::Request<Vec<u8>>) -> Fut + Send + Sync + 'static,
1066 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
1067 {
1068 self.options = self.options.on_scheme(scheme, handler);
1069 self
1070 }
1071
1072 pub fn on_contextual_scheme<F, Fut>(mut self, scheme: &str, handler: F) -> Self
1075 where
1076 F: Fn(ContextualSchemeRequest) -> Fut + Send + Sync + 'static,
1077 Fut: std::future::Future<Output = SchemeOutcome> + Send + 'static,
1078 {
1079 self.options = self.options.on_contextual_scheme(scheme, handler);
1080 self
1081 }
1082
1083 pub fn on_navigation<F>(mut self, handler: F) -> Self
1084 where
1085 F: Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync + 'static,
1086 {
1087 self.options = self.options.on_navigation(handler);
1088 self
1089 }
1090
1091 pub fn on_new_window<F>(mut self, handler: F) -> Self
1092 where
1093 F: Fn(&str) -> NewWindowPolicy + Send + Sync + 'static,
1094 {
1095 self.options = self.options.on_new_window(handler);
1096 self
1097 }
1098
1099 pub fn on_download<F>(mut self, handler: F) -> Self
1104 where
1105 F: Fn(DownloadRequest) + Send + Sync + 'static,
1106 {
1107 self.options = self.options.on_download(handler);
1108 self
1109 }
1110
1111 pub fn data_mode(mut self, data_mode: WebViewDataMode) -> Self {
1113 self.options = self.options.data_mode(data_mode);
1114 self
1115 }
1116
1117 pub fn on_file_chooser<F, Fut>(mut self, handler: F) -> Self
1118 where
1119 F: Fn(FileChooserRequest) -> Fut + Send + Sync + 'static,
1120 Fut: Future<Output = FileChooserResponse> + Send + 'static,
1121 {
1122 self.options = self.options.on_file_chooser(handler);
1123 self
1124 }
1125
1126 pub fn create(self) -> WebViewSession {
1133 create_webview_session(self.webtag, self.options)
1134 }
1135}
1136
1137pub(crate) struct PendingCallbacks {
1140 pub(crate) scheme_handlers: HashMap<String, AsyncSchemeHandler>,
1141 pub(crate) navigation_handler: Option<NavigationHandler>,
1142 pub(crate) new_window_handler: Option<NewWindowHandler>,
1143 pub(crate) download_handler: Option<DownloadHandler>,
1144 pub(crate) file_chooser_handler: Option<FileChooserHandler>,
1145 pub(crate) delegate: Option<Arc<dyn WebViewDelegate>>,
1146}
1147
1148impl PendingCallbacks {
1149 fn has_any(&self) -> bool {
1150 !self.scheme_handlers.is_empty()
1151 || self.navigation_handler.is_some()
1152 || self.new_window_handler.is_some()
1153 || self.download_handler.is_some()
1154 || self.file_chooser_handler.is_some()
1155 || self.delegate.is_some()
1156 }
1157}
1158
1159pub struct WebView {
1161 pub(crate) inner: WebViewInner,
1162 native_view_id: NativeWebViewId,
1163 effective_options: EffectiveWebViewCreateOptions,
1164 delegate: RwLock<Option<Arc<dyn WebViewDelegate>>>,
1166 scheme_handlers: RwLock<HashMap<String, AsyncSchemeHandler>>,
1168 navigation_handler: RwLock<Option<NavigationHandler>>,
1169 new_window_handler: RwLock<Option<NewWindowHandler>>,
1170 download_handler: RwLock<Option<DownloadHandler>>,
1171 file_chooser_handler: RwLock<Option<FileChooserHandler>>,
1172 message_ingress: Arc<WebMessageIngress>,
1173}
1174
1175pub struct TrustedDataLoadReservation<'a> {
1182 webview: &'a WebView,
1183 webtag: WebTag,
1184 native_view_id: NativeWebViewId,
1185 intent: Option<TrustedLoadIntent>,
1186}
1187
1188enum TrustedDataLoadEvidence {
1189 #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))]
1190 NativeKey(crate::events::normalizer::NativeKey),
1191 #[cfg(any(target_os = "windows", all(target_os = "linux", target_env = "ohos")))]
1192 PlatformAttested,
1193}
1194
1195impl TrustedDataLoadReservation<'_> {
1196 pub fn intent(&self) -> TrustedLoadIntent {
1198 self.intent
1199 .expect("trusted data load reservation must retain its intent until consumed")
1200 }
1201
1202 pub fn disarm(mut self) -> TrustedLoadIntent {
1205 self.intent
1206 .take()
1207 .expect("trusted data load reservation must retain its intent until consumed")
1208 }
1209
1210 pub fn load(mut self, request: LoadDataRequest<'_>) -> Result<TrustedLoadIntent, WebViewError> {
1212 let intent = self
1213 .intent
1214 .take()
1215 .expect("trusted data load reservation must be consumed once");
1216 let evidence = match self.webview.load_trusted_data_on_platform(intent, request) {
1217 Ok(evidence) => evidence,
1218 Err(error) => {
1219 crate::events::normalizer::revoke_trusted_load(
1220 &self.webtag,
1221 self.native_view_id,
1222 intent,
1223 );
1224 return Err(error);
1225 }
1226 };
1227
1228 let attested = match evidence {
1229 #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))]
1230 TrustedDataLoadEvidence::NativeKey(key) => {
1231 crate::events::normalizer::attest_trusted_load(
1232 &self.webtag,
1233 self.native_view_id,
1234 intent,
1235 key,
1236 )
1237 }
1238 #[cfg(any(target_os = "windows", all(target_os = "linux", target_env = "ohos")))]
1239 TrustedDataLoadEvidence::PlatformAttested => true,
1240 };
1241 if attested {
1242 Ok(intent)
1243 } else {
1244 crate::events::normalizer::revoke_trusted_load(
1248 &self.webtag,
1249 self.native_view_id,
1250 intent,
1251 );
1252 Err(WebViewError::WebView(
1253 "trusted data load lost its WebView lifecycle binding".to_string(),
1254 ))
1255 }
1256 }
1257}
1258
1259impl Drop for TrustedDataLoadReservation<'_> {
1260 fn drop(&mut self) {
1261 if let Some(intent) = self.intent.take() {
1262 crate::events::normalizer::revoke_trusted_load(
1263 &self.webtag,
1264 self.native_view_id,
1265 intent,
1266 );
1267 }
1268 }
1269}
1270
1271#[cfg_attr(
1272 not(any(
1273 target_os = "android",
1274 target_os = "ios",
1275 target_os = "macos",
1276 target_os = "windows"
1277 )),
1278 allow(dead_code)
1279)]
1280fn snapshot_web_message_context(
1281 native_view_id: NativeWebViewId,
1282 frame: WebMessageFrame,
1283 transport: WebMessageTransport,
1284 source: WebMessageSource,
1285) -> WebMessageContext {
1286 WebMessageContext::new(
1290 native_view_id,
1291 crate::events::normalizer::current_document_binding(native_view_id),
1292 frame,
1293 transport,
1294 source,
1295 )
1296}
1297
1298fn document_port_context(
1301 native_view: NativeWebViewId,
1302 current: crate::DocumentBinding,
1303 expected_generation: crate::DocumentGeneration,
1304 transport: WebMessageTransport,
1305 source: WebMessageSource,
1306) -> Option<WebMessageContext> {
1307 let port_transport = matches!(
1308 transport,
1309 WebMessageTransport::AndroidMessagePort | WebMessageTransport::HarmonyMessagePort
1310 );
1311 (port_transport && current == crate::DocumentBinding::Bound(expected_generation)).then(|| {
1312 WebMessageContext::new(
1313 native_view,
1314 crate::DocumentBinding::Bound(expected_generation),
1315 WebMessageFrame::TopLevel,
1316 transport,
1317 source,
1318 )
1319 })
1320}
1321
1322impl WebView {
1323 pub(crate) fn new(
1324 inner: WebViewInner,
1325 effective_options: EffectiveWebViewCreateOptions,
1326 native_view_id: NativeWebViewId,
1327 ) -> Self {
1328 Self {
1329 inner,
1330 native_view_id,
1331 effective_options,
1332 delegate: RwLock::new(None),
1333 scheme_handlers: RwLock::new(HashMap::new()),
1334 navigation_handler: RwLock::new(None),
1335 new_window_handler: RwLock::new(None),
1336 download_handler: RwLock::new(None),
1337 file_chooser_handler: RwLock::new(None),
1338 message_ingress: Arc::new(WebMessageIngress::default()),
1339 }
1340 }
1341
1342 pub const fn native_view_id(&self) -> NativeWebViewId {
1348 self.native_view_id
1349 }
1350
1351 #[cfg(any(
1354 target_os = "android",
1355 target_os = "ios",
1356 target_os = "macos",
1357 target_os = "windows",
1358 all(target_os = "linux", target_env = "ohos")
1359 ))]
1360 pub(crate) fn reject_oversized_web_message(&self) {
1361 let WebMessageEnqueue::Rejected { reason, count } = self
1362 .message_ingress
1363 .reject(WebMessageRejectReason::MessageTooLarge)
1364 else {
1365 unreachable!("rejecting an ingress message must return a rejection")
1366 };
1367 if should_sample_rejection(count) {
1368 log::warn!(
1369 "Dropping WebView message reason={} count={} ({})",
1370 reason.as_str(),
1371 count,
1372 self.webtag()
1373 );
1374 }
1375 }
1376
1377 pub fn prepare_trusted_data_load(
1385 &self,
1386 ) -> Result<TrustedDataLoadReservation<'_>, WebViewError> {
1387 let webtag = self.webtag();
1388 let native_view_id = self.native_view_id;
1389 let intent = crate::events::normalizer::issue_trusted_load(&webtag, native_view_id)
1390 .ok_or_else(|| {
1391 WebViewError::WebView(
1392 "trusted data load requires a live matching WebView lifecycle".to_string(),
1393 )
1394 })?;
1395 Ok(TrustedDataLoadReservation {
1396 webview: self,
1397 webtag,
1398 native_view_id,
1399 intent: Some(intent),
1400 })
1401 }
1402
1403 #[cfg(any(target_os = "ios", target_os = "macos", target_os = "android"))]
1404 fn load_trusted_data_on_platform(
1405 &self,
1406 _intent: TrustedLoadIntent,
1407 request: LoadDataRequest<'_>,
1408 ) -> Result<TrustedDataLoadEvidence, WebViewError> {
1409 self.inner
1410 .load_trusted_data(request)
1411 .map(TrustedDataLoadEvidence::NativeKey)
1412 }
1413
1414 #[cfg(target_os = "windows")]
1415 fn load_trusted_data_on_platform(
1416 &self,
1417 intent: TrustedLoadIntent,
1418 request: LoadDataRequest<'_>,
1419 ) -> Result<TrustedDataLoadEvidence, WebViewError> {
1420 self.inner.load_trusted_data(intent, request)?;
1421 Ok(TrustedDataLoadEvidence::PlatformAttested)
1422 }
1423
1424 #[cfg(all(target_os = "linux", target_env = "ohos"))]
1425 fn load_trusted_data_on_platform(
1426 &self,
1427 intent: TrustedLoadIntent,
1428 request: LoadDataRequest<'_>,
1429 ) -> Result<TrustedDataLoadEvidence, WebViewError> {
1430 self.inner
1431 .load_trusted_data(intent, request)
1432 .map(|()| TrustedDataLoadEvidence::PlatformAttested)
1433 }
1434
1435 #[cfg(not(any(
1436 target_os = "ios",
1437 target_os = "macos",
1438 target_os = "windows",
1439 target_os = "android",
1440 all(target_os = "linux", target_env = "ohos")
1441 )))]
1442 fn load_trusted_data_on_platform(
1443 &self,
1444 _intent: TrustedLoadIntent,
1445 _request: LoadDataRequest<'_>,
1446 ) -> Result<TrustedDataLoadEvidence, WebViewError> {
1447 Err(WebViewError::Unsupported(
1448 "trusted direct HTML loads require a platform navigation key".to_string(),
1449 ))
1450 }
1451
1452 #[cfg_attr(
1453 not(any(
1454 target_os = "android",
1455 target_os = "ios",
1456 target_os = "macos",
1457 target_os = "windows"
1458 )),
1459 allow(dead_code)
1460 )]
1461 pub(crate) fn enqueue_web_message(
1467 self: &Arc<Self>,
1468 body: String,
1469 frame: WebMessageFrame,
1470 transport: WebMessageTransport,
1471 source: WebMessageSource,
1472 ) {
1473 let context = snapshot_web_message_context(self.native_view_id, frame, transport, source);
1474 match self
1475 .message_ingress
1476 .enqueue(IncomingWebMessage::new(body, context))
1477 {
1478 WebMessageEnqueue::Queued => return,
1479 WebMessageEnqueue::Rejected { reason, count } => {
1480 if should_sample_rejection(count) {
1481 log::warn!(
1482 "Dropping WebView message reason={} count={} ({})",
1483 reason.as_str(),
1484 count,
1485 self.webtag()
1486 );
1487 }
1488 return;
1489 }
1490 WebMessageEnqueue::Schedule => {}
1491 }
1492
1493 if WebMessageExecutor::global()
1494 .schedule(WebMessageJob {
1495 ingress: Arc::clone(&self.message_ingress),
1496 webview: Arc::downgrade(self),
1497 })
1498 .is_err()
1499 {
1500 self.message_ingress.close();
1503 log::error!(
1504 "Dropping WebView message queue because the fixed executor stopped ({})",
1505 self.webtag()
1506 );
1507 }
1508 }
1509
1510 #[cfg_attr(
1514 not(any(target_os = "android", all(target_os = "linux", target_env = "ohos"))),
1515 allow(dead_code)
1516 )]
1517 pub(crate) fn enqueue_document_web_message(
1518 self: &Arc<Self>,
1519 body: String,
1520 expected_generation: crate::DocumentGeneration,
1521 transport: WebMessageTransport,
1522 source: WebMessageSource,
1523 ) {
1524 let Some(context) = document_port_context(
1525 self.native_view_id,
1526 crate::events::normalizer::current_document_binding(self.native_view_id),
1527 expected_generation,
1528 transport,
1529 source,
1530 ) else {
1531 log::debug!(
1532 "Dropping stale document MessagePort frame ({})",
1533 self.webtag()
1534 );
1535 return;
1536 };
1537 match self
1538 .message_ingress
1539 .enqueue(IncomingWebMessage::new(body, context))
1540 {
1541 WebMessageEnqueue::Queued => return,
1542 WebMessageEnqueue::Rejected { reason, count } => {
1543 if should_sample_rejection(count) {
1544 log::warn!(
1545 "Dropping document MessagePort frame reason={} count={} ({})",
1546 reason.as_str(),
1547 count,
1548 self.webtag()
1549 );
1550 }
1551 return;
1552 }
1553 WebMessageEnqueue::Schedule => {}
1554 }
1555 if WebMessageExecutor::global()
1556 .schedule(WebMessageJob {
1557 ingress: Arc::clone(&self.message_ingress),
1558 webview: Arc::downgrade(self),
1559 })
1560 .is_err()
1561 {
1562 self.message_ingress.close();
1563 }
1564 }
1565
1566 fn close_message_ingress(&self) {
1567 self.message_ingress.close();
1568 }
1569
1570 pub fn current_document_binding(&self) -> crate::DocumentBinding {
1575 crate::events::normalizer::current_document_binding(self.native_view_id)
1576 }
1577
1578 pub fn appid(&self) -> String {
1580 self.inner.webtag.extract_appid()
1581 }
1582
1583 pub fn path(&self) -> String {
1585 self.inner.webtag.extract_parts().1
1586 }
1587
1588 pub fn webtag(&self) -> WebTag {
1590 self.inner.webtag.clone()
1591 }
1592
1593 pub(crate) fn effective_options(&self) -> &EffectiveWebViewCreateOptions {
1594 &self.effective_options
1595 }
1596
1597 pub(crate) fn get_delegate(&self) -> Option<Arc<dyn WebViewDelegate>> {
1599 self.delegate.read().ok().and_then(|guard| guard.clone())
1600 }
1601
1602 pub(crate) fn remove_delegate(&self) {
1604 if let Ok(mut guard) = self.delegate.write() {
1605 *guard = None;
1606 }
1607 }
1608
1609 pub(crate) fn install_callbacks(&self, callbacks: PendingCallbacks) {
1611 if let Some(delegate) = callbacks.delegate
1612 && let Ok(mut guard) = self.delegate.write()
1613 {
1614 *guard = Some(delegate);
1615 }
1616 if let Ok(mut guard) = self.scheme_handlers.write() {
1617 *guard = callbacks.scheme_handlers;
1618 }
1619 if let Some(handler) = callbacks.navigation_handler
1620 && let Ok(mut guard) = self.navigation_handler.write()
1621 {
1622 *guard = Some(handler);
1623 }
1624 if let Some(handler) = callbacks.new_window_handler
1625 && let Ok(mut guard) = self.new_window_handler.write()
1626 {
1627 *guard = Some(handler);
1628 }
1629 if let Some(handler) = callbacks.download_handler
1630 && let Ok(mut guard) = self.download_handler.write()
1631 {
1632 *guard = Some(handler);
1633 }
1634 if let Some(handler) = callbacks.file_chooser_handler
1635 && let Ok(mut guard) = self.file_chooser_handler.write()
1636 {
1637 *guard = Some(handler);
1638 }
1639 }
1640
1641 pub fn has_scheme_handler(&self, scheme: &str) -> bool {
1643 self.scheme_handlers
1644 .read()
1645 .ok()
1646 .is_some_and(|guard| guard.contains_key(scheme))
1647 }
1648
1649 #[allow(dead_code)]
1656 pub(crate) fn handle_scheme_request(
1657 &self,
1658 scheme: &str,
1659 request: http::Request<Vec<u8>>,
1660 ) -> Option<WebResourceResponse> {
1661 self.handle_contextual_scheme_request(
1662 scheme,
1663 ContextualSchemeRequest::new(
1664 request,
1665 self.native_view_id(),
1666 SchemeRequestFrame::Unproven,
1667 ),
1668 )
1669 }
1670
1671 pub(crate) fn handle_contextual_scheme_request(
1675 &self,
1676 scheme: &str,
1677 request: ContextualSchemeRequest,
1678 ) -> Option<WebResourceResponse> {
1679 #[cfg(any(target_os = "ios", target_os = "macos"))]
1680 if let Some(response) = self.inner.handle_internal_bridge_request(request.request()) {
1681 return Some(response);
1682 }
1683
1684 let guard = self.scheme_handlers.read().ok()?;
1685 let handler = guard.get(scheme)?;
1686 let outcome = block_on_scheme_future(handler(request));
1687 match outcome {
1688 SchemeOutcome::Handled(response) => Some(response),
1689 SchemeOutcome::PassThrough => None,
1690 }
1691 }
1692
1693 pub fn handle_navigation(&self, request: &NavigationRequest) -> NavigationPolicy {
1698 if crate::url_callback::dispatch(&request.url) {
1699 return NavigationPolicy::Cancel;
1700 }
1701 if let Ok(guard) = self.navigation_handler.read()
1702 && let Some(handler) = guard.as_ref()
1703 {
1704 return handler(request);
1705 }
1706 NavigationPolicy::Allow
1707 }
1708
1709 pub fn has_new_window_handler(&self) -> bool {
1711 self.new_window_handler
1712 .read()
1713 .ok()
1714 .is_some_and(|guard| guard.is_some())
1715 }
1716
1717 pub fn handle_new_window(&self, url: &str) -> NewWindowPolicy {
1722 if crate::url_callback::dispatch(url) {
1723 return NewWindowPolicy::Cancel;
1724 }
1725 if let Ok(guard) = self.new_window_handler.read()
1726 && let Some(handler) = guard.as_ref()
1727 {
1728 return handler(url);
1729 }
1730 NewWindowPolicy::Cancel
1731 }
1732
1733 pub(crate) fn handle_download(&self, request: DownloadRequest) {
1735 if let Ok(guard) = self.download_handler.read()
1736 && let Some(handler) = guard.as_ref()
1737 {
1738 handler(request);
1739 }
1740 }
1741
1742 #[cfg_attr(not(target_os = "windows"), allow(dead_code))]
1744 pub(crate) fn has_download_handler(&self) -> bool {
1745 self.download_handler
1746 .read()
1747 .ok()
1748 .is_some_and(|guard| guard.is_some())
1749 }
1750
1751 #[cfg_attr(target_os = "windows", allow(dead_code))]
1752 pub(crate) fn handle_file_chooser<C>(&self, request: FileChooserRequest, completion: C) -> bool
1753 where
1754 C: FnOnce(FileChooserResponse) + Send + 'static,
1755 {
1756 let Some(future) = self.make_file_chooser_future(request) else {
1757 return false;
1758 };
1759 std::thread::spawn(move || {
1760 completion(block_on_scheme_future(future));
1761 });
1762 true
1763 }
1764
1765 #[cfg_attr(target_os = "windows", allow(dead_code))]
1766 fn make_file_chooser_future(&self, request: FileChooserRequest) -> Option<FileChooserFuture> {
1767 let Ok(guard) = self.file_chooser_handler.read() else {
1768 return None;
1769 };
1770 let handler = guard.as_ref()?;
1771 Some(handler(request))
1772 }
1773
1774 #[cfg(target_os = "macos")]
1776 pub fn toggle_devtools(&self) {
1777 self.inner.toggle_devtools();
1778 }
1779
1780 #[cfg(target_os = "macos")]
1782 pub fn toggle_devtools_detached(&self) {
1783 self.inner.toggle_devtools_detached();
1784 }
1785
1786 #[cfg(any(target_os = "ios", target_os = "macos"))]
1788 pub fn get_swift_webview_ptr(&self) -> usize {
1789 self.inner.get_swift_webview_ptr()
1790 }
1791
1792 #[cfg(target_os = "android")]
1794 pub fn get_java_webview(&self) -> &jni::objects::Global<jni::objects::JObject<'static>> {
1795 self.inner.get_java_webview()
1796 }
1797
1798 pub async fn evaluate_javascript(
1799 &self,
1800 js: &str,
1801 ) -> Result<serde_json::Value, crate::WebViewScriptError> {
1802 self.inner.eval_js(js).await
1803 }
1804
1805 #[cfg(any(
1813 target_os = "ios",
1814 target_os = "android",
1815 all(feature = "webview-input", target_os = "macos"),
1816 all(target_os = "linux", target_env = "ohos")
1817 ))]
1818 async fn run_js_action(&self, script: &str) -> Result<(), WebViewInputError> {
1819 let result = self
1820 .inner
1821 .eval_js(script)
1822 .await
1823 .map_err(WebViewInputError::Script)?;
1824 if result.get("ok").and_then(|v| v.as_bool()) == Some(true) {
1825 return Ok(());
1826 }
1827 let err_msg = result
1828 .get("error")
1829 .and_then(|v| v.as_str())
1830 .unwrap_or("input action failed")
1831 .to_string();
1832 if result.get("interactable").and_then(|v| v.as_bool()) == Some(false) {
1833 Err(WebViewInputError::ElementNotInteractable(err_msg))
1834 } else {
1835 Err(WebViewInputError::ElementNotFound(err_msg))
1836 }
1837 }
1838
1839 #[cfg(any(
1845 target_os = "ios",
1846 all(feature = "webview-input", target_os = "macos"),
1847 all(target_os = "linux", target_env = "ohos")
1848 ))]
1849 pub(crate) async fn click_via_js(
1850 &self,
1851 selector: &str,
1852 index: Option<usize>,
1853 ) -> Result<(), WebViewInputError> {
1854 let selector_json = serde_json::to_string(selector)
1855 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1856 let idx = index.unwrap_or(0);
1857 let script = format!(
1858 "((sel, i) => {{ \
1859 const els = document.querySelectorAll(sel); \
1860 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1861 const el = els[i]; \
1862 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1863 const rect = el.getBoundingClientRect(); \
1864 const style = window.getComputedStyle(el); \
1865 const disabled = !!el.disabled || el.getAttribute('aria-disabled') === 'true'; \
1866 const visible = rect.width > 0 && rect.height > 0 && rect.bottom > 0 && rect.right > 0 && \
1867 rect.top < window.innerHeight && rect.left < window.innerWidth && \
1868 style.visibility !== 'hidden' && style.display !== 'none' && Number(style.opacity || '1') !== 0; \
1869 if (!visible) return {{ ok:false, error:'not visible', interactable:false, count:els.length }}; \
1870 if (disabled) return {{ ok:false, error:'not enabled', interactable:false, count:els.length }}; \
1871 const tag = (el.tagName || '').toLowerCase(); \
1872 if (tag.indexOf('lx-') === 0) {{ \
1873 el.setAttribute('focus', 'true'); \
1874 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1875 return {{ ok:true, count:els.length, native:true }}; \
1876 }} \
1877 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1878 const opts = {{ bubbles:true, cancelable:true, view:window, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 }}; \
1879 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1880 try {{ el.dispatchEvent(new MouseEvent('mousedown', opts)); }} catch(_e) {{}} \
1881 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerup', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1882 try {{ el.dispatchEvent(new MouseEvent('mouseup', opts)); }} catch(_e) {{}} \
1883 try {{ el.dispatchEvent(new MouseEvent('click', opts)); }} catch(_e) {{}} \
1884 return {{ ok:true, count:els.length }}; \
1885 }})({selector_json}, {idx})"
1886 );
1887 self.run_js_action(&script).await
1888 }
1889
1890 #[cfg(any(
1894 target_os = "ios",
1895 target_os = "android",
1896 all(feature = "webview-input", target_os = "macos"),
1897 all(target_os = "linux", target_env = "ohos")
1898 ))]
1899 pub(crate) async fn type_via_js(
1900 &self,
1901 selector: &str,
1902 index: Option<usize>,
1903 text: &str,
1904 replace: bool,
1905 ) -> Result<(), WebViewInputError> {
1906 let selector_json = serde_json::to_string(selector)
1907 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1908 let text_json = serde_json::to_string(text)
1909 .map_err(|err| WebViewInputError::Platform(format!("Invalid text: {err}")))?;
1910 let idx = index.unwrap_or(0);
1911 let script = format!(
1912 "((sel, i, text, replace) => {{ \
1913 const els = document.querySelectorAll(sel); \
1914 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1915 const el = els[i]; \
1916 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1917 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1918 const tag = (el.tagName || '').toLowerCase(); \
1919 if (tag === 'input' || tag === 'textarea') {{ \
1920 const proto = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; \
1921 const desc = Object.getOwnPropertyDescriptor(proto, 'value'); \
1922 const next = (replace ? '' : (el.value || '')) + text; \
1923 if (desc && desc.set) {{ desc.set.call(el, next); }} else {{ el.value = next; }} \
1924 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, cancelable:true, data:text, inputType:'insertText' }})); \
1925 el.dispatchEvent(new Event('change', {{ bubbles:true }})); \
1926 return {{ ok:true, count:els.length }}; \
1927 }} \
1928 if (el.isContentEditable) {{ \
1929 el.textContent = (replace ? '' : (el.textContent || '')) + text; \
1930 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, data:text, inputType:'insertText' }})); \
1931 return {{ ok:true, count:els.length }}; \
1932 }} \
1933 if (tag.indexOf('lx-') === 0) {{ \
1934 try {{ el.value = (replace ? '' : (el.value || '')) + text; }} catch(_e) {{}} \
1935 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1936 el.dispatchEvent(new Event('input', {{ bubbles:true }})); \
1937 return {{ ok:true, count:els.length, native:true }}; \
1938 }} \
1939 return {{ ok:false, error:'not editable', interactable:false, count:els.length }}; \
1940 }})({selector_json}, {idx}, {text_json}, {replace})"
1941 );
1942 self.run_js_action(&script).await
1943 }
1944
1945 #[cfg(any(
1947 target_os = "ios",
1948 target_os = "android",
1949 all(feature = "webview-input", target_os = "macos"),
1950 all(target_os = "linux", target_env = "ohos")
1951 ))]
1952 pub(crate) async fn press_via_js(
1953 &self,
1954 key: &str,
1955 selector: Option<&str>,
1956 index: Option<usize>,
1957 ) -> Result<(), WebViewInputError> {
1958 let key_json = serde_json::to_string(key)
1959 .map_err(|err| WebViewInputError::Platform(format!("Invalid key: {err}")))?;
1960 let selector_json = serde_json::to_string(&selector)
1961 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1962 let idx = index.unwrap_or(0);
1963 let script = format!(
1964 "((key, sel, i) => {{ \
1965 const els = sel === null ? null : document.querySelectorAll(sel); \
1966 if (els && (!els.length || i < 0 || i >= els.length)) return {{ ok:false, error:'no match', count:els.length }}; \
1967 const el = els ? els[i] : (document.activeElement || document.body); \
1968 if (els) {{ \
1969 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1970 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1971 }} \
1972 const map = {{ enter:'Enter', 'return':'Enter', tab:'Tab', esc:'Escape', escape:'Escape', backspace:'Backspace', 'delete':'Delete', forwarddelete:'Delete', space:' ', up:'ArrowUp', down:'ArrowDown', left:'ArrowLeft', right:'ArrowRight', arrowup:'ArrowUp', arrowdown:'ArrowDown', arrowleft:'ArrowLeft', arrowright:'ArrowRight', home:'Home', end:'End', pageup:'PageUp', pagedown:'PageDown' }}; \
1973 const norm = String(key).toLowerCase(); \
1974 const k = map[norm] || key; \
1975 const opts = {{ bubbles:true, cancelable:true, composed:true, key:k, view:window }}; \
1976 el.dispatchEvent(new KeyboardEvent('keydown', opts)); \
1977 el.dispatchEvent(new KeyboardEvent('keyup', opts)); \
1978 return {{ ok:true }}; \
1979 }})({key_json}, {selector_json}, {idx})"
1980 );
1981 self.run_js_action(&script).await
1982 }
1983
1984 #[cfg(any(
1994 target_os = "ios",
1995 all(feature = "webview-input", target_os = "macos"),
1996 all(target_os = "linux", target_env = "ohos")
1997 ))]
1998 pub(crate) async fn scroll_via_js(
1999 &self,
2000 at: Option<(f64, f64)>,
2001 dx: f64,
2002 dy: f64,
2003 ) -> Result<(), WebViewInputError> {
2004 let (px, py) = at.unwrap_or((-1.0, -1.0));
2005 let script = format!(
2006 "((px, py, dx, dy) => {{ \
2007 const overflows = (v) => (/(auto|scroll|overlay)/).test(v); \
2008 const ancestor = (node) => {{ \
2009 while (node && node !== document.body && node !== document.documentElement) {{ \
2010 const s = window.getComputedStyle(node); \
2011 if ((overflows(s.overflowY) && node.scrollHeight > node.clientHeight) || \
2012 (overflows(s.overflowX) && node.scrollWidth > node.clientWidth)) return node; \
2013 node = node.parentElement; \
2014 }} \
2015 return null; \
2016 }}; \
2017 const largest = () => {{ \
2018 let best = null, range = 0; \
2019 const all = document.querySelectorAll('*'); \
2020 for (let k = 0; k < all.length; k++) {{ \
2021 const n = all[k], s = window.getComputedStyle(n); \
2022 const ry = overflows(s.overflowY) ? (n.scrollHeight - n.clientHeight) : 0; \
2023 const rx = overflows(s.overflowX) ? (n.scrollWidth - n.clientWidth) : 0; \
2024 const r = ry > rx ? ry : rx; \
2025 if (r > range) {{ range = r; best = n; }} \
2026 }} \
2027 return best; \
2028 }}; \
2029 const vw = window.innerWidth || document.documentElement.clientWidth || 0; \
2030 const vh = window.innerHeight || document.documentElement.clientHeight || 0; \
2031 let target = null; \
2032 if (px >= 0 && py >= 0) target = ancestor(document.elementFromPoint(px, py) || document.body); \
2033 else if (vw > 0 && vh > 0) target = ancestor(document.elementFromPoint(vw >> 1, vh >> 1) || document.body); \
2034 if (!target) {{ \
2035 const se = document.scrollingElement || document.documentElement; \
2036 target = (se && se.scrollHeight > se.clientHeight) ? se : (largest() || se); \
2037 }} \
2038 target.scrollLeft += dx; target.scrollTop += dy; \
2039 return {{ ok:true }}; \
2040 }})({px}, {py}, {dx}, {dy})"
2041 );
2042 self.run_js_action(&script).await
2043 }
2044
2045 #[cfg(any(
2047 target_os = "ios",
2048 all(feature = "webview-input", target_os = "macos"),
2049 all(target_os = "linux", target_env = "ohos")
2050 ))]
2051 pub(crate) async fn scroll_to_via_js(
2052 &self,
2053 selector: &str,
2054 index: Option<usize>,
2055 ) -> Result<(), WebViewInputError> {
2056 let selector_json = serde_json::to_string(selector)
2057 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
2058 let idx = index.unwrap_or(0);
2059 let script = format!(
2060 "((sel, i) => {{ \
2061 const els = document.querySelectorAll(sel); \
2062 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
2063 try {{ els[i].scrollIntoView({{ block:'center', inline:'center' }}); }} catch(_e) {{ els[i].scrollIntoView(); }} \
2064 return {{ ok:true, count:els.length }}; \
2065 }})({selector_json}, {idx})"
2066 );
2067 self.run_js_action(&script).await
2068 }
2069
2070 pub async fn current_url(&self) -> Result<Option<String>, WebViewError> {
2071 self.inner.current_url().await
2072 }
2073
2074 pub fn reload(&self) -> Result<(), WebViewError> {
2075 self.inner.reload()
2076 }
2077
2078 pub fn go_back(&self) -> Result<(), WebViewError> {
2079 self.inner.go_back()
2080 }
2081
2082 pub fn go_forward(&self) -> Result<(), WebViewError> {
2083 self.inner.go_forward()
2084 }
2085
2086 pub async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
2087 self.inner.list_cookies().await
2088 }
2089
2090 pub async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
2091 self.inner.set_cookie(request).await
2092 }
2093
2094 pub async fn delete_cookie(
2095 &self,
2096 name: &str,
2097 domain: &str,
2098 path: &str,
2099 ) -> Result<(), WebViewError> {
2100 self.inner.delete_cookie(name, domain, path).await
2101 }
2102
2103 pub async fn clear_cookies(&self) -> Result<(), WebViewError> {
2104 self.inner.clear_cookies().await
2105 }
2106
2107 pub async fn start_network_capture(&self) -> Result<(), WebViewError> {
2108 self.inner.start_network_capture().await
2109 }
2110
2111 pub async fn stop_network_capture(&self) -> Result<(), WebViewError> {
2112 self.inner.stop_network_capture().await
2113 }
2114
2115 pub async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
2116 self.inner.network_entries().await
2117 }
2118
2119 pub async fn clear_network_capture(&self) -> Result<(), WebViewError> {
2120 self.inner.clear_network_capture().await
2121 }
2122
2123 pub async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
2124 self.inner.take_screenshot().await
2125 }
2126
2127 pub async fn click(
2128 &self,
2129 selector: &str,
2130 options: ClickOptions,
2131 ) -> Result<(), WebViewInputError> {
2132 <Self as WebViewInputController>::click(self, selector, options).await
2133 }
2134
2135 pub async fn type_text(
2136 &self,
2137 selector: &str,
2138 text: &str,
2139 options: TypeOptions,
2140 ) -> Result<(), WebViewInputError> {
2141 <Self as WebViewInputController>::type_text(self, selector, text, options).await
2142 }
2143
2144 pub async fn fill(
2145 &self,
2146 selector: &str,
2147 text: &str,
2148 options: FillOptions,
2149 ) -> Result<(), WebViewInputError> {
2150 <Self as WebViewInputController>::fill(self, selector, text, options).await
2151 }
2152
2153 pub async fn press(&self, key: &str, options: PressOptions) -> Result<(), WebViewInputError> {
2154 <Self as WebViewInputController>::press(self, key, options).await
2155 }
2156
2157 pub async fn scroll(
2158 &self,
2159 dx: f64,
2160 dy: f64,
2161 options: ScrollOptions,
2162 ) -> Result<(), WebViewInputError> {
2163 <Self as WebViewInputController>::scroll(self, dx, dy, options).await
2164 }
2165
2166 pub async fn scroll_to(
2167 &self,
2168 selector: &str,
2169 options: ScrollOptions,
2170 ) -> Result<(), WebViewInputError> {
2171 <Self as WebViewInputController>::scroll_to(self, selector, options).await
2172 }
2173}
2174
2175#[async_trait]
2176impl WebViewController for WebView {
2177 fn load_url(&self, url: &str) -> Result<(), WebViewError> {
2178 self.inner.load_url(url)
2179 }
2180
2181 fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
2182 self.inner.load_data(request)
2183 }
2184
2185 fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
2186 self.inner.exec_js(js)
2187 }
2188
2189 async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
2190 self.inner.eval_js(js).await
2191 }
2192
2193 async fn current_url(&self) -> Result<Option<String>, WebViewError> {
2194 self.inner.current_url().await
2195 }
2196
2197 fn post_message(&self, message: &str) -> Result<(), WebViewError> {
2198 self.inner.post_message(message)
2199 }
2200
2201 fn post_message_to_document(
2202 &self,
2203 expected_generation: crate::DocumentGeneration,
2204 gate: Arc<dyn crate::DocumentOutboundGate>,
2205 message: &str,
2206 ) -> Result<(), WebViewError> {
2207 self.inner
2208 .post_message_to_document(expected_generation, gate, message)
2209 }
2210
2211 fn clear_browsing_data(&self) -> Result<(), WebViewError> {
2212 self.inner.clear_browsing_data()
2213 }
2214
2215 fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError> {
2216 user_agent.validate()?;
2217 self.inner.set_user_agent_override(user_agent)
2218 }
2219
2220 fn reload(&self) -> Result<(), WebViewError> {
2221 self.inner.reload()
2222 }
2223
2224 fn go_back(&self) -> Result<(), WebViewError> {
2225 self.inner.go_back()
2226 }
2227
2228 fn go_forward(&self) -> Result<(), WebViewError> {
2229 self.inner.go_forward()
2230 }
2231
2232 async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
2233 self.inner.list_cookies().await
2234 }
2235
2236 async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
2237 self.inner.set_cookie(request).await
2238 }
2239
2240 async fn delete_cookie(
2241 &self,
2242 name: &str,
2243 domain: &str,
2244 path: &str,
2245 ) -> Result<(), WebViewError> {
2246 self.inner.delete_cookie(name, domain, path).await
2247 }
2248
2249 async fn clear_cookies(&self) -> Result<(), WebViewError> {
2250 self.inner.clear_cookies().await
2251 }
2252
2253 async fn clear_site_data(
2254 &self,
2255 url: &str,
2256 options: ClearSiteDataOptions,
2257 ) -> Result<ClearSiteDataResult, WebViewError> {
2258 self.inner.clear_site_data(url, options).await
2259 }
2260
2261 async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
2266 self.inner.take_screenshot().await
2267 }
2268
2269 async fn start_network_capture(&self) -> Result<(), WebViewError> {
2270 self.inner.start_network_capture().await
2271 }
2272
2273 async fn stop_network_capture(&self) -> Result<(), WebViewError> {
2274 self.inner.stop_network_capture().await
2275 }
2276
2277 async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
2278 self.inner.network_entries().await
2279 }
2280
2281 async fn clear_network_capture(&self) -> Result<(), WebViewError> {
2282 self.inner.clear_network_capture().await
2283 }
2284}
2285
2286#[async_trait]
2287impl WebViewInputController for WebView {
2288 async fn click(
2289 &self,
2290 _selector: &str,
2291 _options: ClickOptions,
2292 ) -> Result<(), WebViewInputError> {
2293 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2299 {
2300 return self.click_via_js(_selector, _options.index).await;
2301 }
2302 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2303 {
2304 return self.inner.click_inner(_selector, _options).await;
2305 }
2306 #[cfg(target_os = "android")]
2307 {
2308 return self.inner.click_inner(_selector, _options).await;
2309 }
2310 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
2311 {
2312 return self.click_via_js(_selector, _options.index).await;
2313 }
2314 #[allow(unreachable_code)]
2315 Err(WebViewInputError::Unsupported(
2316 "input control is not implemented for this platform",
2317 ))
2318 }
2319
2320 async fn type_text(
2321 &self,
2322 _selector: &str,
2323 _text: &str,
2324 _options: TypeOptions,
2325 ) -> Result<(), WebViewInputError> {
2326 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2327 {
2328 if self.inner.is_window_attached().await {
2329 return self.inner.type_text_inner(_selector, _text, _options).await;
2330 }
2331 return self
2332 .type_via_js(_selector, _options.index, _text, _options.replace)
2333 .await;
2334 }
2335 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2336 {
2337 return self.inner.type_text_inner(_selector, _text, _options).await;
2338 }
2339 #[cfg(any(
2340 target_os = "ios",
2341 target_os = "android",
2342 all(target_os = "linux", target_env = "ohos")
2343 ))]
2344 {
2345 return self
2346 .type_via_js(_selector, _options.index, _text, _options.replace)
2347 .await;
2348 }
2349 #[allow(unreachable_code)]
2350 Err(WebViewInputError::Unsupported(
2351 "input control is not implemented for this platform",
2352 ))
2353 }
2354
2355 async fn fill(
2356 &self,
2357 _selector: &str,
2358 _text: &str,
2359 _options: FillOptions,
2360 ) -> Result<(), WebViewInputError> {
2361 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2362 {
2363 return self
2368 .type_via_js(_selector, _options.index, _text, true)
2369 .await;
2370 }
2371 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2372 {
2373 return self
2374 .inner
2375 .type_text_inner(
2376 _selector,
2377 _text,
2378 TypeOptions {
2379 index: _options.index,
2380 replace: true,
2381 },
2382 )
2383 .await;
2384 }
2385 #[cfg(any(
2386 target_os = "ios",
2387 target_os = "android",
2388 all(target_os = "linux", target_env = "ohos")
2389 ))]
2390 {
2391 return self
2392 .type_via_js(_selector, _options.index, _text, true)
2393 .await;
2394 }
2395 #[allow(unreachable_code)]
2396 Err(WebViewInputError::Unsupported(
2397 "input control is not implemented for this platform",
2398 ))
2399 }
2400
2401 async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
2402 if _options.index.is_some() && _options.selector.is_none() {
2403 return Err(WebViewInputError::Platform(
2404 "press index requires a selector".to_string(),
2405 ));
2406 }
2407 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2408 {
2409 if self.inner.is_window_attached().await {
2410 return self.inner.press_inner(_key, _options).await;
2411 }
2412 return self
2413 .press_via_js(_key, _options.selector.as_deref(), _options.index)
2414 .await;
2415 }
2416 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2417 {
2418 return self.inner.press_inner(_key, _options).await;
2419 }
2420 #[cfg(any(
2421 target_os = "ios",
2422 target_os = "android",
2423 all(target_os = "linux", target_env = "ohos")
2424 ))]
2425 {
2426 return self
2427 .press_via_js(_key, _options.selector.as_deref(), _options.index)
2428 .await;
2429 }
2430 #[allow(unreachable_code)]
2431 Err(WebViewInputError::Unsupported(
2432 "input control is not implemented for this platform",
2433 ))
2434 }
2435
2436 async fn scroll(
2437 &self,
2438 _dx: f64,
2439 _dy: f64,
2440 _options: ScrollOptions,
2441 ) -> Result<(), WebViewInputError> {
2442 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2445 {
2446 if self.inner.is_window_attached().await {
2447 return self.inner.scroll_inner(_dx, _dy, _options).await;
2448 }
2449 return self.scroll_via_js(None, _dx, _dy).await;
2450 }
2451 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2452 {
2453 return self.inner.scroll_inner(_dx, _dy, _options).await;
2454 }
2455 #[cfg(target_os = "android")]
2458 {
2459 return self.inner.scroll_inner(_dx, _dy, _options).await;
2460 }
2461 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
2463 {
2464 return self.scroll_via_js(None, _dx, _dy).await;
2465 }
2466 #[allow(unreachable_code)]
2467 Err(WebViewInputError::Unsupported(
2468 "input control is not implemented for this platform",
2469 ))
2470 }
2471
2472 async fn scroll_to(
2473 &self,
2474 _selector: &str,
2475 _options: ScrollOptions,
2476 ) -> Result<(), WebViewInputError> {
2477 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2478 {
2479 if self.inner.is_window_attached().await {
2480 return self.inner.scroll_to_inner(_selector, _options).await;
2481 }
2482 return self.scroll_to_via_js(_selector, None).await;
2483 }
2484 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2485 {
2486 return self.inner.scroll_to_inner(_selector, _options).await;
2487 }
2488 #[cfg(target_os = "android")]
2489 {
2490 return self.inner.scroll_to_inner(_selector, _options).await;
2491 }
2492 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
2493 {
2494 return self.scroll_to_via_js(_selector, None).await;
2495 }
2496 #[allow(unreachable_code)]
2497 Err(WebViewInputError::Unsupported(
2498 "input control is not implemented for this platform",
2499 ))
2500 }
2501}
2502
2503type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
2505
2506#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2507#[serde(rename_all = "snake_case")]
2508pub enum WebViewCreateStage {
2509 Requested,
2510 NativeCreated,
2511 ControllerAttached,
2512 Ready,
2513 Destroyed,
2514}
2515
2516#[derive(Debug, Clone, PartialEq, Eq)]
2517pub enum WebViewEvent {
2518 Stage(WebViewCreateStage),
2519 Failed {
2520 stage: WebViewCreateStage,
2521 error: WebViewError,
2522 },
2523}
2524
2525type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
2526
2527#[derive(Clone)]
2528pub struct WebViewEventSubscription {
2529 rx: watch::Receiver<WebViewEvent>,
2530}
2531
2532impl WebViewEventSubscription {
2533 pub fn current(&self) -> WebViewEvent {
2534 self.rx.borrow().clone()
2535 }
2536
2537 pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
2538 self.rx.changed().await.map_err(|_| {
2539 WebViewError::WebView("webview event channel unexpectedly closed".to_string())
2540 })?;
2541 Ok(self.current())
2542 }
2543}
2544
2545#[derive(Clone)]
2546pub struct WebViewSession {
2547 webtag: WebTag,
2548 event_rx: watch::Receiver<WebViewEvent>,
2549 ready_rx: watch::Receiver<WebViewReadyState>,
2550 signals: Arc<WebViewSessionSignals>,
2551}
2552
2553impl WebViewSession {
2554 pub fn webtag(&self) -> &WebTag {
2555 &self.webtag
2556 }
2557
2558 pub fn subscribe_events(&self) -> WebViewEventSubscription {
2559 WebViewEventSubscription {
2560 rx: self.event_rx.clone(),
2561 }
2562 }
2563
2564 pub fn current_event(&self) -> WebViewEvent {
2565 self.event_rx.borrow().clone()
2566 }
2567
2568 pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
2569 let mut rx = self.ready_rx.clone();
2570 loop {
2571 if let Some(result) = self.signals.terminal_result() {
2572 return result;
2573 }
2574 if let Some(result) = rx.borrow().clone() {
2575 return result;
2576 }
2577 if rx.changed().await.is_err() {
2578 if let Some(result) = self.signals.terminal_result() {
2579 return result;
2580 }
2581 return Err(WebViewError::WebView(
2582 "webview ready channel unexpectedly closed".to_string(),
2583 ));
2584 }
2585 }
2586 }
2587}
2588
2589struct WebViewSessionSignals {
2590 event_tx: watch::Sender<WebViewEvent>,
2591 ready_tx: watch::Sender<WebViewReadyState>,
2592 state: Mutex<WebViewSessionState>,
2593}
2594
2595#[derive(Default)]
2596struct WebViewSessionState {
2597 terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
2598 destroyed: bool,
2599}
2600
2601impl WebViewSessionSignals {
2602 fn new() -> Arc<Self> {
2603 let (event_tx, _event_rx) =
2604 watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
2605 let (ready_tx, _ready_rx) = watch::channel(None);
2606 Arc::new(Self {
2607 event_tx,
2608 ready_tx,
2609 state: Mutex::new(WebViewSessionState::default()),
2610 })
2611 }
2612
2613 fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
2614 WebViewSession {
2615 webtag,
2616 event_rx: self.event_tx.subscribe(),
2617 ready_rx: self.ready_tx.subscribe(),
2618 signals: Arc::clone(self),
2619 }
2620 }
2621
2622 fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
2623 let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
2624 state.terminal_result.clone()
2625 }
2626
2627 #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
2629 fn is_destroyed(&self) -> bool {
2630 let state = lock_or_recover(&self.state, "webview_session_state.is_destroyed");
2631 state.destroyed
2632 }
2633
2634 fn publish_result(
2635 &self,
2636 result: Result<Arc<WebView>, WebViewError>,
2637 stage_on_error: WebViewCreateStage,
2638 ) {
2639 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
2640 if state.destroyed || state.terminal_result.is_some() {
2641 return;
2642 }
2643 state.terminal_result = Some(result.clone());
2644 drop(state);
2645
2646 match result {
2647 Ok(webview) => {
2648 self.event_tx
2649 .send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
2650 self.event_tx
2651 .send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
2652 self.ready_tx.send_replace(Some(Ok(webview)));
2653 self.event_tx
2654 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
2655 }
2656 Err(error) => {
2657 self.ready_tx.send_replace(Some(Err(error.clone())));
2658 self.event_tx.send_replace(WebViewEvent::Failed {
2659 stage: stage_on_error,
2660 error,
2661 });
2662 }
2663 }
2664 }
2665
2666 fn publish_destroyed(&self) {
2667 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
2668 if state.destroyed {
2669 return;
2670 }
2671 state.destroyed = true;
2672 if state.terminal_result.is_none() {
2673 state.terminal_result = Some(Err(WebViewError::WebView(
2674 "webview destroyed before ready".to_string(),
2675 )));
2676 }
2677 let terminal_result = state.terminal_result.clone();
2678 drop(state);
2679
2680 self.event_tx
2681 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
2682 if let Some(result) = terminal_result {
2683 self.ready_tx.send_replace(Some(result));
2684 }
2685 }
2686}
2687
2688pub(crate) struct WebViewCreateSender {
2689 webtag: WebTag,
2690 signals: Arc<WebViewSessionSignals>,
2691 native_view_id: NativeWebViewId,
2692}
2693
2694impl WebViewCreateSender {
2695 fn new(webtag: WebTag, signals: Arc<WebViewSessionSignals>) -> Self {
2696 Self {
2697 webtag,
2698 signals,
2699 native_view_id: next_native_webview_id(),
2700 }
2701 }
2702
2703 pub(crate) const fn native_view_id(&self) -> NativeWebViewId {
2707 self.native_view_id
2708 }
2709
2710 pub(crate) fn succeed(self, webview: Arc<WebView>) {
2711 self.signals
2712 .publish_result(Ok(webview), WebViewCreateStage::Requested);
2713 }
2714
2715 pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
2716 if remove_session_signals_if_matches(&self.webtag, &self.signals) {
2717 crate::events::normalizer::destroy(&self.webtag);
2718 }
2719 self.signals.publish_result(Err(error), stage);
2720 }
2721
2722 #[cfg_attr(not(target_os = "android"), allow(dead_code))]
2726 pub(crate) fn cancel_superseded(self) {
2727 if remove_session_signals_if_matches(&self.webtag, &self.signals) {
2728 crate::events::normalizer::destroy(&self.webtag);
2729 }
2730 self.signals.publish_destroyed();
2731 }
2732
2733 #[cfg_attr(
2738 not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
2739 allow(dead_code)
2740 )]
2741 pub(crate) fn is_destroyed(&self) -> bool {
2742 self.signals.is_destroyed()
2743 }
2744}
2745
2746static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
2748
2749struct PendingCallbacksEntry {
2752 #[cfg(target_os = "android")]
2753 signals: Arc<WebViewSessionSignals>,
2754 callbacks: PendingCallbacks,
2755}
2756
2757static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacksEntry>>> = OnceLock::new();
2758static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
2759 OnceLock::new();
2760#[cfg(target_os = "windows")]
2761static WEBVIEW_CREATE_LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<Mutex<()>>>>> =
2762 OnceLock::new();
2763static DESIRED_PROXY_FOR_NEW_WEBVIEWS: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
2764static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2765
2766fn apply_http_proxy_platform(
2767 config: Option<&ProxyConfig>,
2768) -> Result<ProxyApplyReport, WebViewError> {
2769 #[cfg(target_os = "android")]
2770 {
2771 crate::android::apply_http_proxy(config)
2772 }
2773
2774 #[cfg(any(target_os = "ios", target_os = "macos"))]
2775 {
2776 crate::apple::apply_http_proxy(config)
2777 }
2778
2779 #[cfg(all(target_os = "linux", target_env = "ohos"))]
2780 {
2781 crate::harmony::apply_http_proxy(config)
2782 }
2783
2784 #[cfg(not(any(
2785 target_os = "android",
2786 target_os = "ios",
2787 target_os = "macos",
2788 all(target_os = "linux", target_env = "ohos")
2789 )))]
2790 {
2791 let _ = config;
2792 Ok(ProxyApplyReport::unsupported(
2793 "proxy is not supported on this platform",
2794 ))
2795 }
2796}
2797
2798pub fn configure_proxy_for_new_webviews(config: Option<ProxyConfig>) -> Result<(), WebViewError> {
2803 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
2804 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
2805
2806 let normalized_config = match config {
2807 Some(cfg) => Some(cfg.validate()?),
2808 None => None,
2809 };
2810
2811 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
2812 match state.write() {
2813 Ok(mut guard) => {
2814 *guard = normalized_config;
2815 }
2816 Err(poisoned) => {
2817 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2818 *poisoned.into_inner() = normalized_config;
2819 }
2820 }
2821 Ok(())
2822}
2823
2824pub fn apply_proxy_to_current_runtime(
2829 config: Option<ProxyConfig>,
2830) -> Result<ProxyApplyReport, WebViewError> {
2831 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
2832 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
2833
2834 let normalized_config = match config {
2835 Some(cfg) => Some(cfg.validate()?),
2836 None => None,
2837 };
2838
2839 let report = apply_http_proxy_platform(normalized_config.as_ref())?;
2840
2841 if matches!(
2842 report.status,
2843 ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
2844 ) {
2845 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
2846 match state.write() {
2847 Ok(mut guard) => {
2848 *guard = normalized_config;
2849 }
2850 Err(poisoned) => {
2851 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2852 *poisoned.into_inner() = normalized_config;
2853 }
2854 }
2855 }
2856
2857 Ok(report)
2858}
2859
2860pub fn configured_proxy_for_new_webviews() -> Option<ProxyConfig> {
2862 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get()?;
2863 match state.read() {
2864 Ok(guard) => guard.clone(),
2865 Err(poisoned) => {
2866 log::error!("RwLock poisoned at webview_desired_proxy.read, recovering");
2867 poisoned.into_inner().clone()
2868 }
2869 }
2870}
2871
2872fn clear_pending_callbacks(webtag: &WebTag) {
2873 if let Some(pending) = PENDING_CALLBACKS.get()
2874 && let Ok(mut map) = pending.lock()
2875 {
2876 map.remove(webtag.key());
2877 }
2878}
2879
2880fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
2881 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2882 let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
2883 guard.insert(webtag.key().to_string(), signals);
2884}
2885
2886fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
2887 let sessions = WEBVIEW_SESSIONS.get()?;
2888 let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
2889 guard.remove(webtag.key())
2890}
2891
2892fn remove_session_signals_if_matches(
2893 webtag: &WebTag,
2894 expected: &Arc<WebViewSessionSignals>,
2895) -> bool {
2896 let Some(sessions) = WEBVIEW_SESSIONS.get() else {
2897 return false;
2898 };
2899 let mut guard = lock_or_recover(sessions, "webview_sessions.remove_if_matches");
2900 if guard
2901 .get(webtag.key())
2902 .is_some_and(|current| Arc::ptr_eq(current, expected))
2903 {
2904 guard.remove(webtag.key());
2905 true
2906 } else {
2907 false
2908 }
2909}
2910
2911#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2914pub struct WebTag(String);
2915
2916impl std::fmt::Display for WebTag {
2917 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2918 write!(f, "{}", self.0)
2919 }
2920}
2921
2922impl WebTag {
2923 pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
2924 let mut tag = format!("{}:{}", appid, path);
2925 if let Some(session) = session_id {
2926 tag.push('#');
2927 tag.push_str(&session.to_string());
2928 }
2929 Self(tag)
2930 }
2931
2932 pub fn as_str(&self) -> &str {
2933 &self.0
2934 }
2935
2936 pub fn key(&self) -> &str {
2940 &self.0
2941 }
2942
2943 pub fn extract_appid(&self) -> String {
2945 self.0.split(':').next().unwrap_or("").to_string()
2946 }
2947
2948 pub fn extract_parts(&self) -> (String, String) {
2951 if let Some((appid, path_with_session)) = self.0.split_once(':') {
2952 let path = path_with_session
2953 .split('#')
2954 .next()
2955 .unwrap_or(path_with_session);
2956 (appid.to_string(), path.to_string())
2957 } else {
2958 log::error!("Invalid webtag format: {}", self.0);
2959 ("".to_string(), self.0.clone())
2960 }
2961 }
2962
2963 pub fn session_id(&self) -> Option<u64> {
2965 self.0
2966 .split('#')
2967 .next_back()
2968 .and_then(|raw| raw.parse::<u64>().ok())
2969 }
2970
2971 #[cfg_attr(
2975 any(not(target_os = "windows"), target_os = "windows"),
2976 allow(dead_code)
2977 )]
2978 pub(crate) fn group_key(&self) -> String {
2979 let Some((appid, path_with_session)) = self.0.split_once(':') else {
2980 return self.0.clone();
2981 };
2982 let session = path_with_session
2983 .rsplit_once('#')
2984 .and_then(|(_, suffix)| suffix.parse::<u64>().ok())
2985 .map(|session| session.to_string())
2986 .unwrap_or_else(|| "0".to_string());
2987 format!("{appid}#{session}")
2988 }
2989
2990 fn key_path(&self) -> String {
2991 let Some((_, path_with_suffix)) = self.0.split_once(':') else {
2992 return self.0.clone();
2993 };
2994 if self.session_id().is_some()
2995 && let Some((path, _)) = path_with_suffix.rsplit_once('#')
2996 {
2997 return path.to_string();
2998 }
2999 path_with_suffix.to_string()
3000 }
3001}
3002
3003impl From<&str> for WebTag {
3004 fn from(webtag_str: &str) -> Self {
3005 Self(webtag_str.to_string())
3006 }
3007}
3008
3009fn request_create_webview(
3010 webtag: &WebTag,
3011 sender: WebViewCreateSender,
3012 options: WebViewCreateOptions,
3013) {
3014 let (appid, _) = webtag.extract_parts();
3015 let (effective_options, pending_callbacks) = match options.normalize() {
3016 Ok(value) => value,
3017 Err(error) => {
3018 sender.fail(WebViewCreateStage::Requested, error);
3019 return;
3020 }
3021 };
3022
3023 log::info!(
3024 "Creating WebView for key={} profile={:?} data_mode={:?} schemes={:?}",
3025 webtag.key(),
3026 effective_options.profile,
3027 effective_options.data_mode,
3028 effective_options.registered_schemes,
3029 );
3030
3031 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
3033
3034 if let Ok(webviews) = instances.lock()
3039 && let Some(existing_webview) = webviews.get(webtag.key())
3040 {
3041 if existing_webview.effective_options() != &effective_options {
3042 sender.fail(
3043 WebViewCreateStage::Requested,
3044 WebViewError::InvalidCreateOptions(format!(
3045 "webview already exists with different options: key={} existing={:?} requested={:?}",
3046 webtag.key(),
3047 existing_webview.effective_options(),
3048 effective_options
3049 )),
3050 );
3051 return;
3052 }
3053
3054 if pending_callbacks.has_any() {
3055 sender.fail(
3056 WebViewCreateStage::Requested,
3057 WebViewError::InvalidCreateOptions(format!(
3058 "webview already exists and callback registrations are immutable: key={} options={:?}",
3059 webtag.key(),
3060 existing_webview.effective_options()
3061 )),
3062 );
3063 log::warn!(
3064 "Rejected recreate with callbacks for existing webview key={} options={:?}",
3065 webtag.key(),
3066 existing_webview.effective_options()
3067 );
3068 return;
3069 }
3070
3071 log::info!("WebView already exists, reusing: {}", webtag.key());
3072 sender.succeed(existing_webview.clone());
3073 return;
3074 }
3075
3076 clear_pending_callbacks(webtag);
3078
3079 if pending_callbacks.has_any() {
3081 let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
3082 if let Ok(mut map) = pending.lock() {
3083 map.insert(
3084 webtag.key().to_string(),
3085 PendingCallbacksEntry {
3086 #[cfg(target_os = "android")]
3087 signals: Arc::clone(&sender.signals),
3088 callbacks: pending_callbacks,
3089 },
3090 );
3091 }
3092 }
3093
3094 WebViewInner::create(
3096 &appid,
3097 &webtag.key_path(),
3098 webtag.session_id(),
3099 effective_options,
3100 sender,
3101 );
3102}
3103
3104fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
3105 #[cfg(target_os = "windows")]
3110 let create_lock = windows_webview_create_lock(webtag.key());
3111 #[cfg(target_os = "windows")]
3112 let _create_guard = lock_or_recover(&create_lock, "windows_webview_create_lock");
3113
3114 let signals = WebViewSessionSignals::new();
3115 let session = signals.subscribe(webtag.clone());
3116 let sender = WebViewCreateSender::new(webtag.clone(), signals.clone());
3117 replace_session_signals(&webtag, signals);
3118 crate::events::normalizer::begin(&webtag, sender.native_view_id());
3119 request_create_webview(&webtag, sender, options);
3120 session
3121}
3122
3123#[cfg(target_os = "windows")]
3124fn windows_webview_create_lock(webtag_key: &str) -> Arc<Mutex<()>> {
3125 let locks = WEBVIEW_CREATE_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
3126 let mut locks = lock_or_recover(locks, "windows_webview_create_locks");
3127 locks.retain(|_, lock| lock.strong_count() > 0);
3128 if let Some(lock) = locks.get(webtag_key).and_then(std::sync::Weak::upgrade) {
3129 return lock;
3130 }
3131 let lock = Arc::new(Mutex::new(()));
3132 locks.insert(webtag_key.to_string(), Arc::downgrade(&lock));
3133 lock
3134}
3135
3136#[cfg_attr(target_os = "android", allow(dead_code))]
3137pub(crate) fn register_webview(webview: Arc<WebView>) {
3138 let webtag = webview.webtag();
3139 crate::events::normalizer::bind_native_view(&webtag, webview.native_view_id());
3140
3141 if let Some(pending) = PENDING_CALLBACKS.get()
3143 && let Ok(mut map) = pending.lock()
3144 && let Some(entry) = map.remove(webtag.key())
3145 {
3146 let callbacks = entry.callbacks;
3147 log::info!(
3148 "Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, file_chooser={}, delegate={})",
3149 webtag.key(),
3150 callbacks.scheme_handlers.len(),
3151 callbacks.navigation_handler.is_some(),
3152 callbacks.new_window_handler.is_some(),
3153 callbacks.download_handler.is_some(),
3154 callbacks.file_chooser_handler.is_some(),
3155 callbacks.delegate.is_some()
3156 );
3157 webview.install_callbacks(callbacks);
3158 }
3159
3160 if let Some(instances) = WEBVIEW_INSTANCES.get()
3161 && let Ok(mut webviews) = instances.lock()
3162 {
3163 webviews.insert(webtag.key().to_string(), webview.clone());
3164 log::info!("WebView created and stored: {}", webtag.key());
3165 }
3166}
3167
3168#[cfg(target_os = "android")]
3169pub(crate) fn register_android_webview_if_current(
3170 webview: Arc<WebView>,
3171 sender: &WebViewCreateSender,
3172) -> bool {
3173 let webtag = webview.webtag();
3174 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
3175 let session_guard = lock_or_recover(sessions, "webview_sessions.register_android");
3176 if !session_guard
3177 .get(webtag.key())
3178 .is_some_and(|current| Arc::ptr_eq(current, &sender.signals))
3179 {
3180 return false;
3181 }
3182
3183 if let Some(pending) = PENDING_CALLBACKS.get()
3184 && let Ok(mut map) = pending.lock()
3185 && map
3186 .get(webtag.key())
3187 .is_some_and(|entry| Arc::ptr_eq(&entry.signals, &sender.signals))
3188 && let Some(entry) = map.remove(webtag.key())
3189 {
3190 webview.install_callbacks(entry.callbacks);
3191 }
3192
3193 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
3194 let mut webviews = lock_or_recover(instances, "webview_instances.register_android");
3195 crate::events::normalizer::bind_native_view(&webtag, webview.native_view_id());
3196 webviews.insert(webtag.key().to_string(), webview);
3197 true
3198}
3199
3200pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
3202 if let Some(instances) = WEBVIEW_INSTANCES.get() {
3203 if let Ok(webviews) = instances.lock() {
3204 webviews.get(webtag.key()).cloned()
3205 } else {
3206 None
3207 }
3208 } else {
3209 None
3210 }
3211}
3212
3213#[allow(dead_code)]
3219pub(crate) fn find_webview_by_native_view_id(
3220 webtag: &WebTag,
3221 native_view_id: NativeWebViewId,
3222) -> Option<Arc<WebView>> {
3223 find_webview(webtag).filter(|webview| webview.native_view_id() == native_view_id)
3224}
3225
3226#[cfg(target_os = "windows")]
3227pub(crate) fn first_browser_webview() -> Option<Arc<WebView>> {
3228 WEBVIEW_INSTANCES
3229 .get()
3230 .and_then(|instances| instances.lock().ok())
3231 .and_then(|webviews| {
3232 webviews
3233 .values()
3234 .find(|webview| {
3235 webview.effective_options.profile == SecurityProfile::BrowserRelaxed
3236 })
3237 .cloned()
3238 })
3239}
3240
3241pub(crate) fn list_webviews() -> Vec<WebTag> {
3242 if let Some(instances) = WEBVIEW_INSTANCES.get()
3243 && let Ok(webviews) = instances.lock()
3244 {
3245 let mut tags: Vec<WebTag> = webviews.values().map(|webview| webview.webtag()).collect();
3246 tags.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3247 return tags;
3248 }
3249 Vec::new()
3250}
3251
3252pub(crate) fn find_webview_delegate_by_native_view_id(
3256 webtag: &WebTag,
3257 native_view_id: NativeWebViewId,
3258) -> Option<Arc<dyn WebViewDelegate>> {
3259 find_webview_by_native_view_id(webtag, native_view_id)
3260 .and_then(|webview| webview.get_delegate())
3261}
3262
3263fn remove_arc_if_matches<T>(
3264 entries: &mut HashMap<String, Arc<T>>,
3265 key: &str,
3266 expected: &Arc<T>,
3267) -> Option<Arc<T>> {
3268 entries
3269 .get(key)
3270 .is_some_and(|current| Arc::ptr_eq(current, expected))
3271 .then(|| entries.remove(key))
3272 .flatten()
3273}
3274
3275pub(crate) fn destroy_webview_if_matches(webtag: &WebTag, expected: &Arc<WebView>) -> bool {
3279 expected.close_message_ingress();
3283 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
3284 && let Ok(mut webviews) = instances.lock()
3285 {
3286 remove_arc_if_matches(&mut webviews, webtag.key(), expected)
3287 } else {
3288 None
3289 };
3290 if let Some(webview) = removed {
3291 #[cfg(target_os = "windows")]
3292 {
3293 let _ = webview.inner.set_content_visible(false);
3294 webview.inner.request_shutdown();
3295 }
3296 webview.remove_delegate();
3297 true
3298 } else {
3299 false
3300 }
3301}
3302
3303pub(crate) fn destroy_current_webview(webtag: &WebTag) {
3308 if let Some(webview) = find_webview(webtag) {
3312 webview.close_message_ingress();
3313 }
3314 crate::events::normalizer::destroy(webtag);
3317 if let Some(signals) = remove_session_signals(webtag) {
3323 signals.publish_destroyed();
3324 }
3325 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
3326 && let Ok(mut webviews) = instances.lock()
3327 {
3328 webviews.remove(webtag.key())
3329 } else {
3330 None
3331 };
3332 if let Some(webview) = removed {
3333 webview.close_message_ingress();
3334 #[cfg(target_os = "windows")]
3338 {
3339 let _ = webview.inner.set_content_visible(false);
3340 webview.inner.request_shutdown();
3344 }
3345 webview.remove_delegate();
3346 }
3347 clear_pending_callbacks(webtag);
3348}
3349
3350#[cfg(test)]
3351mod tests {
3352 use super::{
3353 MAX_PENDING_WEB_MESSAGE_BYTES, MAX_PENDING_WEB_MESSAGES, MAX_WEB_MESSAGE_BYTES,
3354 PlatformConsoleBackend, PlatformConsoleDelivery, SecurityProfile, WEBVIEW_SESSIONS,
3355 WebMessageEnqueue, WebMessageIngress, WebMessageRejectReason, WebTag, WebViewCreateOptions,
3356 WebViewCreateSender, WebViewSessionSignals, block_on_scheme_future, document_port_context,
3357 next_native_webview_id, platform_console_delivery, remove_arc_if_matches,
3358 remove_session_signals_if_matches, replace_session_signals, should_sample_rejection,
3359 snapshot_web_message_context, web_message_bytes_within_limit,
3360 web_message_utf16_units_within_limit,
3361 };
3362 use crate::{
3363 ContextualSchemeRequest, DocumentBinding, IncomingWebMessage, NativeWebViewId,
3364 SchemeOutcome, SchemeRequestFrame, WebMessageContext, WebMessageFrame, WebMessageSource,
3365 WebMessageTransport,
3366 };
3367 use std::collections::HashMap;
3368 use std::sync::{Arc, Mutex, mpsc};
3369 use std::thread;
3370
3371 fn message(body: &str, native_view: crate::NativeWebViewId) -> IncomingWebMessage {
3372 IncomingWebMessage::new(
3373 body.to_string(),
3374 WebMessageContext::new(
3375 native_view,
3376 DocumentBinding::Unbound,
3377 WebMessageFrame::Unproven,
3378 WebMessageTransport::Other,
3379 WebMessageSource::unavailable(),
3380 ),
3381 )
3382 }
3383
3384 fn assert_browser_console_is_bound(backend: PlatformConsoleBackend) {
3385 assert_eq!(
3386 platform_console_delivery(SecurityProfile::BrowserRelaxed, backend),
3387 PlatformConsoleDelivery::RequiredV3Envelope
3388 );
3389 assert_eq!(
3390 platform_console_delivery(SecurityProfile::StrictDefault, backend),
3391 PlatformConsoleDelivery::DirectDelegate
3392 );
3393 }
3394
3395 #[test]
3396 fn apple_browser_console_requires_v3_envelope() {
3397 assert_browser_console_is_bound(PlatformConsoleBackend::Apple);
3398 }
3399
3400 #[test]
3401 fn android_browser_console_requires_v3_envelope() {
3402 assert_browser_console_is_bound(PlatformConsoleBackend::Android);
3403 }
3404
3405 #[test]
3406 fn harmony_browser_console_requires_v3_envelope() {
3407 assert_browser_console_is_bound(PlatformConsoleBackend::Harmony);
3408 }
3409
3410 #[test]
3411 fn windows_browser_console_requires_v3_envelope() {
3412 assert_browser_console_is_bound(PlatformConsoleBackend::Windows);
3413 }
3414
3415 #[test]
3416 fn native_webview_ids_are_not_reused_across_logical_tag_reuse() {
3417 let retired = next_native_webview_id();
3418 let replacement = next_native_webview_id();
3419 assert_ne!(retired, replacement);
3420
3421 let late_message = message("late", retired);
3422 assert_ne!(late_message.context().native_view(), replacement);
3423 assert_eq!(late_message.context().document(), DocumentBinding::Unbound);
3424 }
3425
3426 #[test]
3427 fn document_port_top_level_proof_requires_current_generation() {
3428 let native_view = next_native_webview_id();
3429 let current = crate::DocumentGeneration::new(42);
3430 let stale = crate::DocumentGeneration::new(41);
3431
3432 let context = document_port_context(
3433 native_view,
3434 DocumentBinding::Bound(current),
3435 current,
3436 WebMessageTransport::AndroidMessagePort,
3437 WebMessageSource::unavailable(),
3438 )
3439 .expect("the exact document port proves the top-level document");
3440 assert_eq!(context.frame(), WebMessageFrame::TopLevel);
3441 assert_eq!(context.document(), DocumentBinding::Bound(current));
3442
3443 assert!(
3444 document_port_context(
3445 native_view,
3446 DocumentBinding::Bound(current),
3447 stale,
3448 WebMessageTransport::AndroidMessagePort,
3449 WebMessageSource::unavailable(),
3450 )
3451 .is_none()
3452 );
3453 assert!(
3454 document_port_context(
3455 native_view,
3456 DocumentBinding::Bound(current),
3457 current,
3458 WebMessageTransport::AndroidJavascriptInterface,
3459 WebMessageSource::unavailable(),
3460 )
3461 .is_none()
3462 );
3463
3464 assert_eq!(
3466 document_port_context(
3467 native_view,
3468 DocumentBinding::Bound(current),
3469 current,
3470 WebMessageTransport::HarmonyMessagePort,
3471 WebMessageSource::unavailable(),
3472 )
3473 .expect("a current Harmony port proves the top-level document")
3474 .frame(),
3475 WebMessageFrame::TopLevel
3476 );
3477 assert!(
3478 document_port_context(
3479 native_view,
3480 DocumentBinding::Bound(current),
3481 stale,
3482 WebMessageTransport::HarmonyMessagePort,
3483 WebMessageSource::unavailable(),
3484 )
3485 .is_none()
3486 );
3487 }
3488
3489 #[test]
3490 fn legacy_scheme_handler_runs_from_contextual_ingress() {
3491 let options = WebViewCreateOptions::strict().on_scheme("lx", |request| async move {
3492 assert_eq!(request.uri(), "lx://app/index.html");
3493 SchemeOutcome::PassThrough
3494 });
3495 let (_, callbacks) = options.normalize().unwrap();
3496 let handler = callbacks.scheme_handlers.get("lx").unwrap();
3497 let outcome = block_on_scheme_future(handler(ContextualSchemeRequest::new(
3498 http::Request::builder()
3499 .uri("lx://app/index.html")
3500 .body(Vec::new())
3501 .unwrap(),
3502 NativeWebViewId::new(101),
3503 SchemeRequestFrame::TopLevelDocument,
3504 )));
3505 assert!(matches!(outcome, SchemeOutcome::PassThrough));
3506 }
3507
3508 #[test]
3509 fn ingress_snapshots_document_binding_at_enqueue_time() {
3510 let webtag = WebTag::new("test-app", "binding-snapshot", Some(1));
3511 let native_view = next_native_webview_id();
3512 crate::events::normalizer::begin(&webtag, native_view);
3513 crate::events::normalizer::submit(
3514 &webtag,
3515 native_view,
3516 crate::events::normalizer::NativeSignal::NavigationStarted {
3517 key: Some(71),
3518 url: "https://first/".into(),
3519 },
3520 );
3521 crate::events::normalizer::submit(
3522 &webtag,
3523 native_view,
3524 crate::events::normalizer::NativeSignal::DocumentCommitted { key: Some(71) },
3525 );
3526
3527 let ingress = WebMessageIngress::default();
3528 let bound = IncomingWebMessage::new(
3529 "bound".to_string(),
3530 snapshot_web_message_context(
3531 native_view,
3532 WebMessageFrame::Unproven,
3533 WebMessageTransport::Other,
3534 WebMessageSource::unavailable(),
3535 ),
3536 );
3537 assert_eq!(
3538 bound.context().document(),
3539 DocumentBinding::Bound(crate::DocumentGeneration::new(1))
3540 );
3541 assert_eq!(ingress.enqueue(bound), WebMessageEnqueue::Schedule);
3542
3543 crate::events::normalizer::submit(
3544 &webtag,
3545 native_view,
3546 crate::events::normalizer::NativeSignal::NavigationStarted {
3547 key: Some(72),
3548 url: "https://second/".into(),
3549 },
3550 );
3551 let revoked = IncomingWebMessage::new(
3552 "revoked".to_string(),
3553 snapshot_web_message_context(
3554 native_view,
3555 WebMessageFrame::Unproven,
3556 WebMessageTransport::Other,
3557 WebMessageSource::unavailable(),
3558 ),
3559 );
3560 assert_eq!(revoked.context().document(), DocumentBinding::Unbound);
3561 assert_eq!(ingress.enqueue(revoked), WebMessageEnqueue::Queued);
3562
3563 let mut bindings = Vec::new();
3564 while let Some(message) = ingress.begin_delivery() {
3565 bindings.push(message.context().document());
3566 ingress.finish_delivery();
3567 }
3568 assert_eq!(
3569 bindings,
3570 vec![
3571 DocumentBinding::Bound(crate::DocumentGeneration::new(1)),
3572 DocumentBinding::Unbound,
3573 ]
3574 );
3575 crate::events::normalizer::destroy(&webtag);
3576 }
3577
3578 #[test]
3579 fn message_ingress_preserves_fifo_across_reentrant_enqueue() {
3580 let ingress = WebMessageIngress::default();
3581 let native_view = next_native_webview_id();
3582 let delivered = Mutex::new(Vec::new());
3583
3584 assert_eq!(
3585 ingress.enqueue(message("first", native_view)),
3586 WebMessageEnqueue::Schedule
3587 );
3588 while let Some(incoming) = ingress.begin_delivery() {
3589 let body = incoming.body().to_string();
3590 delivered.lock().unwrap().push(body.clone());
3591 if body == "first" {
3592 assert_eq!(
3593 ingress.enqueue(message("second", native_view)),
3594 WebMessageEnqueue::Queued
3595 );
3596 assert_eq!(
3597 ingress.enqueue(message("third", native_view)),
3598 WebMessageEnqueue::Queued
3599 );
3600 }
3601 ingress.finish_delivery();
3602 }
3603
3604 assert_eq!(*delivered.lock().unwrap(), vec!["first", "second", "third"]);
3605 }
3606
3607 #[test]
3608 fn message_ingress_bounds_untrusted_backlog_without_reordering_accepted_messages() {
3609 let ingress = WebMessageIngress::default();
3610 let native_view = next_native_webview_id();
3611 assert_eq!(
3612 ingress.enqueue(message("0", native_view)),
3613 WebMessageEnqueue::Schedule
3614 );
3615 for index in 1..MAX_PENDING_WEB_MESSAGES {
3616 assert_eq!(
3617 ingress.enqueue(message(&index.to_string(), native_view)),
3618 WebMessageEnqueue::Queued
3619 );
3620 }
3621 assert_eq!(
3622 ingress.enqueue(message("overflow", native_view)),
3623 WebMessageEnqueue::Rejected {
3624 reason: WebMessageRejectReason::QueueCountLimit,
3625 count: 1,
3626 }
3627 );
3628
3629 let mut accepted = Vec::new();
3630 while let Some(incoming) = ingress.begin_delivery() {
3631 accepted.push(incoming.body().to_owned());
3632 ingress.finish_delivery();
3633 }
3634 assert_eq!(accepted.len(), MAX_PENDING_WEB_MESSAGES);
3635 assert_eq!(accepted.first().map(String::as_str), Some("0"));
3636 let last = (MAX_PENDING_WEB_MESSAGES - 1).to_string();
3637 assert_eq!(accepted.last().map(String::as_str), Some(last.as_str()));
3638 }
3639
3640 #[test]
3641 fn message_ingress_rejects_an_oversized_message_before_queueing() {
3642 let ingress = WebMessageIngress::default();
3643 let native_view = next_native_webview_id();
3644 let oversized = "x".repeat(MAX_WEB_MESSAGE_BYTES + 1);
3645
3646 assert_eq!(
3647 ingress.enqueue(message(&oversized, native_view)),
3648 WebMessageEnqueue::Rejected {
3649 reason: WebMessageRejectReason::MessageTooLarge,
3650 count: 1,
3651 }
3652 );
3653 assert_eq!(ingress.queued_bytes(), 0);
3654 assert!(ingress.begin_delivery().is_none());
3655 }
3656
3657 #[test]
3658 fn raw_web_message_byte_cap_includes_the_exact_boundary() {
3659 assert!(web_message_bytes_within_limit(MAX_WEB_MESSAGE_BYTES - 1));
3660 assert!(web_message_bytes_within_limit(MAX_WEB_MESSAGE_BYTES));
3661 assert!(!web_message_bytes_within_limit(MAX_WEB_MESSAGE_BYTES + 1));
3662 assert!(web_message_utf16_units_within_limit(MAX_WEB_MESSAGE_BYTES));
3663 assert!(!web_message_utf16_units_within_limit(
3664 MAX_WEB_MESSAGE_BYTES + 1
3665 ));
3666 assert!(!web_message_bytes_within_limit(usize::MAX));
3667 }
3668
3669 #[test]
3670 fn message_ingress_byte_budget_is_released_on_delivery_and_close() {
3671 let ingress = WebMessageIngress::default();
3672 let native_view = next_native_webview_id();
3673 let chunk = "x".repeat(MAX_WEB_MESSAGE_BYTES);
3674 let accepted = MAX_PENDING_WEB_MESSAGE_BYTES / MAX_WEB_MESSAGE_BYTES;
3675
3676 for index in 0..accepted {
3677 assert_eq!(
3678 ingress.enqueue(message(&chunk, native_view)),
3679 if index == 0 {
3680 WebMessageEnqueue::Schedule
3681 } else {
3682 WebMessageEnqueue::Queued
3683 }
3684 );
3685 }
3686 assert_eq!(ingress.queued_bytes(), MAX_PENDING_WEB_MESSAGE_BYTES);
3687 assert_eq!(
3688 ingress.enqueue(message("overflow", native_view)),
3689 WebMessageEnqueue::Rejected {
3690 reason: WebMessageRejectReason::QueueByteLimit,
3691 count: 1,
3692 }
3693 );
3694
3695 let first = ingress.begin_delivery().expect("first queued message");
3696 assert_eq!(first.body().len(), MAX_WEB_MESSAGE_BYTES);
3697 assert_eq!(
3698 ingress.queued_bytes(),
3699 MAX_PENDING_WEB_MESSAGE_BYTES - MAX_WEB_MESSAGE_BYTES
3700 );
3701 ingress.finish_delivery();
3702 assert_eq!(
3703 ingress.enqueue(message(&chunk, native_view)),
3704 WebMessageEnqueue::Queued
3705 );
3706 ingress.close();
3707 assert_eq!(ingress.queued_bytes(), 0);
3708 assert!(ingress.begin_delivery().is_none());
3709 }
3710
3711 #[test]
3712 fn ingress_rejection_counters_are_reason_coded_and_logs_are_sampled() {
3713 let ingress = WebMessageIngress::default();
3714 let native_view = next_native_webview_id();
3715 let oversized = "x".repeat(MAX_WEB_MESSAGE_BYTES + 1);
3716 assert_eq!(
3717 ingress.reject(WebMessageRejectReason::MessageTooLarge),
3718 WebMessageEnqueue::Rejected {
3719 reason: WebMessageRejectReason::MessageTooLarge,
3720 count: 1,
3721 }
3722 );
3723 for expected_count in 2..=4 {
3724 assert_eq!(
3725 ingress.enqueue(message(&oversized, native_view)),
3726 WebMessageEnqueue::Rejected {
3727 reason: WebMessageRejectReason::MessageTooLarge,
3728 count: expected_count,
3729 }
3730 );
3731 }
3732 assert_eq!(
3733 ingress.rejection_count(WebMessageRejectReason::MessageTooLarge),
3734 4
3735 );
3736 assert!(should_sample_rejection(1));
3737 assert!(should_sample_rejection(2));
3738 assert!(!should_sample_rejection(3));
3739 assert!(should_sample_rejection(4));
3740 }
3741
3742 #[test]
3743 fn message_ingress_preserves_fifo_across_producer_threads() {
3744 let ingress = Arc::new(WebMessageIngress::default());
3745 let native_view = next_native_webview_id();
3746 let (a_to_b_tx, a_to_b_rx) = mpsc::channel();
3747 let (b_to_a_tx, b_to_a_rx) = mpsc::channel();
3748
3749 let a_ingress = Arc::clone(&ingress);
3750 let producer_a = thread::spawn(move || {
3751 assert_eq!(
3752 a_ingress.enqueue(message("0", native_view)),
3753 WebMessageEnqueue::Schedule
3754 );
3755 a_to_b_tx.send(()).unwrap();
3756 for value in (2..64).step_by(2) {
3757 b_to_a_rx.recv().unwrap();
3758 assert_eq!(
3759 a_ingress.enqueue(message(&value.to_string(), native_view)),
3760 WebMessageEnqueue::Queued
3761 );
3762 a_to_b_tx.send(()).unwrap();
3763 }
3764 });
3765
3766 let b_ingress = Arc::clone(&ingress);
3767 let producer_b = thread::spawn(move || {
3768 for value in (1..64).step_by(2) {
3769 a_to_b_rx.recv().unwrap();
3770 assert_eq!(
3771 b_ingress.enqueue(message(&value.to_string(), native_view)),
3772 WebMessageEnqueue::Queued
3773 );
3774 if value != 63 {
3775 b_to_a_tx.send(()).unwrap();
3776 }
3777 }
3778 });
3779
3780 producer_a.join().unwrap();
3781 producer_b.join().unwrap();
3782
3783 let mut accepted = Vec::new();
3784 while let Some(incoming) = ingress.begin_delivery() {
3785 accepted.push(incoming.body().to_owned());
3786 ingress.finish_delivery();
3787 }
3788 assert_eq!(
3789 accepted,
3790 (0..64).map(|value| value.to_string()).collect::<Vec<_>>()
3791 );
3792 }
3793
3794 #[test]
3795 fn destroying_ingress_discards_queued_messages_but_allows_admitted_delivery() {
3796 let ingress = Arc::new(WebMessageIngress::default());
3797 let native_view = next_native_webview_id();
3798 assert_eq!(
3799 ingress.enqueue(message("in-flight", native_view)),
3800 WebMessageEnqueue::Schedule
3801 );
3802 assert_eq!(
3803 ingress.enqueue(message("queued", native_view)),
3804 WebMessageEnqueue::Queued
3805 );
3806
3807 let delivered = Mutex::new(Vec::new());
3808 let ingress_for_delivery = Arc::clone(&ingress);
3809 ingress.drain(|incoming| {
3810 delivered.lock().unwrap().push(incoming.body().to_owned());
3811 ingress_for_delivery.close();
3812 assert_eq!(
3813 ingress_for_delivery.enqueue(message("after-close", native_view)),
3814 WebMessageEnqueue::Rejected {
3815 reason: WebMessageRejectReason::Closed,
3816 count: 1,
3817 }
3818 );
3819 });
3820
3821 assert_eq!(*delivered.lock().unwrap(), vec!["in-flight"]);
3822 assert!(ingress.begin_delivery().is_none());
3823 }
3824
3825 #[test]
3826 fn closing_before_registry_removal_prevents_queued_message_admission() {
3827 let ingress = WebMessageIngress::default();
3828 let native_view = next_native_webview_id();
3829 assert_eq!(
3830 ingress.enqueue(message("queued", native_view)),
3831 WebMessageEnqueue::Schedule
3832 );
3833
3834 ingress.close();
3837
3838 assert!(ingress.begin_delivery().is_none());
3839 assert_eq!(
3840 ingress.enqueue(message("late", native_view)),
3841 WebMessageEnqueue::Rejected {
3842 reason: WebMessageRejectReason::Closed,
3843 count: 1,
3844 }
3845 );
3846 }
3847
3848 #[test]
3849 fn delegate_panic_does_not_stall_following_messages() {
3850 let ingress = WebMessageIngress::default();
3851 let native_view = next_native_webview_id();
3852 assert_eq!(
3853 ingress.enqueue(message("panic", native_view)),
3854 WebMessageEnqueue::Schedule
3855 );
3856 assert_eq!(
3857 ingress.enqueue(message("after", native_view)),
3858 WebMessageEnqueue::Queued
3859 );
3860
3861 let delivered = Mutex::new(Vec::new());
3862 ingress.drain(|incoming| {
3863 if incoming.body() == "panic" {
3864 panic!("test delegate panic");
3865 }
3866 delivered.lock().unwrap().push(incoming.body().to_owned());
3867 });
3868
3869 assert_eq!(*delivered.lock().unwrap(), vec!["after"]);
3870 assert_eq!(
3871 ingress.enqueue(message("recovered", native_view)),
3872 WebMessageEnqueue::Schedule
3873 );
3874 }
3875
3876 #[test]
3877 fn conditional_instance_removal_uses_arc_identity() {
3878 let current = Arc::new(7_u8);
3879 let same_value_different_instance = Arc::new(7_u8);
3880 let mut entries = HashMap::from([("tab".to_string(), current.clone())]);
3881
3882 assert!(
3883 remove_arc_if_matches(&mut entries, "tab", &same_value_different_instance).is_none()
3884 );
3885 assert!(Arc::ptr_eq(entries.get("tab").unwrap(), ¤t));
3886
3887 let removed = remove_arc_if_matches(&mut entries, "tab", ¤t).unwrap();
3888 assert!(Arc::ptr_eq(&removed, ¤t));
3889 assert!(!entries.contains_key("tab"));
3890 }
3891
3892 #[test]
3893 fn superseded_sender_completes_without_removing_current_generation() {
3894 let webtag = WebTag::from("test:pages/superseded#9173");
3895 let superseded = WebViewSessionSignals::new();
3896 let current = WebViewSessionSignals::new();
3897 replace_session_signals(&webtag, current.clone());
3898
3899 WebViewCreateSender::new(webtag.clone(), superseded.clone()).cancel_superseded();
3900
3901 assert!(
3902 superseded
3903 .terminal_result()
3904 .is_some_and(|result| result.is_err())
3905 );
3906 assert!(current.terminal_result().is_none());
3907 let sessions = WEBVIEW_SESSIONS.get().unwrap().lock().unwrap();
3908 assert!(
3909 sessions
3910 .get(webtag.key())
3911 .is_some_and(|signals| Arc::ptr_eq(signals, ¤t))
3912 );
3913 drop(sessions);
3914 assert!(remove_session_signals_if_matches(&webtag, ¤t));
3915 }
3916}