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 hit = document.elementFromPoint(rect.left + rect.width/2, rect.top + rect.height/2); \
1872 if (!hit || !(hit === el || el.contains(hit))) return {{ ok:false, error:'element is obscured', interactable:false }}; \
1873 const tag = (el.tagName || '').toLowerCase(); \
1874 if (tag.indexOf('lx-') === 0) {{ \
1875 el.setAttribute('focus', 'true'); \
1876 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1877 return {{ ok:true, count:els.length, native:true }}; \
1878 }} \
1879 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1880 const opts = {{ bubbles:true, cancelable:true, view:window, clientX: rect.left + rect.width/2, clientY: rect.top + rect.height/2 }}; \
1881 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerdown', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1882 try {{ el.dispatchEvent(new MouseEvent('mousedown', opts)); }} catch(_e) {{}} \
1883 try {{ if (window.PointerEvent) el.dispatchEvent(new PointerEvent('pointerup', Object.assign({{pointerId:1, isPrimary:true, pointerType:'mouse'}}, opts))); }} catch(_e) {{}} \
1884 try {{ el.dispatchEvent(new MouseEvent('mouseup', opts)); }} catch(_e) {{}} \
1885 try {{ el.dispatchEvent(new MouseEvent('click', opts)); }} catch(_e) {{}} \
1886 return {{ ok:true, count:els.length }}; \
1887 }})({selector_json}, {idx})"
1888 );
1889 self.run_js_action(&script).await
1890 }
1891
1892 #[cfg(any(
1896 target_os = "ios",
1897 target_os = "android",
1898 all(feature = "webview-input", target_os = "macos"),
1899 all(target_os = "linux", target_env = "ohos")
1900 ))]
1901 pub(crate) async fn type_via_js(
1902 &self,
1903 selector: &str,
1904 index: Option<usize>,
1905 text: &str,
1906 replace: bool,
1907 ) -> Result<(), WebViewInputError> {
1908 let selector_json = serde_json::to_string(selector)
1909 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1910 let text_json = serde_json::to_string(text)
1911 .map_err(|err| WebViewInputError::Platform(format!("Invalid text: {err}")))?;
1912 let idx = index.unwrap_or(0);
1913 let script = format!(
1914 "((sel, i, text, replace) => {{ \
1915 const els = document.querySelectorAll(sel); \
1916 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
1917 const el = els[i]; \
1918 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1919 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1920 const tag = (el.tagName || '').toLowerCase(); \
1921 if (tag === 'input' || tag === 'textarea') {{ \
1922 const proto = tag === 'textarea' ? window.HTMLTextAreaElement.prototype : window.HTMLInputElement.prototype; \
1923 const desc = Object.getOwnPropertyDescriptor(proto, 'value'); \
1924 const next = (replace ? '' : (el.value || '')) + text; \
1925 if (desc && desc.set) {{ desc.set.call(el, next); }} else {{ el.value = next; }} \
1926 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, cancelable:true, data:text, inputType:'insertText' }})); \
1927 el.dispatchEvent(new Event('change', {{ bubbles:true }})); \
1928 return {{ ok:true, count:els.length }}; \
1929 }} \
1930 if (el.isContentEditable) {{ \
1931 el.textContent = (replace ? '' : (el.textContent || '')) + text; \
1932 el.dispatchEvent(new InputEvent('input', {{ bubbles:true, data:text, inputType:'insertText' }})); \
1933 return {{ ok:true, count:els.length }}; \
1934 }} \
1935 if (tag.indexOf('lx-') === 0) {{ \
1936 try {{ el.value = (replace ? '' : (el.value || '')) + text; }} catch(_e) {{}} \
1937 if (typeof el.syncNativeProps === 'function') {{ try {{ el.syncNativeProps(); }} catch(_e) {{}} }} \
1938 el.dispatchEvent(new Event('input', {{ bubbles:true }})); \
1939 return {{ ok:true, count:els.length, native:true }}; \
1940 }} \
1941 return {{ ok:false, error:'not editable', interactable:false, count:els.length }}; \
1942 }})({selector_json}, {idx}, {text_json}, {replace})"
1943 );
1944 self.run_js_action(&script).await
1945 }
1946
1947 #[cfg(any(
1949 target_os = "ios",
1950 target_os = "android",
1951 all(feature = "webview-input", target_os = "macos"),
1952 all(target_os = "linux", target_env = "ohos")
1953 ))]
1954 pub(crate) async fn press_via_js(
1955 &self,
1956 key: &str,
1957 selector: Option<&str>,
1958 index: Option<usize>,
1959 ) -> Result<(), WebViewInputError> {
1960 let key_json = serde_json::to_string(key)
1961 .map_err(|err| WebViewInputError::Platform(format!("Invalid key: {err}")))?;
1962 let selector_json = serde_json::to_string(&selector)
1963 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
1964 let idx = index.unwrap_or(0);
1965 let script = format!(
1966 "((key, sel, i) => {{ \
1967 const els = sel === null ? null : document.querySelectorAll(sel); \
1968 if (els && (!els.length || i < 0 || i >= els.length)) return {{ ok:false, error:'no match', count:els.length }}; \
1969 const el = els ? els[i] : (document.activeElement || document.body); \
1970 if (els) {{ \
1971 try {{ el.scrollIntoView({{block:'center', inline:'center'}}); }} catch(_e) {{}} \
1972 if (typeof el.focus === 'function') {{ try {{ el.focus({{preventScroll:true}}); }} catch(_e) {{ try {{ el.focus(); }} catch(__){{}} }} }} \
1973 }} \
1974 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' }}; \
1975 const norm = String(key).toLowerCase(); \
1976 const k = map[norm] || key; \
1977 const opts = {{ bubbles:true, cancelable:true, composed:true, key:k, view:window }}; \
1978 el.dispatchEvent(new KeyboardEvent('keydown', opts)); \
1979 el.dispatchEvent(new KeyboardEvent('keyup', opts)); \
1980 return {{ ok:true }}; \
1981 }})({key_json}, {selector_json}, {idx})"
1982 );
1983 self.run_js_action(&script).await
1984 }
1985
1986 #[cfg(any(
1996 target_os = "ios",
1997 all(feature = "webview-input", target_os = "macos"),
1998 all(target_os = "linux", target_env = "ohos")
1999 ))]
2000 pub(crate) async fn scroll_via_js(
2001 &self,
2002 at: Option<(f64, f64)>,
2003 dx: f64,
2004 dy: f64,
2005 ) -> Result<(), WebViewInputError> {
2006 let (px, py) = at.unwrap_or((-1.0, -1.0));
2007 let script = format!(
2008 "((px, py, dx, dy) => {{ \
2009 const overflows = (v) => (/(auto|scroll|overlay)/).test(v); \
2010 const ancestor = (node) => {{ \
2011 while (node && node !== document.body && node !== document.documentElement) {{ \
2012 const s = window.getComputedStyle(node); \
2013 if ((overflows(s.overflowY) && node.scrollHeight > node.clientHeight) || \
2014 (overflows(s.overflowX) && node.scrollWidth > node.clientWidth)) return node; \
2015 node = node.parentElement; \
2016 }} \
2017 return null; \
2018 }}; \
2019 const largest = () => {{ \
2020 let best = null, range = 0; \
2021 const all = document.querySelectorAll('*'); \
2022 for (let k = 0; k < all.length; k++) {{ \
2023 const n = all[k], s = window.getComputedStyle(n); \
2024 const ry = overflows(s.overflowY) ? (n.scrollHeight - n.clientHeight) : 0; \
2025 const rx = overflows(s.overflowX) ? (n.scrollWidth - n.clientWidth) : 0; \
2026 const r = ry > rx ? ry : rx; \
2027 if (r > range) {{ range = r; best = n; }} \
2028 }} \
2029 return best; \
2030 }}; \
2031 const vw = window.innerWidth || document.documentElement.clientWidth || 0; \
2032 const vh = window.innerHeight || document.documentElement.clientHeight || 0; \
2033 let target = null; \
2034 if (px >= 0 && py >= 0) target = ancestor(document.elementFromPoint(px, py) || document.body); \
2035 else if (vw > 0 && vh > 0) target = ancestor(document.elementFromPoint(vw >> 1, vh >> 1) || document.body); \
2036 if (!target) {{ \
2037 const se = document.scrollingElement || document.documentElement; \
2038 target = (se && se.scrollHeight > se.clientHeight) ? se : (largest() || se); \
2039 }} \
2040 target.scrollLeft += dx; target.scrollTop += dy; \
2041 return {{ ok:true }}; \
2042 }})({px}, {py}, {dx}, {dy})"
2043 );
2044 self.run_js_action(&script).await
2045 }
2046
2047 #[cfg(any(
2049 target_os = "ios",
2050 all(feature = "webview-input", target_os = "macos"),
2051 all(target_os = "linux", target_env = "ohos")
2052 ))]
2053 pub(crate) async fn scroll_to_via_js(
2054 &self,
2055 selector: &str,
2056 index: Option<usize>,
2057 ) -> Result<(), WebViewInputError> {
2058 let selector_json = serde_json::to_string(selector)
2059 .map_err(|err| WebViewInputError::Platform(format!("Invalid selector: {err}")))?;
2060 let idx = index.unwrap_or(0);
2061 let script = format!(
2062 "((sel, i) => {{ \
2063 const els = document.querySelectorAll(sel); \
2064 if (!els.length || i < 0 || i >= els.length) return {{ ok:false, error:'no match', count:els.length }}; \
2065 try {{ els[i].scrollIntoView({{ block:'center', inline:'center' }}); }} catch(_e) {{ els[i].scrollIntoView(); }} \
2066 return {{ ok:true, count:els.length }}; \
2067 }})({selector_json}, {idx})"
2068 );
2069 self.run_js_action(&script).await
2070 }
2071
2072 pub async fn current_url(&self) -> Result<Option<String>, WebViewError> {
2073 self.inner.current_url().await
2074 }
2075
2076 pub fn reload(&self) -> Result<(), WebViewError> {
2077 self.inner.reload()
2078 }
2079
2080 pub fn go_back(&self) -> Result<(), WebViewError> {
2081 self.inner.go_back()
2082 }
2083
2084 pub fn go_forward(&self) -> Result<(), WebViewError> {
2085 self.inner.go_forward()
2086 }
2087
2088 pub async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
2089 self.inner.list_cookies().await
2090 }
2091
2092 pub async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
2093 self.inner.set_cookie(request).await
2094 }
2095
2096 pub async fn delete_cookie(
2097 &self,
2098 name: &str,
2099 domain: &str,
2100 path: &str,
2101 ) -> Result<(), WebViewError> {
2102 self.inner.delete_cookie(name, domain, path).await
2103 }
2104
2105 pub async fn clear_cookies(&self) -> Result<(), WebViewError> {
2106 self.inner.clear_cookies().await
2107 }
2108
2109 pub async fn start_network_capture(&self) -> Result<(), WebViewError> {
2110 self.inner.start_network_capture().await
2111 }
2112
2113 pub async fn stop_network_capture(&self) -> Result<(), WebViewError> {
2114 self.inner.stop_network_capture().await
2115 }
2116
2117 pub async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
2118 self.inner.network_entries().await
2119 }
2120
2121 pub async fn clear_network_capture(&self) -> Result<(), WebViewError> {
2122 self.inner.clear_network_capture().await
2123 }
2124
2125 pub async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
2126 self.inner.take_screenshot().await
2127 }
2128
2129 pub async fn click(
2130 &self,
2131 selector: &str,
2132 options: ClickOptions,
2133 ) -> Result<(), WebViewInputError> {
2134 <Self as WebViewInputController>::click(self, selector, options).await
2135 }
2136
2137 pub async fn type_text(
2138 &self,
2139 selector: &str,
2140 text: &str,
2141 options: TypeOptions,
2142 ) -> Result<(), WebViewInputError> {
2143 <Self as WebViewInputController>::type_text(self, selector, text, options).await
2144 }
2145
2146 pub async fn fill(
2147 &self,
2148 selector: &str,
2149 text: &str,
2150 options: FillOptions,
2151 ) -> Result<(), WebViewInputError> {
2152 <Self as WebViewInputController>::fill(self, selector, text, options).await
2153 }
2154
2155 pub async fn press(&self, key: &str, options: PressOptions) -> Result<(), WebViewInputError> {
2156 <Self as WebViewInputController>::press(self, key, options).await
2157 }
2158
2159 pub async fn scroll(
2160 &self,
2161 dx: f64,
2162 dy: f64,
2163 options: ScrollOptions,
2164 ) -> Result<(), WebViewInputError> {
2165 <Self as WebViewInputController>::scroll(self, dx, dy, options).await
2166 }
2167
2168 pub async fn scroll_to(
2169 &self,
2170 selector: &str,
2171 options: ScrollOptions,
2172 ) -> Result<(), WebViewInputError> {
2173 <Self as WebViewInputController>::scroll_to(self, selector, options).await
2174 }
2175}
2176
2177#[async_trait]
2178impl WebViewController for WebView {
2179 fn load_url(&self, url: &str) -> Result<(), WebViewError> {
2180 self.inner.load_url(url)
2181 }
2182
2183 fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError> {
2184 self.inner.load_data(request)
2185 }
2186
2187 fn exec_js(&self, js: &str) -> Result<(), WebViewError> {
2188 self.inner.exec_js(js)
2189 }
2190
2191 async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError> {
2192 self.inner.eval_js(js).await
2193 }
2194
2195 async fn current_url(&self) -> Result<Option<String>, WebViewError> {
2196 self.inner.current_url().await
2197 }
2198
2199 fn post_message(&self, message: &str) -> Result<(), WebViewError> {
2200 self.inner.post_message(message)
2201 }
2202
2203 fn post_message_to_document(
2204 &self,
2205 expected_generation: crate::DocumentGeneration,
2206 gate: Arc<dyn crate::DocumentOutboundGate>,
2207 message: &str,
2208 ) -> Result<(), WebViewError> {
2209 self.inner
2210 .post_message_to_document(expected_generation, gate, message)
2211 }
2212
2213 fn clear_browsing_data(&self) -> Result<(), WebViewError> {
2214 self.inner.clear_browsing_data()
2215 }
2216
2217 fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError> {
2218 user_agent.validate()?;
2219 self.inner.set_user_agent_override(user_agent)
2220 }
2221
2222 fn reload(&self) -> Result<(), WebViewError> {
2223 self.inner.reload()
2224 }
2225
2226 fn go_back(&self) -> Result<(), WebViewError> {
2227 self.inner.go_back()
2228 }
2229
2230 fn go_forward(&self) -> Result<(), WebViewError> {
2231 self.inner.go_forward()
2232 }
2233
2234 async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
2235 self.inner.list_cookies().await
2236 }
2237
2238 async fn set_cookie(&self, request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
2239 self.inner.set_cookie(request).await
2240 }
2241
2242 async fn delete_cookie(
2243 &self,
2244 name: &str,
2245 domain: &str,
2246 path: &str,
2247 ) -> Result<(), WebViewError> {
2248 self.inner.delete_cookie(name, domain, path).await
2249 }
2250
2251 async fn clear_cookies(&self) -> Result<(), WebViewError> {
2252 self.inner.clear_cookies().await
2253 }
2254
2255 async fn clear_site_data(
2256 &self,
2257 url: &str,
2258 options: ClearSiteDataOptions,
2259 ) -> Result<ClearSiteDataResult, WebViewError> {
2260 self.inner.clear_site_data(url, options).await
2261 }
2262
2263 async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
2268 self.inner.take_screenshot().await
2269 }
2270
2271 async fn start_network_capture(&self) -> Result<(), WebViewError> {
2272 self.inner.start_network_capture().await
2273 }
2274
2275 async fn stop_network_capture(&self) -> Result<(), WebViewError> {
2276 self.inner.stop_network_capture().await
2277 }
2278
2279 async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
2280 self.inner.network_entries().await
2281 }
2282
2283 async fn clear_network_capture(&self) -> Result<(), WebViewError> {
2284 self.inner.clear_network_capture().await
2285 }
2286}
2287
2288#[async_trait]
2289impl WebViewInputController for WebView {
2290 async fn click(
2291 &self,
2292 _selector: &str,
2293 _options: ClickOptions,
2294 ) -> Result<(), WebViewInputError> {
2295 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2301 {
2302 return self.click_via_js(_selector, _options.index).await;
2303 }
2304 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2305 {
2306 return self.inner.click_inner(_selector, _options).await;
2307 }
2308 #[cfg(target_os = "android")]
2309 {
2310 return self.inner.click_inner(_selector, _options).await;
2311 }
2312 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
2313 {
2314 return self.click_via_js(_selector, _options.index).await;
2315 }
2316 #[allow(unreachable_code)]
2317 Err(WebViewInputError::Unsupported(
2318 "input control is not implemented for this platform",
2319 ))
2320 }
2321
2322 async fn type_text(
2323 &self,
2324 _selector: &str,
2325 _text: &str,
2326 _options: TypeOptions,
2327 ) -> Result<(), WebViewInputError> {
2328 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2329 {
2330 if self.inner.is_window_attached().await {
2331 return self.inner.type_text_inner(_selector, _text, _options).await;
2332 }
2333 return self
2334 .type_via_js(_selector, _options.index, _text, _options.replace)
2335 .await;
2336 }
2337 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2338 {
2339 return self.inner.type_text_inner(_selector, _text, _options).await;
2340 }
2341 #[cfg(any(
2342 target_os = "ios",
2343 target_os = "android",
2344 all(target_os = "linux", target_env = "ohos")
2345 ))]
2346 {
2347 return self
2348 .type_via_js(_selector, _options.index, _text, _options.replace)
2349 .await;
2350 }
2351 #[allow(unreachable_code)]
2352 Err(WebViewInputError::Unsupported(
2353 "input control is not implemented for this platform",
2354 ))
2355 }
2356
2357 async fn fill(
2358 &self,
2359 _selector: &str,
2360 _text: &str,
2361 _options: FillOptions,
2362 ) -> Result<(), WebViewInputError> {
2363 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2364 {
2365 return self
2370 .type_via_js(_selector, _options.index, _text, true)
2371 .await;
2372 }
2373 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2374 {
2375 return self
2376 .inner
2377 .type_text_inner(
2378 _selector,
2379 _text,
2380 TypeOptions {
2381 index: _options.index,
2382 replace: true,
2383 },
2384 )
2385 .await;
2386 }
2387 #[cfg(any(
2388 target_os = "ios",
2389 target_os = "android",
2390 all(target_os = "linux", target_env = "ohos")
2391 ))]
2392 {
2393 return self
2394 .type_via_js(_selector, _options.index, _text, true)
2395 .await;
2396 }
2397 #[allow(unreachable_code)]
2398 Err(WebViewInputError::Unsupported(
2399 "input control is not implemented for this platform",
2400 ))
2401 }
2402
2403 async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
2404 if _options.index.is_some() && _options.selector.is_none() {
2405 return Err(WebViewInputError::Platform(
2406 "press index requires a selector".to_string(),
2407 ));
2408 }
2409 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2410 {
2411 if self.inner.is_window_attached().await {
2412 return self.inner.press_inner(_key, _options).await;
2413 }
2414 return self
2415 .press_via_js(_key, _options.selector.as_deref(), _options.index)
2416 .await;
2417 }
2418 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2419 {
2420 return self.inner.press_inner(_key, _options).await;
2421 }
2422 #[cfg(any(
2423 target_os = "ios",
2424 target_os = "android",
2425 all(target_os = "linux", target_env = "ohos")
2426 ))]
2427 {
2428 return self
2429 .press_via_js(_key, _options.selector.as_deref(), _options.index)
2430 .await;
2431 }
2432 #[allow(unreachable_code)]
2433 Err(WebViewInputError::Unsupported(
2434 "input control is not implemented for this platform",
2435 ))
2436 }
2437
2438 async fn scroll(
2439 &self,
2440 _dx: f64,
2441 _dy: f64,
2442 _options: ScrollOptions,
2443 ) -> Result<(), WebViewInputError> {
2444 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2447 {
2448 if self.inner.is_window_attached().await {
2449 return self.inner.scroll_inner(_dx, _dy, _options).await;
2450 }
2451 return self.scroll_via_js(None, _dx, _dy).await;
2452 }
2453 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2454 {
2455 return self.inner.scroll_inner(_dx, _dy, _options).await;
2456 }
2457 #[cfg(target_os = "android")]
2460 {
2461 return self.inner.scroll_inner(_dx, _dy, _options).await;
2462 }
2463 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
2465 {
2466 return self.scroll_via_js(None, _dx, _dy).await;
2467 }
2468 #[allow(unreachable_code)]
2469 Err(WebViewInputError::Unsupported(
2470 "input control is not implemented for this platform",
2471 ))
2472 }
2473
2474 async fn scroll_to(
2475 &self,
2476 _selector: &str,
2477 _options: ScrollOptions,
2478 ) -> Result<(), WebViewInputError> {
2479 #[cfg(all(feature = "webview-input", target_os = "macos"))]
2480 {
2481 if self.inner.is_window_attached().await {
2482 return self.inner.scroll_to_inner(_selector, _options).await;
2483 }
2484 return self.scroll_to_via_js(_selector, None).await;
2485 }
2486 #[cfg(all(feature = "webview-input", target_os = "windows"))]
2487 {
2488 return self.inner.scroll_to_inner(_selector, _options).await;
2489 }
2490 #[cfg(target_os = "android")]
2491 {
2492 return self.inner.scroll_to_inner(_selector, _options).await;
2493 }
2494 #[cfg(any(target_os = "ios", all(target_os = "linux", target_env = "ohos")))]
2495 {
2496 return self.scroll_to_via_js(_selector, None).await;
2497 }
2498 #[allow(unreachable_code)]
2499 Err(WebViewInputError::Unsupported(
2500 "input control is not implemented for this platform",
2501 ))
2502 }
2503}
2504
2505type WebViewInstancesMap = Arc<Mutex<HashMap<String, Arc<WebView>>>>;
2507
2508#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
2509#[serde(rename_all = "snake_case")]
2510pub enum WebViewCreateStage {
2511 Requested,
2512 NativeCreated,
2513 ControllerAttached,
2514 Ready,
2515 Destroyed,
2516}
2517
2518#[derive(Debug, Clone, PartialEq, Eq)]
2519pub enum WebViewEvent {
2520 Stage(WebViewCreateStage),
2521 Failed {
2522 stage: WebViewCreateStage,
2523 error: WebViewError,
2524 },
2525}
2526
2527type WebViewReadyState = Option<Result<Arc<WebView>, WebViewError>>;
2528
2529#[derive(Clone)]
2530pub struct WebViewEventSubscription {
2531 rx: watch::Receiver<WebViewEvent>,
2532}
2533
2534impl WebViewEventSubscription {
2535 pub fn current(&self) -> WebViewEvent {
2536 self.rx.borrow().clone()
2537 }
2538
2539 pub async fn changed(&mut self) -> Result<WebViewEvent, WebViewError> {
2540 self.rx.changed().await.map_err(|_| {
2541 WebViewError::WebView("webview event channel unexpectedly closed".to_string())
2542 })?;
2543 Ok(self.current())
2544 }
2545}
2546
2547#[derive(Clone)]
2548pub struct WebViewSession {
2549 webtag: WebTag,
2550 event_rx: watch::Receiver<WebViewEvent>,
2551 ready_rx: watch::Receiver<WebViewReadyState>,
2552 signals: Arc<WebViewSessionSignals>,
2553}
2554
2555impl WebViewSession {
2556 pub fn webtag(&self) -> &WebTag {
2557 &self.webtag
2558 }
2559
2560 pub fn subscribe_events(&self) -> WebViewEventSubscription {
2561 WebViewEventSubscription {
2562 rx: self.event_rx.clone(),
2563 }
2564 }
2565
2566 pub fn current_event(&self) -> WebViewEvent {
2567 self.event_rx.borrow().clone()
2568 }
2569
2570 pub async fn wait_ready(&self) -> Result<Arc<WebView>, WebViewError> {
2571 let mut rx = self.ready_rx.clone();
2572 loop {
2573 if let Some(result) = self.signals.terminal_result() {
2574 return result;
2575 }
2576 if let Some(result) = rx.borrow().clone() {
2577 return result;
2578 }
2579 if rx.changed().await.is_err() {
2580 if let Some(result) = self.signals.terminal_result() {
2581 return result;
2582 }
2583 return Err(WebViewError::WebView(
2584 "webview ready channel unexpectedly closed".to_string(),
2585 ));
2586 }
2587 }
2588 }
2589}
2590
2591struct WebViewSessionSignals {
2592 event_tx: watch::Sender<WebViewEvent>,
2593 ready_tx: watch::Sender<WebViewReadyState>,
2594 state: Mutex<WebViewSessionState>,
2595}
2596
2597#[derive(Default)]
2598struct WebViewSessionState {
2599 terminal_result: Option<Result<Arc<WebView>, WebViewError>>,
2600 destroyed: bool,
2601}
2602
2603impl WebViewSessionSignals {
2604 fn new() -> Arc<Self> {
2605 let (event_tx, _event_rx) =
2606 watch::channel(WebViewEvent::Stage(WebViewCreateStage::Requested));
2607 let (ready_tx, _ready_rx) = watch::channel(None);
2608 Arc::new(Self {
2609 event_tx,
2610 ready_tx,
2611 state: Mutex::new(WebViewSessionState::default()),
2612 })
2613 }
2614
2615 fn subscribe(self: &Arc<Self>, webtag: WebTag) -> WebViewSession {
2616 WebViewSession {
2617 webtag,
2618 event_rx: self.event_tx.subscribe(),
2619 ready_rx: self.ready_tx.subscribe(),
2620 signals: Arc::clone(self),
2621 }
2622 }
2623
2624 fn terminal_result(&self) -> Option<Result<Arc<WebView>, WebViewError>> {
2625 let state = lock_or_recover(&self.state, "webview_session_state.terminal_result");
2626 state.terminal_result.clone()
2627 }
2628
2629 #[cfg_attr(not(any(target_os = "macos", target_os = "ios")), allow(dead_code))]
2631 fn is_destroyed(&self) -> bool {
2632 let state = lock_or_recover(&self.state, "webview_session_state.is_destroyed");
2633 state.destroyed
2634 }
2635
2636 fn publish_result(
2637 &self,
2638 result: Result<Arc<WebView>, WebViewError>,
2639 stage_on_error: WebViewCreateStage,
2640 ) {
2641 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_result");
2642 if state.destroyed || state.terminal_result.is_some() {
2643 return;
2644 }
2645 state.terminal_result = Some(result.clone());
2646 drop(state);
2647
2648 match result {
2649 Ok(webview) => {
2650 self.event_tx
2651 .send_replace(WebViewEvent::Stage(WebViewCreateStage::NativeCreated));
2652 self.event_tx
2653 .send_replace(WebViewEvent::Stage(WebViewCreateStage::ControllerAttached));
2654 self.ready_tx.send_replace(Some(Ok(webview)));
2655 self.event_tx
2656 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Ready));
2657 }
2658 Err(error) => {
2659 self.ready_tx.send_replace(Some(Err(error.clone())));
2660 self.event_tx.send_replace(WebViewEvent::Failed {
2661 stage: stage_on_error,
2662 error,
2663 });
2664 }
2665 }
2666 }
2667
2668 fn publish_destroyed(&self) {
2669 let mut state = lock_or_recover(&self.state, "webview_session_state.publish_destroyed");
2670 if state.destroyed {
2671 return;
2672 }
2673 state.destroyed = true;
2674 if state.terminal_result.is_none() {
2675 state.terminal_result = Some(Err(WebViewError::WebView(
2676 "webview destroyed before ready".to_string(),
2677 )));
2678 }
2679 let terminal_result = state.terminal_result.clone();
2680 drop(state);
2681
2682 self.event_tx
2683 .send_replace(WebViewEvent::Stage(WebViewCreateStage::Destroyed));
2684 if let Some(result) = terminal_result {
2685 self.ready_tx.send_replace(Some(result));
2686 }
2687 }
2688}
2689
2690pub(crate) struct WebViewCreateSender {
2691 webtag: WebTag,
2692 signals: Arc<WebViewSessionSignals>,
2693 native_view_id: NativeWebViewId,
2694}
2695
2696impl WebViewCreateSender {
2697 fn new(webtag: WebTag, signals: Arc<WebViewSessionSignals>) -> Self {
2698 Self {
2699 webtag,
2700 signals,
2701 native_view_id: next_native_webview_id(),
2702 }
2703 }
2704
2705 pub(crate) const fn native_view_id(&self) -> NativeWebViewId {
2709 self.native_view_id
2710 }
2711
2712 pub(crate) fn succeed(self, webview: Arc<WebView>) {
2713 self.signals
2714 .publish_result(Ok(webview), WebViewCreateStage::Requested);
2715 }
2716
2717 pub(crate) fn fail(self, stage: WebViewCreateStage, error: WebViewError) {
2718 if remove_session_signals_if_matches(&self.webtag, &self.signals) {
2719 crate::events::normalizer::destroy(&self.webtag);
2720 }
2721 self.signals.publish_result(Err(error), stage);
2722 }
2723
2724 #[cfg_attr(not(target_os = "android"), allow(dead_code))]
2728 pub(crate) fn cancel_superseded(self) {
2729 if remove_session_signals_if_matches(&self.webtag, &self.signals) {
2730 crate::events::normalizer::destroy(&self.webtag);
2731 }
2732 self.signals.publish_destroyed();
2733 }
2734
2735 #[cfg_attr(
2740 not(any(target_os = "macos", target_os = "ios", target_os = "windows")),
2741 allow(dead_code)
2742 )]
2743 pub(crate) fn is_destroyed(&self) -> bool {
2744 self.signals.is_destroyed()
2745 }
2746}
2747
2748static WEBVIEW_INSTANCES: OnceLock<WebViewInstancesMap> = OnceLock::new();
2750
2751struct PendingCallbacksEntry {
2754 #[cfg(target_os = "android")]
2755 signals: Arc<WebViewSessionSignals>,
2756 callbacks: PendingCallbacks,
2757}
2758
2759static PENDING_CALLBACKS: OnceLock<Mutex<HashMap<String, PendingCallbacksEntry>>> = OnceLock::new();
2760static WEBVIEW_SESSIONS: OnceLock<Mutex<HashMap<String, Arc<WebViewSessionSignals>>>> =
2761 OnceLock::new();
2762#[cfg(target_os = "windows")]
2763static WEBVIEW_CREATE_LOCKS: OnceLock<Mutex<HashMap<String, std::sync::Weak<Mutex<()>>>>> =
2764 OnceLock::new();
2765static DESIRED_PROXY_FOR_NEW_WEBVIEWS: OnceLock<RwLock<Option<ProxyConfig>>> = OnceLock::new();
2766static PROXY_APPLY_LOCK: OnceLock<Mutex<()>> = OnceLock::new();
2767
2768fn apply_http_proxy_platform(
2769 config: Option<&ProxyConfig>,
2770) -> Result<ProxyApplyReport, WebViewError> {
2771 #[cfg(target_os = "android")]
2772 {
2773 crate::android::apply_http_proxy(config)
2774 }
2775
2776 #[cfg(any(target_os = "ios", target_os = "macos"))]
2777 {
2778 crate::apple::apply_http_proxy(config)
2779 }
2780
2781 #[cfg(all(target_os = "linux", target_env = "ohos"))]
2782 {
2783 crate::harmony::apply_http_proxy(config)
2784 }
2785
2786 #[cfg(not(any(
2787 target_os = "android",
2788 target_os = "ios",
2789 target_os = "macos",
2790 all(target_os = "linux", target_env = "ohos")
2791 )))]
2792 {
2793 let _ = config;
2794 Ok(ProxyApplyReport::unsupported(
2795 "proxy is not supported on this platform",
2796 ))
2797 }
2798}
2799
2800pub fn configure_proxy_for_new_webviews(config: Option<ProxyConfig>) -> Result<(), WebViewError> {
2805 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
2806 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
2807
2808 let normalized_config = match config {
2809 Some(cfg) => Some(cfg.validate()?),
2810 None => None,
2811 };
2812
2813 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
2814 match state.write() {
2815 Ok(mut guard) => {
2816 *guard = normalized_config;
2817 }
2818 Err(poisoned) => {
2819 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2820 *poisoned.into_inner() = normalized_config;
2821 }
2822 }
2823 Ok(())
2824}
2825
2826pub fn apply_proxy_to_current_runtime(
2831 config: Option<ProxyConfig>,
2832) -> Result<ProxyApplyReport, WebViewError> {
2833 let apply_lock = PROXY_APPLY_LOCK.get_or_init(|| Mutex::new(()));
2834 let _guard = lock_or_recover(apply_lock, "webview_proxy_apply_lock");
2835
2836 let normalized_config = match config {
2837 Some(cfg) => Some(cfg.validate()?),
2838 None => None,
2839 };
2840
2841 let report = apply_http_proxy_platform(normalized_config.as_ref())?;
2842
2843 if matches!(
2844 report.status,
2845 ProxyApplyStatus::Applied | ProxyApplyStatus::Cleared
2846 ) {
2847 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get_or_init(|| RwLock::new(None));
2848 match state.write() {
2849 Ok(mut guard) => {
2850 *guard = normalized_config;
2851 }
2852 Err(poisoned) => {
2853 log::error!("RwLock poisoned at webview_desired_proxy.write, recovering");
2854 *poisoned.into_inner() = normalized_config;
2855 }
2856 }
2857 }
2858
2859 Ok(report)
2860}
2861
2862pub fn configured_proxy_for_new_webviews() -> Option<ProxyConfig> {
2864 let state = DESIRED_PROXY_FOR_NEW_WEBVIEWS.get()?;
2865 match state.read() {
2866 Ok(guard) => guard.clone(),
2867 Err(poisoned) => {
2868 log::error!("RwLock poisoned at webview_desired_proxy.read, recovering");
2869 poisoned.into_inner().clone()
2870 }
2871 }
2872}
2873
2874fn clear_pending_callbacks(webtag: &WebTag) {
2875 if let Some(pending) = PENDING_CALLBACKS.get()
2876 && let Ok(mut map) = pending.lock()
2877 {
2878 map.remove(webtag.key());
2879 }
2880}
2881
2882fn replace_session_signals(webtag: &WebTag, signals: Arc<WebViewSessionSignals>) {
2883 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
2884 let mut guard = lock_or_recover(sessions, "webview_sessions.replace");
2885 guard.insert(webtag.key().to_string(), signals);
2886}
2887
2888fn remove_session_signals(webtag: &WebTag) -> Option<Arc<WebViewSessionSignals>> {
2889 let sessions = WEBVIEW_SESSIONS.get()?;
2890 let mut guard = lock_or_recover(sessions, "webview_sessions.remove");
2891 guard.remove(webtag.key())
2892}
2893
2894fn remove_session_signals_if_matches(
2895 webtag: &WebTag,
2896 expected: &Arc<WebViewSessionSignals>,
2897) -> bool {
2898 let Some(sessions) = WEBVIEW_SESSIONS.get() else {
2899 return false;
2900 };
2901 let mut guard = lock_or_recover(sessions, "webview_sessions.remove_if_matches");
2902 if guard
2903 .get(webtag.key())
2904 .is_some_and(|current| Arc::ptr_eq(current, expected))
2905 {
2906 guard.remove(webtag.key());
2907 true
2908 } else {
2909 false
2910 }
2911}
2912
2913fn remove_session_signals_owning(
2917 webtag: &WebTag,
2918 expected: &Arc<WebView>,
2919) -> Option<Arc<WebViewSessionSignals>> {
2920 let sessions = WEBVIEW_SESSIONS.get()?;
2921 let mut guard = lock_or_recover(sessions, "webview_sessions.remove_owning");
2922 let owns = guard.get(webtag.key()).is_some_and(|signals| {
2923 matches!(signals.terminal_result(), Some(Ok(current)) if Arc::ptr_eq(¤t, expected))
2924 });
2925 owns.then(|| guard.remove(webtag.key())).flatten()
2926}
2927
2928#[derive(Debug, Clone, PartialEq, Eq, Hash)]
2931pub struct WebTag(String);
2932
2933impl std::fmt::Display for WebTag {
2934 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2935 write!(f, "{}", self.0)
2936 }
2937}
2938
2939impl WebTag {
2940 pub fn new(appid: &str, path: &str, session_id: Option<u64>) -> Self {
2941 let mut tag = format!("{}:{}", appid, path);
2942 if let Some(session) = session_id {
2943 tag.push('#');
2944 tag.push_str(&session.to_string());
2945 }
2946 Self(tag)
2947 }
2948
2949 pub fn as_str(&self) -> &str {
2950 &self.0
2951 }
2952
2953 pub fn key(&self) -> &str {
2957 &self.0
2958 }
2959
2960 pub fn extract_appid(&self) -> String {
2962 self.0.split(':').next().unwrap_or("").to_string()
2963 }
2964
2965 pub fn extract_parts(&self) -> (String, String) {
2968 if let Some((appid, path_with_session)) = self.0.split_once(':') {
2969 let path = path_with_session
2970 .split('#')
2971 .next()
2972 .unwrap_or(path_with_session);
2973 (appid.to_string(), path.to_string())
2974 } else {
2975 log::error!("Invalid webtag format: {}", self.0);
2976 ("".to_string(), self.0.clone())
2977 }
2978 }
2979
2980 pub fn session_id(&self) -> Option<u64> {
2982 self.0
2983 .split('#')
2984 .next_back()
2985 .and_then(|raw| raw.parse::<u64>().ok())
2986 }
2987
2988 #[cfg_attr(
2992 any(not(target_os = "windows"), target_os = "windows"),
2993 allow(dead_code)
2994 )]
2995 pub(crate) fn group_key(&self) -> String {
2996 let Some((appid, path_with_session)) = self.0.split_once(':') else {
2997 return self.0.clone();
2998 };
2999 let session = path_with_session
3000 .rsplit_once('#')
3001 .and_then(|(_, suffix)| suffix.parse::<u64>().ok())
3002 .map(|session| session.to_string())
3003 .unwrap_or_else(|| "0".to_string());
3004 format!("{appid}#{session}")
3005 }
3006
3007 fn key_path(&self) -> String {
3008 let Some((_, path_with_suffix)) = self.0.split_once(':') else {
3009 return self.0.clone();
3010 };
3011 if self.session_id().is_some()
3012 && let Some((path, _)) = path_with_suffix.rsplit_once('#')
3013 {
3014 return path.to_string();
3015 }
3016 path_with_suffix.to_string()
3017 }
3018}
3019
3020impl From<&str> for WebTag {
3021 fn from(webtag_str: &str) -> Self {
3022 Self(webtag_str.to_string())
3023 }
3024}
3025
3026fn request_create_webview(
3027 webtag: &WebTag,
3028 sender: WebViewCreateSender,
3029 options: WebViewCreateOptions,
3030) {
3031 let (appid, _) = webtag.extract_parts();
3032 let (effective_options, pending_callbacks) = match options.normalize() {
3033 Ok(value) => value,
3034 Err(error) => {
3035 sender.fail(WebViewCreateStage::Requested, error);
3036 return;
3037 }
3038 };
3039
3040 log::info!(
3041 "Creating WebView for key={} profile={:?} data_mode={:?} schemes={:?}",
3042 webtag.key(),
3043 effective_options.profile,
3044 effective_options.data_mode,
3045 effective_options.registered_schemes,
3046 );
3047
3048 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
3050
3051 if let Ok(webviews) = instances.lock()
3056 && let Some(existing_webview) = webviews.get(webtag.key())
3057 {
3058 if existing_webview.effective_options() != &effective_options {
3059 sender.fail(
3060 WebViewCreateStage::Requested,
3061 WebViewError::InvalidCreateOptions(format!(
3062 "webview already exists with different options: key={} existing={:?} requested={:?}",
3063 webtag.key(),
3064 existing_webview.effective_options(),
3065 effective_options
3066 )),
3067 );
3068 return;
3069 }
3070
3071 if pending_callbacks.has_any() {
3072 sender.fail(
3073 WebViewCreateStage::Requested,
3074 WebViewError::InvalidCreateOptions(format!(
3075 "webview already exists and callback registrations are immutable: key={} options={:?}",
3076 webtag.key(),
3077 existing_webview.effective_options()
3078 )),
3079 );
3080 log::warn!(
3081 "Rejected recreate with callbacks for existing webview key={} options={:?}",
3082 webtag.key(),
3083 existing_webview.effective_options()
3084 );
3085 return;
3086 }
3087
3088 log::info!("WebView already exists, reusing: {}", webtag.key());
3089 sender.succeed(existing_webview.clone());
3090 return;
3091 }
3092
3093 clear_pending_callbacks(webtag);
3095
3096 if pending_callbacks.has_any() {
3098 let pending = PENDING_CALLBACKS.get_or_init(|| Mutex::new(HashMap::new()));
3099 if let Ok(mut map) = pending.lock() {
3100 map.insert(
3101 webtag.key().to_string(),
3102 PendingCallbacksEntry {
3103 #[cfg(target_os = "android")]
3104 signals: Arc::clone(&sender.signals),
3105 callbacks: pending_callbacks,
3106 },
3107 );
3108 }
3109 }
3110
3111 WebViewInner::create(
3113 &appid,
3114 &webtag.key_path(),
3115 webtag.session_id(),
3116 effective_options,
3117 sender,
3118 );
3119}
3120
3121fn create_webview_session(webtag: WebTag, options: WebViewCreateOptions) -> WebViewSession {
3122 #[cfg(target_os = "windows")]
3127 let create_lock = windows_webview_create_lock(webtag.key());
3128 #[cfg(target_os = "windows")]
3129 let _create_guard = lock_or_recover(&create_lock, "windows_webview_create_lock");
3130
3131 let signals = WebViewSessionSignals::new();
3132 let session = signals.subscribe(webtag.clone());
3133 let sender = WebViewCreateSender::new(webtag.clone(), signals.clone());
3134 replace_session_signals(&webtag, signals);
3135 crate::events::normalizer::begin(&webtag, sender.native_view_id());
3136 request_create_webview(&webtag, sender, options);
3137 session
3138}
3139
3140#[cfg(target_os = "windows")]
3141fn windows_webview_create_lock(webtag_key: &str) -> Arc<Mutex<()>> {
3142 let locks = WEBVIEW_CREATE_LOCKS.get_or_init(|| Mutex::new(HashMap::new()));
3143 let mut locks = lock_or_recover(locks, "windows_webview_create_locks");
3144 locks.retain(|_, lock| lock.strong_count() > 0);
3145 if let Some(lock) = locks.get(webtag_key).and_then(std::sync::Weak::upgrade) {
3146 return lock;
3147 }
3148 let lock = Arc::new(Mutex::new(()));
3149 locks.insert(webtag_key.to_string(), Arc::downgrade(&lock));
3150 lock
3151}
3152
3153#[cfg_attr(target_os = "android", allow(dead_code))]
3154pub(crate) fn register_webview(webview: Arc<WebView>) {
3155 let webtag = webview.webtag();
3156 crate::events::normalizer::bind_native_view(&webtag, webview.native_view_id());
3157
3158 if let Some(pending) = PENDING_CALLBACKS.get()
3160 && let Ok(mut map) = pending.lock()
3161 && let Some(entry) = map.remove(webtag.key())
3162 {
3163 let callbacks = entry.callbacks;
3164 log::info!(
3165 "Installing callbacks for {} (schemes={}, nav={}, new_window={}, download={}, file_chooser={}, delegate={})",
3166 webtag.key(),
3167 callbacks.scheme_handlers.len(),
3168 callbacks.navigation_handler.is_some(),
3169 callbacks.new_window_handler.is_some(),
3170 callbacks.download_handler.is_some(),
3171 callbacks.file_chooser_handler.is_some(),
3172 callbacks.delegate.is_some()
3173 );
3174 webview.install_callbacks(callbacks);
3175 }
3176
3177 if let Some(instances) = WEBVIEW_INSTANCES.get()
3178 && let Ok(mut webviews) = instances.lock()
3179 {
3180 webviews.insert(webtag.key().to_string(), webview.clone());
3181 log::info!("WebView created and stored: {}", webtag.key());
3182 }
3183}
3184
3185#[cfg(target_os = "android")]
3186pub(crate) fn register_android_webview_if_current(
3187 webview: Arc<WebView>,
3188 sender: &WebViewCreateSender,
3189) -> bool {
3190 let webtag = webview.webtag();
3191 let sessions = WEBVIEW_SESSIONS.get_or_init(|| Mutex::new(HashMap::new()));
3192 let session_guard = lock_or_recover(sessions, "webview_sessions.register_android");
3193 if !session_guard
3194 .get(webtag.key())
3195 .is_some_and(|current| Arc::ptr_eq(current, &sender.signals))
3196 {
3197 return false;
3198 }
3199
3200 if let Some(pending) = PENDING_CALLBACKS.get()
3201 && let Ok(mut map) = pending.lock()
3202 && map
3203 .get(webtag.key())
3204 .is_some_and(|entry| Arc::ptr_eq(&entry.signals, &sender.signals))
3205 && let Some(entry) = map.remove(webtag.key())
3206 {
3207 webview.install_callbacks(entry.callbacks);
3208 }
3209
3210 let instances = WEBVIEW_INSTANCES.get_or_init(|| Arc::new(Mutex::new(HashMap::new())));
3211 let mut webviews = lock_or_recover(instances, "webview_instances.register_android");
3212 crate::events::normalizer::bind_native_view(&webtag, webview.native_view_id());
3213 webviews.insert(webtag.key().to_string(), webview);
3214 true
3215}
3216
3217pub(crate) fn find_webview(webtag: &WebTag) -> Option<Arc<WebView>> {
3219 if let Some(instances) = WEBVIEW_INSTANCES.get() {
3220 if let Ok(webviews) = instances.lock() {
3221 webviews.get(webtag.key()).cloned()
3222 } else {
3223 None
3224 }
3225 } else {
3226 None
3227 }
3228}
3229
3230#[allow(dead_code)]
3236pub(crate) fn find_webview_by_native_view_id(
3237 webtag: &WebTag,
3238 native_view_id: NativeWebViewId,
3239) -> Option<Arc<WebView>> {
3240 find_webview(webtag).filter(|webview| webview.native_view_id() == native_view_id)
3241}
3242
3243#[cfg(target_os = "windows")]
3244pub(crate) fn first_browser_webview() -> Option<Arc<WebView>> {
3245 WEBVIEW_INSTANCES
3246 .get()
3247 .and_then(|instances| instances.lock().ok())
3248 .and_then(|webviews| {
3249 webviews
3250 .values()
3251 .find(|webview| {
3252 webview.effective_options.profile == SecurityProfile::BrowserRelaxed
3253 })
3254 .cloned()
3255 })
3256}
3257
3258pub(crate) fn list_webviews() -> Vec<WebTag> {
3259 if let Some(instances) = WEBVIEW_INSTANCES.get()
3260 && let Ok(webviews) = instances.lock()
3261 {
3262 let mut tags: Vec<WebTag> = webviews.values().map(|webview| webview.webtag()).collect();
3263 tags.sort_by(|a, b| a.as_str().cmp(b.as_str()));
3264 return tags;
3265 }
3266 Vec::new()
3267}
3268
3269pub(crate) fn find_webview_delegate_by_native_view_id(
3273 webtag: &WebTag,
3274 native_view_id: NativeWebViewId,
3275) -> Option<Arc<dyn WebViewDelegate>> {
3276 find_webview_by_native_view_id(webtag, native_view_id)
3277 .and_then(|webview| webview.get_delegate())
3278}
3279
3280fn remove_arc_if_matches<T>(
3281 entries: &mut HashMap<String, Arc<T>>,
3282 key: &str,
3283 expected: &Arc<T>,
3284) -> Option<Arc<T>> {
3285 entries
3286 .get(key)
3287 .is_some_and(|current| Arc::ptr_eq(current, expected))
3288 .then(|| entries.remove(key))
3289 .flatten()
3290}
3291
3292pub(crate) fn destroy_webview_if_matches(webtag: &WebTag, expected: &Arc<WebView>) -> bool {
3297 expected.close_message_ingress();
3301 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
3302 && let Ok(mut webviews) = instances.lock()
3303 {
3304 remove_arc_if_matches(&mut webviews, webtag.key(), expected)
3305 } else {
3306 None
3307 };
3308 if let Some(webview) = removed {
3309 if let Some(signals) = remove_session_signals_owning(webtag, &webview) {
3310 signals.publish_destroyed();
3311 }
3312 #[cfg(target_os = "windows")]
3313 {
3314 let _ = webview.inner.set_content_visible(false);
3315 webview.inner.request_shutdown();
3316 }
3317 webview.remove_delegate();
3318 true
3319 } else {
3320 false
3321 }
3322}
3323
3324pub(crate) fn destroy_current_webview(webtag: &WebTag) {
3329 if let Some(webview) = find_webview(webtag) {
3333 webview.close_message_ingress();
3334 }
3335 crate::events::normalizer::destroy(webtag);
3338 if let Some(signals) = remove_session_signals(webtag) {
3344 signals.publish_destroyed();
3345 }
3346 let removed = if let Some(instances) = WEBVIEW_INSTANCES.get()
3347 && let Ok(mut webviews) = instances.lock()
3348 {
3349 webviews.remove(webtag.key())
3350 } else {
3351 None
3352 };
3353 if let Some(webview) = removed {
3354 webview.close_message_ingress();
3355 #[cfg(target_os = "windows")]
3359 {
3360 let _ = webview.inner.set_content_visible(false);
3361 webview.inner.request_shutdown();
3365 }
3366 webview.remove_delegate();
3367 }
3368 clear_pending_callbacks(webtag);
3369}
3370
3371#[cfg(test)]
3372mod tests {
3373 use super::{
3374 MAX_PENDING_WEB_MESSAGE_BYTES, MAX_PENDING_WEB_MESSAGES, MAX_WEB_MESSAGE_BYTES,
3375 PlatformConsoleBackend, PlatformConsoleDelivery, SecurityProfile, WEBVIEW_SESSIONS,
3376 WebMessageEnqueue, WebMessageIngress, WebMessageRejectReason, WebTag, WebViewCreateOptions,
3377 WebViewCreateSender, WebViewSessionSignals, block_on_scheme_future, document_port_context,
3378 next_native_webview_id, platform_console_delivery, remove_arc_if_matches,
3379 remove_session_signals_if_matches, replace_session_signals, should_sample_rejection,
3380 snapshot_web_message_context, web_message_bytes_within_limit,
3381 web_message_utf16_units_within_limit,
3382 };
3383 use crate::{
3384 ContextualSchemeRequest, DocumentBinding, IncomingWebMessage, NativeWebViewId,
3385 SchemeOutcome, SchemeRequestFrame, WebMessageContext, WebMessageFrame, WebMessageSource,
3386 WebMessageTransport,
3387 };
3388 use std::collections::HashMap;
3389 use std::sync::{Arc, Mutex, mpsc};
3390 use std::thread;
3391
3392 fn message(body: &str, native_view: crate::NativeWebViewId) -> IncomingWebMessage {
3393 IncomingWebMessage::new(
3394 body.to_string(),
3395 WebMessageContext::new(
3396 native_view,
3397 DocumentBinding::Unbound,
3398 WebMessageFrame::Unproven,
3399 WebMessageTransport::Other,
3400 WebMessageSource::unavailable(),
3401 ),
3402 )
3403 }
3404
3405 fn assert_browser_console_is_bound(backend: PlatformConsoleBackend) {
3406 assert_eq!(
3407 platform_console_delivery(SecurityProfile::BrowserRelaxed, backend),
3408 PlatformConsoleDelivery::RequiredV3Envelope
3409 );
3410 assert_eq!(
3411 platform_console_delivery(SecurityProfile::StrictDefault, backend),
3412 PlatformConsoleDelivery::DirectDelegate
3413 );
3414 }
3415
3416 #[test]
3417 fn apple_browser_console_requires_v3_envelope() {
3418 assert_browser_console_is_bound(PlatformConsoleBackend::Apple);
3419 }
3420
3421 #[test]
3422 fn android_browser_console_requires_v3_envelope() {
3423 assert_browser_console_is_bound(PlatformConsoleBackend::Android);
3424 }
3425
3426 #[test]
3427 fn harmony_browser_console_requires_v3_envelope() {
3428 assert_browser_console_is_bound(PlatformConsoleBackend::Harmony);
3429 }
3430
3431 #[test]
3432 fn windows_browser_console_requires_v3_envelope() {
3433 assert_browser_console_is_bound(PlatformConsoleBackend::Windows);
3434 }
3435
3436 #[test]
3437 fn native_webview_ids_are_not_reused_across_logical_tag_reuse() {
3438 let retired = next_native_webview_id();
3439 let replacement = next_native_webview_id();
3440 assert_ne!(retired, replacement);
3441
3442 let late_message = message("late", retired);
3443 assert_ne!(late_message.context().native_view(), replacement);
3444 assert_eq!(late_message.context().document(), DocumentBinding::Unbound);
3445 }
3446
3447 #[test]
3448 fn document_port_top_level_proof_requires_current_generation() {
3449 let native_view = next_native_webview_id();
3450 let current = crate::DocumentGeneration::new(42);
3451 let stale = crate::DocumentGeneration::new(41);
3452
3453 let context = document_port_context(
3454 native_view,
3455 DocumentBinding::Bound(current),
3456 current,
3457 WebMessageTransport::AndroidMessagePort,
3458 WebMessageSource::unavailable(),
3459 )
3460 .expect("the exact document port proves the top-level document");
3461 assert_eq!(context.frame(), WebMessageFrame::TopLevel);
3462 assert_eq!(context.document(), DocumentBinding::Bound(current));
3463
3464 assert!(
3465 document_port_context(
3466 native_view,
3467 DocumentBinding::Bound(current),
3468 stale,
3469 WebMessageTransport::AndroidMessagePort,
3470 WebMessageSource::unavailable(),
3471 )
3472 .is_none()
3473 );
3474 assert!(
3475 document_port_context(
3476 native_view,
3477 DocumentBinding::Bound(current),
3478 current,
3479 WebMessageTransport::AndroidJavascriptInterface,
3480 WebMessageSource::unavailable(),
3481 )
3482 .is_none()
3483 );
3484
3485 assert_eq!(
3487 document_port_context(
3488 native_view,
3489 DocumentBinding::Bound(current),
3490 current,
3491 WebMessageTransport::HarmonyMessagePort,
3492 WebMessageSource::unavailable(),
3493 )
3494 .expect("a current Harmony port proves the top-level document")
3495 .frame(),
3496 WebMessageFrame::TopLevel
3497 );
3498 assert!(
3499 document_port_context(
3500 native_view,
3501 DocumentBinding::Bound(current),
3502 stale,
3503 WebMessageTransport::HarmonyMessagePort,
3504 WebMessageSource::unavailable(),
3505 )
3506 .is_none()
3507 );
3508 }
3509
3510 #[test]
3511 fn legacy_scheme_handler_runs_from_contextual_ingress() {
3512 let options = WebViewCreateOptions::strict().on_scheme("lx", |request| async move {
3513 assert_eq!(request.uri(), "lx://app/index.html");
3514 SchemeOutcome::PassThrough
3515 });
3516 let (_, callbacks) = options.normalize().unwrap();
3517 let handler = callbacks.scheme_handlers.get("lx").unwrap();
3518 let outcome = block_on_scheme_future(handler(ContextualSchemeRequest::new(
3519 http::Request::builder()
3520 .uri("lx://app/index.html")
3521 .body(Vec::new())
3522 .unwrap(),
3523 NativeWebViewId::new(101),
3524 SchemeRequestFrame::TopLevelDocument,
3525 )));
3526 assert!(matches!(outcome, SchemeOutcome::PassThrough));
3527 }
3528
3529 #[test]
3530 fn ingress_snapshots_document_binding_at_enqueue_time() {
3531 let webtag = WebTag::new("test-app", "binding-snapshot", Some(1));
3532 let native_view = next_native_webview_id();
3533 crate::events::normalizer::begin(&webtag, native_view);
3534 crate::events::normalizer::submit(
3535 &webtag,
3536 native_view,
3537 crate::events::normalizer::NativeSignal::NavigationStarted {
3538 key: Some(71),
3539 url: "https://first/".into(),
3540 },
3541 );
3542 crate::events::normalizer::submit(
3543 &webtag,
3544 native_view,
3545 crate::events::normalizer::NativeSignal::DocumentCommitted { key: Some(71) },
3546 );
3547
3548 let ingress = WebMessageIngress::default();
3549 let bound = IncomingWebMessage::new(
3550 "bound".to_string(),
3551 snapshot_web_message_context(
3552 native_view,
3553 WebMessageFrame::Unproven,
3554 WebMessageTransport::Other,
3555 WebMessageSource::unavailable(),
3556 ),
3557 );
3558 assert_eq!(
3559 bound.context().document(),
3560 DocumentBinding::Bound(crate::DocumentGeneration::new(1))
3561 );
3562 assert_eq!(ingress.enqueue(bound), WebMessageEnqueue::Schedule);
3563
3564 crate::events::normalizer::submit(
3565 &webtag,
3566 native_view,
3567 crate::events::normalizer::NativeSignal::NavigationStarted {
3568 key: Some(72),
3569 url: "https://second/".into(),
3570 },
3571 );
3572 let revoked = IncomingWebMessage::new(
3573 "revoked".to_string(),
3574 snapshot_web_message_context(
3575 native_view,
3576 WebMessageFrame::Unproven,
3577 WebMessageTransport::Other,
3578 WebMessageSource::unavailable(),
3579 ),
3580 );
3581 assert_eq!(revoked.context().document(), DocumentBinding::Unbound);
3582 assert_eq!(ingress.enqueue(revoked), WebMessageEnqueue::Queued);
3583
3584 let mut bindings = Vec::new();
3585 while let Some(message) = ingress.begin_delivery() {
3586 bindings.push(message.context().document());
3587 ingress.finish_delivery();
3588 }
3589 assert_eq!(
3590 bindings,
3591 vec![
3592 DocumentBinding::Bound(crate::DocumentGeneration::new(1)),
3593 DocumentBinding::Unbound,
3594 ]
3595 );
3596 crate::events::normalizer::destroy(&webtag);
3597 }
3598
3599 #[test]
3600 fn message_ingress_preserves_fifo_across_reentrant_enqueue() {
3601 let ingress = WebMessageIngress::default();
3602 let native_view = next_native_webview_id();
3603 let delivered = Mutex::new(Vec::new());
3604
3605 assert_eq!(
3606 ingress.enqueue(message("first", native_view)),
3607 WebMessageEnqueue::Schedule
3608 );
3609 while let Some(incoming) = ingress.begin_delivery() {
3610 let body = incoming.body().to_string();
3611 delivered.lock().unwrap().push(body.clone());
3612 if body == "first" {
3613 assert_eq!(
3614 ingress.enqueue(message("second", native_view)),
3615 WebMessageEnqueue::Queued
3616 );
3617 assert_eq!(
3618 ingress.enqueue(message("third", native_view)),
3619 WebMessageEnqueue::Queued
3620 );
3621 }
3622 ingress.finish_delivery();
3623 }
3624
3625 assert_eq!(*delivered.lock().unwrap(), vec!["first", "second", "third"]);
3626 }
3627
3628 #[test]
3629 fn message_ingress_bounds_untrusted_backlog_without_reordering_accepted_messages() {
3630 let ingress = WebMessageIngress::default();
3631 let native_view = next_native_webview_id();
3632 assert_eq!(
3633 ingress.enqueue(message("0", native_view)),
3634 WebMessageEnqueue::Schedule
3635 );
3636 for index in 1..MAX_PENDING_WEB_MESSAGES {
3637 assert_eq!(
3638 ingress.enqueue(message(&index.to_string(), native_view)),
3639 WebMessageEnqueue::Queued
3640 );
3641 }
3642 assert_eq!(
3643 ingress.enqueue(message("overflow", native_view)),
3644 WebMessageEnqueue::Rejected {
3645 reason: WebMessageRejectReason::QueueCountLimit,
3646 count: 1,
3647 }
3648 );
3649
3650 let mut accepted = Vec::new();
3651 while let Some(incoming) = ingress.begin_delivery() {
3652 accepted.push(incoming.body().to_owned());
3653 ingress.finish_delivery();
3654 }
3655 assert_eq!(accepted.len(), MAX_PENDING_WEB_MESSAGES);
3656 assert_eq!(accepted.first().map(String::as_str), Some("0"));
3657 let last = (MAX_PENDING_WEB_MESSAGES - 1).to_string();
3658 assert_eq!(accepted.last().map(String::as_str), Some(last.as_str()));
3659 }
3660
3661 #[test]
3662 fn message_ingress_rejects_an_oversized_message_before_queueing() {
3663 let ingress = WebMessageIngress::default();
3664 let native_view = next_native_webview_id();
3665 let oversized = "x".repeat(MAX_WEB_MESSAGE_BYTES + 1);
3666
3667 assert_eq!(
3668 ingress.enqueue(message(&oversized, native_view)),
3669 WebMessageEnqueue::Rejected {
3670 reason: WebMessageRejectReason::MessageTooLarge,
3671 count: 1,
3672 }
3673 );
3674 assert_eq!(ingress.queued_bytes(), 0);
3675 assert!(ingress.begin_delivery().is_none());
3676 }
3677
3678 #[test]
3679 fn raw_web_message_byte_cap_includes_the_exact_boundary() {
3680 assert!(web_message_bytes_within_limit(MAX_WEB_MESSAGE_BYTES - 1));
3681 assert!(web_message_bytes_within_limit(MAX_WEB_MESSAGE_BYTES));
3682 assert!(!web_message_bytes_within_limit(MAX_WEB_MESSAGE_BYTES + 1));
3683 assert!(web_message_utf16_units_within_limit(MAX_WEB_MESSAGE_BYTES));
3684 assert!(!web_message_utf16_units_within_limit(
3685 MAX_WEB_MESSAGE_BYTES + 1
3686 ));
3687 assert!(!web_message_bytes_within_limit(usize::MAX));
3688 }
3689
3690 #[test]
3691 fn message_ingress_byte_budget_is_released_on_delivery_and_close() {
3692 let ingress = WebMessageIngress::default();
3693 let native_view = next_native_webview_id();
3694 let chunk = "x".repeat(MAX_WEB_MESSAGE_BYTES);
3695 let accepted = MAX_PENDING_WEB_MESSAGE_BYTES / MAX_WEB_MESSAGE_BYTES;
3696
3697 for index in 0..accepted {
3698 assert_eq!(
3699 ingress.enqueue(message(&chunk, native_view)),
3700 if index == 0 {
3701 WebMessageEnqueue::Schedule
3702 } else {
3703 WebMessageEnqueue::Queued
3704 }
3705 );
3706 }
3707 assert_eq!(ingress.queued_bytes(), MAX_PENDING_WEB_MESSAGE_BYTES);
3708 assert_eq!(
3709 ingress.enqueue(message("overflow", native_view)),
3710 WebMessageEnqueue::Rejected {
3711 reason: WebMessageRejectReason::QueueByteLimit,
3712 count: 1,
3713 }
3714 );
3715
3716 let first = ingress.begin_delivery().expect("first queued message");
3717 assert_eq!(first.body().len(), MAX_WEB_MESSAGE_BYTES);
3718 assert_eq!(
3719 ingress.queued_bytes(),
3720 MAX_PENDING_WEB_MESSAGE_BYTES - MAX_WEB_MESSAGE_BYTES
3721 );
3722 ingress.finish_delivery();
3723 assert_eq!(
3724 ingress.enqueue(message(&chunk, native_view)),
3725 WebMessageEnqueue::Queued
3726 );
3727 ingress.close();
3728 assert_eq!(ingress.queued_bytes(), 0);
3729 assert!(ingress.begin_delivery().is_none());
3730 }
3731
3732 #[test]
3733 fn ingress_rejection_counters_are_reason_coded_and_logs_are_sampled() {
3734 let ingress = WebMessageIngress::default();
3735 let native_view = next_native_webview_id();
3736 let oversized = "x".repeat(MAX_WEB_MESSAGE_BYTES + 1);
3737 assert_eq!(
3738 ingress.reject(WebMessageRejectReason::MessageTooLarge),
3739 WebMessageEnqueue::Rejected {
3740 reason: WebMessageRejectReason::MessageTooLarge,
3741 count: 1,
3742 }
3743 );
3744 for expected_count in 2..=4 {
3745 assert_eq!(
3746 ingress.enqueue(message(&oversized, native_view)),
3747 WebMessageEnqueue::Rejected {
3748 reason: WebMessageRejectReason::MessageTooLarge,
3749 count: expected_count,
3750 }
3751 );
3752 }
3753 assert_eq!(
3754 ingress.rejection_count(WebMessageRejectReason::MessageTooLarge),
3755 4
3756 );
3757 assert!(should_sample_rejection(1));
3758 assert!(should_sample_rejection(2));
3759 assert!(!should_sample_rejection(3));
3760 assert!(should_sample_rejection(4));
3761 }
3762
3763 #[test]
3764 fn message_ingress_preserves_fifo_across_producer_threads() {
3765 let ingress = Arc::new(WebMessageIngress::default());
3766 let native_view = next_native_webview_id();
3767 let (a_to_b_tx, a_to_b_rx) = mpsc::channel();
3768 let (b_to_a_tx, b_to_a_rx) = mpsc::channel();
3769
3770 let a_ingress = Arc::clone(&ingress);
3771 let producer_a = thread::spawn(move || {
3772 assert_eq!(
3773 a_ingress.enqueue(message("0", native_view)),
3774 WebMessageEnqueue::Schedule
3775 );
3776 a_to_b_tx.send(()).unwrap();
3777 for value in (2..64).step_by(2) {
3778 b_to_a_rx.recv().unwrap();
3779 assert_eq!(
3780 a_ingress.enqueue(message(&value.to_string(), native_view)),
3781 WebMessageEnqueue::Queued
3782 );
3783 a_to_b_tx.send(()).unwrap();
3784 }
3785 });
3786
3787 let b_ingress = Arc::clone(&ingress);
3788 let producer_b = thread::spawn(move || {
3789 for value in (1..64).step_by(2) {
3790 a_to_b_rx.recv().unwrap();
3791 assert_eq!(
3792 b_ingress.enqueue(message(&value.to_string(), native_view)),
3793 WebMessageEnqueue::Queued
3794 );
3795 if value != 63 {
3796 b_to_a_tx.send(()).unwrap();
3797 }
3798 }
3799 });
3800
3801 producer_a.join().unwrap();
3802 producer_b.join().unwrap();
3803
3804 let mut accepted = Vec::new();
3805 while let Some(incoming) = ingress.begin_delivery() {
3806 accepted.push(incoming.body().to_owned());
3807 ingress.finish_delivery();
3808 }
3809 assert_eq!(
3810 accepted,
3811 (0..64).map(|value| value.to_string()).collect::<Vec<_>>()
3812 );
3813 }
3814
3815 #[test]
3816 fn destroying_ingress_discards_queued_messages_but_allows_admitted_delivery() {
3817 let ingress = Arc::new(WebMessageIngress::default());
3818 let native_view = next_native_webview_id();
3819 assert_eq!(
3820 ingress.enqueue(message("in-flight", native_view)),
3821 WebMessageEnqueue::Schedule
3822 );
3823 assert_eq!(
3824 ingress.enqueue(message("queued", native_view)),
3825 WebMessageEnqueue::Queued
3826 );
3827
3828 let delivered = Mutex::new(Vec::new());
3829 let ingress_for_delivery = Arc::clone(&ingress);
3830 ingress.drain(|incoming| {
3831 delivered.lock().unwrap().push(incoming.body().to_owned());
3832 ingress_for_delivery.close();
3833 assert_eq!(
3834 ingress_for_delivery.enqueue(message("after-close", native_view)),
3835 WebMessageEnqueue::Rejected {
3836 reason: WebMessageRejectReason::Closed,
3837 count: 1,
3838 }
3839 );
3840 });
3841
3842 assert_eq!(*delivered.lock().unwrap(), vec!["in-flight"]);
3843 assert!(ingress.begin_delivery().is_none());
3844 }
3845
3846 #[test]
3847 fn closing_before_registry_removal_prevents_queued_message_admission() {
3848 let ingress = WebMessageIngress::default();
3849 let native_view = next_native_webview_id();
3850 assert_eq!(
3851 ingress.enqueue(message("queued", native_view)),
3852 WebMessageEnqueue::Schedule
3853 );
3854
3855 ingress.close();
3858
3859 assert!(ingress.begin_delivery().is_none());
3860 assert_eq!(
3861 ingress.enqueue(message("late", native_view)),
3862 WebMessageEnqueue::Rejected {
3863 reason: WebMessageRejectReason::Closed,
3864 count: 1,
3865 }
3866 );
3867 }
3868
3869 #[test]
3870 fn delegate_panic_does_not_stall_following_messages() {
3871 let ingress = WebMessageIngress::default();
3872 let native_view = next_native_webview_id();
3873 assert_eq!(
3874 ingress.enqueue(message("panic", native_view)),
3875 WebMessageEnqueue::Schedule
3876 );
3877 assert_eq!(
3878 ingress.enqueue(message("after", native_view)),
3879 WebMessageEnqueue::Queued
3880 );
3881
3882 let delivered = Mutex::new(Vec::new());
3883 ingress.drain(|incoming| {
3884 if incoming.body() == "panic" {
3885 panic!("test delegate panic");
3886 }
3887 delivered.lock().unwrap().push(incoming.body().to_owned());
3888 });
3889
3890 assert_eq!(*delivered.lock().unwrap(), vec!["after"]);
3891 assert_eq!(
3892 ingress.enqueue(message("recovered", native_view)),
3893 WebMessageEnqueue::Schedule
3894 );
3895 }
3896
3897 #[test]
3898 fn conditional_instance_removal_uses_arc_identity() {
3899 let current = Arc::new(7_u8);
3900 let same_value_different_instance = Arc::new(7_u8);
3901 let mut entries = HashMap::from([("tab".to_string(), current.clone())]);
3902
3903 assert!(
3904 remove_arc_if_matches(&mut entries, "tab", &same_value_different_instance).is_none()
3905 );
3906 assert!(Arc::ptr_eq(entries.get("tab").unwrap(), ¤t));
3907
3908 let removed = remove_arc_if_matches(&mut entries, "tab", ¤t).unwrap();
3909 assert!(Arc::ptr_eq(&removed, ¤t));
3910 assert!(!entries.contains_key("tab"));
3911 }
3912
3913 #[test]
3914 fn superseded_sender_completes_without_removing_current_generation() {
3915 let webtag = WebTag::from("test:pages/superseded#9173");
3916 let superseded = WebViewSessionSignals::new();
3917 let current = WebViewSessionSignals::new();
3918 replace_session_signals(&webtag, current.clone());
3919
3920 WebViewCreateSender::new(webtag.clone(), superseded.clone()).cancel_superseded();
3921
3922 assert!(
3923 superseded
3924 .terminal_result()
3925 .is_some_and(|result| result.is_err())
3926 );
3927 assert!(current.terminal_result().is_none());
3928 let sessions = WEBVIEW_SESSIONS.get().unwrap().lock().unwrap();
3929 assert!(
3930 sessions
3931 .get(webtag.key())
3932 .is_some_and(|signals| Arc::ptr_eq(signals, ¤t))
3933 );
3934 drop(sessions);
3935 assert!(remove_session_signals_if_matches(&webtag, ¤t));
3936 }
3937}