Skip to main content

cbf_chrome/bridge/
client.rs

1#![allow(non_upper_case_globals)]
2
3use std::{
4    ffi::{CStr, CString},
5    os::raw::c_char,
6    ptr,
7    time::Duration,
8};
9
10use cbf::data::{edit::EditAction, window_open::WindowOpenResponse};
11use cbf_chrome_sys::{
12    bridge::{BridgeLibrary, BridgeLoadError, bridge},
13    ffi::*,
14};
15use tracing::warn;
16
17use super::map::{
18    ime_range_to_ffi, key_event_type_to_ffi, mouse_button_to_ffi, mouse_event_type_to_ffi,
19    parse_event, parse_extension_list, pointer_type_to_ffi, scroll_granularity_to_ffi,
20    to_ffi_ime_text_spans,
21};
22use super::utils::{c_string_to_string, to_optional_cstring};
23use super::{BridgeError, IpcEvent};
24use crate::data::{
25    background::ChromeBackgroundPolicy,
26    browsing_context_open::ChromeBrowsingContextOpenResponse,
27    custom_scheme::{ChromeCustomSchemeResponse, ChromeCustomSchemeResponseResult},
28    download::ChromeDownloadId,
29    drag::{
30        ChromeDragData, ChromeDragDrop, ChromeDragUpdate, ChromeExternalDragDrop,
31        ChromeExternalDragEnter, ChromeExternalDragUpdate,
32    },
33    execution::ChromeTabExecutionState,
34    extension::ChromeExtensionInfo,
35    find::{ChromeFindInPageOptions, ChromeStopFindAction},
36    ids::{PopupId, TabId},
37    ime::{
38        ChromeConfirmCompositionBehavior, ChromeImeCommitText, ChromeImeComposition,
39        ChromeTransientImeCommitText, ChromeTransientImeComposition,
40    },
41    input::{ChromeKeyEvent, ChromeMouseWheelEvent},
42    ipc::{TabIpcConfig, TabIpcErrorCode, TabIpcMessage, TabIpcMessageType, TabIpcPayload},
43    mouse::ChromeMouseEvent,
44    navigation::ChromeNavigationEntryId,
45    policy::{ChromeBrowsingContextPolicy, ChromeCapabilityPolicy, ChromeIpcPolicy},
46    profile::ChromeProfileInfo,
47    prompt_ui::{PromptUiId, PromptUiResponse},
48    visibility::ChromeTabVisibility,
49};
50
51fn execution_state_to_ffi(state: ChromeTabExecutionState) -> u8 {
52    match state {
53        ChromeTabExecutionState::Running => CbfTabExecutionState_kCbfTabExecutionStateRunning as u8,
54        ChromeTabExecutionState::Suspended => {
55            CbfTabExecutionState_kCbfTabExecutionStateSuspended as u8
56        }
57    }
58}
59
60/// Client wrapper for the CBF IPC bridge.
61pub struct IpcClient {
62    inner: *mut CbfBridgeClientHandle,
63}
64
65#[derive(Debug, Clone, Copy)]
66pub(crate) struct IpcEventWaitHandle {
67    inner: *mut CbfBridgeClientHandle,
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq)]
71pub enum EventWaitResult {
72    EventAvailable,
73    TimedOut,
74    Disconnected,
75    Closed,
76}
77
78// SAFETY: IpcClient owns the bridge client handle and its methods
79// serialize access through the Mojo thread internally. The handle
80// is not shared, only moved across the process::start_chromium →
81// backend thread boundary exactly once.
82unsafe impl Send for IpcClient {}
83// SAFETY: `cbf_bridge_client_wait_for_event` synchronizes through the bridge's
84// internal event wait state. This handle is non-owning and only used while the
85// owning `IpcClient` remains alive.
86unsafe impl Send for IpcEventWaitHandle {}
87// SAFETY: The bridge wait path is internally synchronized; this wrapper only
88// exposes `wait_for_event` and does not own the underlying handle.
89unsafe impl Sync for IpcEventWaitHandle {}
90
91macro_rules! bridge_call {
92    ($method:ident ( $($arg:expr),* $(,)? )) => {{
93        let bridge = bridge_api()?;
94        unsafe { bridge.$method($($arg),*) }
95    }};
96}
97
98impl std::fmt::Debug for IpcClient {
99    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
100        f.debug_struct("IpcClient")
101            .field("inner", &format!("{:p}", self.inner))
102            .finish()
103    }
104}
105
106impl IpcClient {
107    /// Override the base bundle ID used for Mach rendezvous on macOS.
108    ///
109    /// Must be called before `prepare_channel`.
110    pub fn set_base_bundle_id(bundle_id: &str) -> Result<(), BridgeError> {
111        let bundle_id = CString::new(bundle_id).map_err(|_| BridgeError::InvalidInput)?;
112        bridge_call!(cbf_bridge_set_base_bundle_id(bundle_id.as_ptr()));
113
114        Ok(())
115    }
116
117    /// Prepare the Mojo channel before spawning the Chromium process and, on
118    /// macOS, hold the rendezvous lock until the child pid is registered.
119    ///
120    /// Returns `(remote_fd, switch_arg)` where:
121    /// - `remote_fd` is the file descriptor of the remote channel endpoint that
122    ///   must be inherited by the child process (Unix only; -1 on other platforms).
123    /// - `switch_arg` is the command-line switch that Chromium needs to recover
124    ///   the endpoint (e.g. `--cbf-ipc-handle=...`).
125    pub fn prepare_channel_and_lock() -> Result<(i32, String), BridgeError> {
126        let mut buf = [0u8; 512];
127
128        let fd = {
129            bridge_call!(cbf_bridge_init());
130            bridge_call!(cbf_bridge_prepare_channel_and_lock(
131                buf.as_mut_ptr() as *mut std::os::raw::c_char,
132                buf.len() as i32,
133            ))
134        };
135
136        let switch_arg = parse_channel_switch_arg(&buf)?;
137
138        Ok((fd, switch_arg))
139    }
140
141    /// Notify the bridge of the spawned child's PID.
142    ///
143    /// Must be called after spawning the Chromium process and before
144    /// `connect_inherited`. On macOS this registers the Mach port with the
145    /// rendezvous server; on other platforms it completes channel bookkeeping.
146    pub fn pass_child_pid_and_unlock(pid: u32) {
147        if let Err(err) = bridge_api()
148            .map(|bridge| unsafe { bridge.cbf_bridge_pass_child_pid_and_unlock(pid as i64) })
149        {
150            warn!(error = ?err, "failed to pass child pid to bridge");
151        }
152    }
153
154    /// Abort a prepared child launch and release any rendezvous lock.
155    pub fn abort_channel_launch() {
156        if let Err(err) =
157            bridge_api().map(|bridge| unsafe { bridge.cbf_bridge_abort_channel_launch() })
158        {
159            warn!(error = ?err, "failed to abort bridge child launch");
160        }
161    }
162
163    /// Wrap a pre-created bridge client handle and complete the Mojo connection.
164    ///
165    /// `inner` must have been created by `cbf_bridge_client_create()` and the
166    /// channel must have been prepared with `prepare_channel_and_lock()` before
167    /// calling this function (after the child process has been spawned).
168    ///
169    /// # Safety
170    ///
171    /// `inner` must be a valid, live `CbfBridgeClientHandle` allocated by
172    /// `cbf_bridge_client_create()`. Ownership is transferred to the returned
173    /// `IpcClient` on success and consumed by this function on failure.
174    pub unsafe fn connect_inherited(
175        inner: *mut CbfBridgeClientHandle,
176    ) -> Result<Self, BridgeError> {
177        if inner.is_null() {
178            return Err(BridgeError::InvalidState);
179        }
180
181        let connected = bridge_call!(cbf_bridge_client_connect_inherited(inner));
182
183        if !connected {
184            warn!(
185                result = "err",
186                error = "ipc_connect_inherited_failed",
187                "IPC inherited connect failed"
188            );
189            cleanup_bridge_call("destroy bridge client after connect failure", |bridge| {
190                unsafe { bridge.cbf_bridge_client_destroy(inner) };
191            });
192
193            return Err(BridgeError::ConnectionFailed);
194        }
195
196        Ok(Self { inner })
197    }
198
199    /// Authenticate with the session token and set up the browser observer.
200    ///
201    /// Must be called once after `connect_inherited` and before any other method.
202    pub fn authenticate(&self, token: &str) -> Result<(), BridgeError> {
203        if self.inner.is_null() {
204            return Err(BridgeError::InvalidState);
205        }
206
207        let token = CString::new(token).map_err(|_| BridgeError::InvalidInput)?;
208        authentication_result(bridge_call!(cbf_bridge_client_authenticate(
209            self.inner,
210            token.as_ptr()
211        )))
212    }
213
214    /// Wait until an event is available or the bridge closes.
215    pub fn wait_for_event(
216        &self,
217        timeout: Option<Duration>,
218    ) -> Result<EventWaitResult, BridgeError> {
219        wait_for_event_inner(self.inner, timeout)
220    }
221
222    pub(crate) fn event_wait_handle(&self) -> IpcEventWaitHandle {
223        IpcEventWaitHandle { inner: self.inner }
224    }
225
226    /// Poll the next IPC event, if any, from the backend.
227    pub fn poll_event(&mut self) -> Option<Result<IpcEvent, BridgeError>> {
228        if self.inner.is_null() {
229            return None;
230        }
231
232        let mut event = CbfBridgeEvent::default();
233        let polled = bridge_api()
234            .map(|bridge| unsafe { bridge.cbf_bridge_client_poll_event(self.inner, &mut event) })
235            .ok()?;
236
237        if !polled {
238            return None;
239        }
240
241        let parsed = parse_event(event);
242        cleanup_bridge_call("free bridge event", |bridge| {
243            unsafe { bridge.cbf_bridge_event_free(&mut event) };
244        });
245
246        if let Err(err) = &parsed {
247            warn!(
248                result = "err",
249                error = "ipc_event_parse_failed",
250                err = ?err,
251                "IPC event parse failed"
252            );
253        }
254
255        Some(parsed)
256    }
257
258    /// Retrieve the list of browser profiles from the backend.
259    pub fn list_profiles(&mut self) -> Result<Vec<ChromeProfileInfo>, BridgeError> {
260        self.ensure_ready()?;
261
262        let mut list = CbfProfileList::default();
263        if !bridge_call!(cbf_bridge_client_get_profiles(self.inner, &mut list)) {
264            return Err(BridgeError::OperationFailed {
265                operation: "list_profiles",
266            });
267        }
268
269        let profiles = if list.len == 0 || list.profiles.is_null() {
270            &[]
271        } else {
272            unsafe { std::slice::from_raw_parts(list.profiles, list.len as usize) }
273        };
274        let mut result = Vec::with_capacity(profiles.len());
275
276        for profile in profiles {
277            result.push(ChromeProfileInfo {
278                profile_id: c_string_to_string(profile.profile_id),
279                profile_path: c_string_to_string(profile.profile_path),
280                display_name: c_string_to_string(profile.display_name),
281                is_default: profile.is_default,
282            });
283        }
284
285        cleanup_bridge_call("free profile list", |bridge| {
286            unsafe { bridge.cbf_bridge_profile_list_free(&mut list) };
287        });
288
289        Ok(result)
290    }
291
292    /// Retrieve the list of extensions from the backend.
293    pub fn list_extensions(
294        &mut self,
295        profile_id: &str,
296    ) -> Result<Vec<ChromeExtensionInfo>, BridgeError> {
297        self.ensure_ready()?;
298
299        let profile = CString::new(profile_id).map_err(|_| BridgeError::InvalidInput)?;
300
301        let mut list = CbfExtensionInfoList::default();
302        if !bridge_call!(cbf_bridge_client_list_extensions(
303            self.inner,
304            profile.as_ptr(),
305            &mut list
306        )) {
307            return Err(BridgeError::OperationFailed {
308                operation: "list_extensions",
309            });
310        }
311
312        let result = parse_extension_list(list);
313
314        cleanup_bridge_call("free extension list", |bridge| {
315            unsafe { bridge.cbf_bridge_extension_list_free(&mut list) };
316        });
317        Ok(result)
318    }
319
320    pub fn register_custom_scheme_handler(
321        &mut self,
322        scheme: &str,
323        host: &str,
324    ) -> Result<(), BridgeError> {
325        self.ensure_ready()?;
326
327        let scheme = CString::new(scheme).map_err(|_| BridgeError::InvalidInput)?;
328        let host = CString::new(host).map_err(|_| BridgeError::InvalidInput)?;
329        bridge_ok(
330            "register_custom_scheme_handler",
331            bridge_call!(cbf_bridge_client_register_custom_scheme_handler(
332                self.inner,
333                scheme.as_ptr(),
334                host.as_ptr(),
335            )),
336        )
337    }
338
339    pub fn respond_custom_scheme_request(
340        &mut self,
341        response: &ChromeCustomSchemeResponse,
342    ) -> Result<(), BridgeError> {
343        self.ensure_ready()?;
344
345        let mime_type =
346            CString::new(response.mime_type.as_str()).map_err(|_| BridgeError::InvalidInput)?;
347        let content_security_policy = to_optional_cstring(&response.content_security_policy)
348            .map_err(|_| BridgeError::InvalidInput)?;
349        let access_control_allow_origin =
350            to_optional_cstring(&response.access_control_allow_origin)
351                .map_err(|_| BridgeError::InvalidInput)?;
352
353        let result = match response.result {
354            ChromeCustomSchemeResponseResult::Ok => {
355                CbfCustomSchemeResponseResult_kCbfCustomSchemeResponseResultOk
356            }
357            ChromeCustomSchemeResponseResult::NotFound => {
358                CbfCustomSchemeResponseResult_kCbfCustomSchemeResponseResultNotFound
359            }
360            ChromeCustomSchemeResponseResult::Aborted => {
361                CbfCustomSchemeResponseResult_kCbfCustomSchemeResponseResultAborted
362            }
363        } as u8;
364
365        let body_ptr = if response.body.is_empty() {
366            ptr::null()
367        } else {
368            response.body.as_ptr()
369        };
370
371        bridge_ok(
372            "respond_custom_scheme_request",
373            bridge_call!(cbf_bridge_client_respond_custom_scheme_request(
374                self.inner,
375                response.request_id,
376                result,
377                mime_type.as_ptr(),
378                content_security_policy
379                    .as_ref()
380                    .map_or(ptr::null(), |value| value.as_ptr()),
381                access_control_allow_origin
382                    .as_ref()
383                    .map_or(ptr::null(), |value| value.as_ptr()),
384                body_ptr,
385                response.body.len() as u32,
386            )),
387        )
388    }
389
390    pub fn activate_extension_action(
391        &mut self,
392        browsing_context_id: TabId,
393        extension_id: &str,
394    ) -> Result<(), BridgeError> {
395        self.ensure_ready()?;
396
397        let extension_id = CString::new(extension_id).map_err(|_| BridgeError::InvalidInput)?;
398        bridge_ok(
399            "activate_extension_action",
400            bridge_call!(cbf_bridge_client_activate_extension_action(
401                self.inner,
402                browsing_context_id.get(),
403                extension_id.as_ptr(),
404            )),
405        )
406    }
407
408    /// Create a tab via the IPC bridge.
409    pub fn create_tab(
410        &mut self,
411        request_id: u64,
412        initial_url: &str,
413        profile_id: &str,
414        policy: Option<&ChromeBrowsingContextPolicy>,
415    ) -> Result<(), BridgeError> {
416        self.ensure_ready()?;
417
418        let url = CString::new(initial_url).map_err(|_| BridgeError::InvalidInput)?;
419        let profile = CString::new(profile_id).map_err(|_| BridgeError::InvalidInput)?;
420        let allowed_origins = match policy.map(|policy| &policy.ipc) {
421            Some(ChromeIpcPolicy::Allow { allowed_origins }) => Some(
422                allowed_origins
423                    .iter()
424                    .map(|origin| CString::new(origin.as_str()))
425                    .collect::<Result<Vec<_>, _>>()
426                    .map_err(|_| BridgeError::InvalidInput)?,
427            ),
428            _ => None,
429        };
430
431        let allowed_origin_ptrs: Vec<*const c_char> = allowed_origins
432            .as_ref()
433            .map(|origins| origins.iter().map(|origin| origin.as_ptr()).collect())
434            .unwrap_or_default();
435        let allowed_origin_list = CbfCommandList {
436            items: if allowed_origin_ptrs.is_empty() {
437                ptr::null_mut()
438            } else {
439                allowed_origin_ptrs.as_ptr() as *mut *const c_char
440            },
441            len: allowed_origin_ptrs.len() as u32,
442        };
443
444        let (has_policy, ipc_policy_kind, extensions_policy) = match policy {
445            Some(policy) => (
446                true,
447                match policy.ipc {
448                    ChromeIpcPolicy::Deny => {
449                        CbfBrowsingContextIpcPolicy_kCbfBrowsingContextIpcPolicyDeny as u8
450                    }
451                    ChromeIpcPolicy::Allow { .. } => {
452                        CbfBrowsingContextIpcPolicy_kCbfBrowsingContextIpcPolicyAllow as u8
453                    }
454                },
455                match policy.extensions {
456                    ChromeCapabilityPolicy::Allow => {
457                        CbfCapabilityPolicy_kCbfCapabilityPolicyAllow as u8
458                    }
459                    ChromeCapabilityPolicy::Deny => {
460                        CbfCapabilityPolicy_kCbfCapabilityPolicyDeny as u8
461                    }
462                },
463            ),
464            None => (
465                false,
466                CbfBrowsingContextIpcPolicy_kCbfBrowsingContextIpcPolicyDeny as u8,
467                CbfCapabilityPolicy_kCbfCapabilityPolicyAllow as u8,
468            ),
469        };
470
471        bridge_ok(
472            "create_tab",
473            bridge_call!(cbf_bridge_client_create_tab(
474                self.inner,
475                request_id,
476                url.as_ptr(),
477                profile.as_ptr(),
478                has_policy,
479                ipc_policy_kind,
480                &allowed_origin_list,
481                extensions_policy
482            )),
483        )
484    }
485
486    /// Request closing the specified tab.
487    pub fn request_close_tab(&mut self, browsing_context_id: TabId) -> Result<(), BridgeError> {
488        self.ensure_ready()?;
489
490        bridge_ok(
491            "request_close_tab",
492            bridge_call!(cbf_bridge_client_request_close_tab(
493                self.inner,
494                browsing_context_id.get()
495            )),
496        )
497    }
498
499    /// Start a non-destructive close transaction for multiple tabs.
500    pub fn begin_close_tabs_transaction(
501        &mut self,
502        request_id: u64,
503        browsing_context_ids: &[TabId],
504    ) -> Result<(), BridgeError> {
505        self.ensure_ready()?;
506
507        let tab_ids: Vec<u64> = browsing_context_ids.iter().map(|id| id.get()).collect();
508        let list = CbfUint64List {
509            items: if tab_ids.is_empty() {
510                ptr::null()
511            } else {
512                tab_ids.as_ptr()
513            },
514            len: tab_ids.len() as u32,
515        };
516
517        bridge_ok(
518            "begin_close_tabs_transaction",
519            bridge_call!(cbf_bridge_client_begin_close_tabs_transaction(
520                self.inner, request_id, &list
521            )),
522        )
523    }
524
525    /// Commit a previously prepared close transaction.
526    pub fn commit_close_tabs_transaction(&mut self, request_id: u64) -> Result<(), BridgeError> {
527        self.ensure_ready()?;
528
529        bridge_ok(
530            "commit_close_tabs_transaction",
531            bridge_call!(cbf_bridge_client_commit_close_tabs_transaction(
532                self.inner, request_id
533            )),
534        )
535    }
536
537    /// Cancel a previously prepared close transaction.
538    pub fn cancel_close_tabs_transaction(&mut self, request_id: u64) -> Result<(), BridgeError> {
539        self.ensure_ready()?;
540
541        bridge_ok(
542            "cancel_close_tabs_transaction",
543            bridge_call!(cbf_bridge_client_cancel_close_tabs_transaction(
544                self.inner, request_id
545            )),
546        )
547    }
548
549    /// Update the surface size of the specified tab.
550    pub fn set_tab_size(
551        &mut self,
552        browsing_context_id: TabId,
553        width: u32,
554        height: u32,
555    ) -> Result<(), BridgeError> {
556        self.ensure_ready()?;
557
558        bridge_ok(
559            "set_tab_size",
560            bridge_call!(cbf_bridge_client_set_tab_size(
561                self.inner,
562                browsing_context_id.get(),
563                width,
564                height
565            )),
566        )
567    }
568
569    /// Update whether the specified tab should receive text input focus.
570    pub fn set_tab_focus(
571        &mut self,
572        browsing_context_id: TabId,
573        focused: bool,
574    ) -> Result<(), BridgeError> {
575        self.ensure_ready()?;
576
577        bridge_ok(
578            "set_tab_focus",
579            bridge_call!(cbf_bridge_client_set_tab_focus(
580                self.inner,
581                browsing_context_id.get(),
582                focused
583            )),
584        )
585    }
586
587    /// Update whether the specified tab should be treated as visible.
588    pub fn set_tab_visibility(
589        &mut self,
590        browsing_context_id: TabId,
591        visibility: ChromeTabVisibility,
592    ) -> Result<(), BridgeError> {
593        self.ensure_ready()?;
594
595        let visibility = match visibility {
596            ChromeTabVisibility::Visible => CbfTabVisibility_kCbfTabVisibilityVisible,
597            ChromeTabVisibility::Hidden => CbfTabVisibility_kCbfTabVisibilityHidden,
598        } as u8;
599
600        bridge_ok(
601            "set_tab_visibility",
602            bridge_call!(cbf_bridge_client_set_tab_visibility(
603                self.inner,
604                browsing_context_id.get(),
605                visibility,
606            )),
607        )
608    }
609
610    /// Update whether the specified tab should keep executing page activity.
611    pub fn set_tab_execution_state(
612        &mut self,
613        browsing_context_id: TabId,
614        state: ChromeTabExecutionState,
615    ) -> Result<(), BridgeError> {
616        self.ensure_ready()?;
617
618        bridge_ok(
619            "set_tab_execution_state",
620            bridge_call!(cbf_bridge_client_set_tab_execution_state(
621                self.inner,
622                browsing_context_id.get(),
623                execution_state_to_ffi(state),
624            )),
625        )
626    }
627
628    /// Enable browsing context IPC with explicit origin allow list.
629    pub fn enable_tab_ipc(
630        &mut self,
631        browsing_context_id: TabId,
632        config: &TabIpcConfig,
633    ) -> Result<(), BridgeError> {
634        self.ensure_ready()?;
635
636        let origin_cstrings: Result<Vec<CString>, _> = config
637            .allowed_origins
638            .iter()
639            .map(|origin| CString::new(origin.as_str()))
640            .collect();
641        let origin_cstrings = origin_cstrings.map_err(|_| BridgeError::InvalidInput)?;
642        let origin_ptrs: Vec<*const c_char> = origin_cstrings.iter().map(|s| s.as_ptr()).collect();
643
644        let list = CbfCommandList {
645            items: if origin_ptrs.is_empty() {
646                ptr::null_mut()
647            } else {
648                origin_ptrs.as_ptr() as *mut *const c_char
649            },
650            len: origin_ptrs.len() as u32,
651        };
652
653        bridge_ok(
654            "enable_tab_ipc",
655            bridge_call!(cbf_bridge_client_enable_tab_ipc(
656                self.inner,
657                browsing_context_id.get(),
658                &list
659            )),
660        )
661    }
662
663    /// Disable browsing context IPC.
664    pub fn disable_tab_ipc(&mut self, browsing_context_id: TabId) -> Result<(), BridgeError> {
665        self.ensure_ready()?;
666
667        bridge_ok(
668            "disable_tab_ipc",
669            bridge_call!(cbf_bridge_client_disable_tab_ipc(
670                self.inner,
671                browsing_context_id.get()
672            )),
673        )
674    }
675
676    /// Post host -> page IPC message.
677    pub fn post_tab_ipc_message(
678        &mut self,
679        browsing_context_id: TabId,
680        message: &TabIpcMessage,
681    ) -> Result<(), BridgeError> {
682        self.ensure_ready()?;
683
684        let channel =
685            CString::new(message.channel.as_str()).map_err(|_| BridgeError::InvalidInput)?;
686        let content_type =
687            to_optional_cstring(&message.content_type).map_err(|_| BridgeError::InvalidInput)?;
688        let payload_kind = match message.payload {
689            TabIpcPayload::Text(_) => CbfIpcPayloadKind_kCbfIpcPayloadText,
690            TabIpcPayload::Binary(_) => CbfIpcPayloadKind_kCbfIpcPayloadBinary,
691        } as u8;
692        let message_type = ipc_message_type_to_ffi(message.message_type);
693        let error_code = message
694            .error_code
695            .map(ipc_error_code_to_ffi)
696            .unwrap_or(CbfIpcErrorCode_kCbfIpcErrorNone as u8);
697        let (payload_text, payload_binary) = match &message.payload {
698            TabIpcPayload::Text(text) => (
699                Some(CString::new(text.as_str()).map_err(|_| BridgeError::InvalidInput)?),
700                Vec::new(),
701            ),
702            TabIpcPayload::Binary(binary) => (None, binary.clone()),
703        };
704
705        bridge_ok(
706            "post_tab_ipc_message",
707            bridge_call!(cbf_bridge_client_post_tab_ipc_message(
708                self.inner,
709                browsing_context_id.get(),
710                channel.as_ptr(),
711                message_type,
712                message.request_id,
713                payload_kind,
714                payload_text
715                    .as_ref()
716                    .map(|value| value.as_ptr())
717                    .unwrap_or(ptr::null()),
718                if payload_binary.is_empty() {
719                    ptr::null()
720                } else {
721                    payload_binary.as_ptr()
722                },
723                payload_binary.len() as u32,
724                content_type
725                    .as_ref()
726                    .map(|value| value.as_ptr())
727                    .unwrap_or(ptr::null()),
728                error_code,
729            )),
730        )
731    }
732
733    /// Update the page background policy of the specified tab.
734    pub fn set_tab_background_policy(
735        &mut self,
736        browsing_context_id: TabId,
737        policy: ChromeBackgroundPolicy,
738    ) -> Result<(), BridgeError> {
739        self.ensure_ready()?;
740
741        let transparent = matches!(policy, ChromeBackgroundPolicy::Transparent);
742
743        bridge_ok(
744            "set_tab_background_policy",
745            bridge_call!(cbf_bridge_client_set_tab_background_policy(
746                self.inner,
747                browsing_context_id.get(),
748                transparent,
749            )),
750        )
751    }
752
753    /// Respond to a beforeunload confirmation request.
754    pub fn confirm_beforeunload(
755        &mut self,
756        browsing_context_id: TabId,
757        request_id: u64,
758        proceed: bool,
759    ) -> Result<(), BridgeError> {
760        self.ensure_ready()?;
761
762        bridge_ok(
763            "confirm_beforeunload",
764            bridge_call!(cbf_bridge_client_confirm_beforeunload(
765                self.inner,
766                browsing_context_id.get(),
767                request_id,
768                proceed,
769            )),
770        )
771    }
772
773    /// Respond to a JavaScript dialog request for a tab.
774    pub fn respond_javascript_dialog(
775        &mut self,
776        browsing_context_id: TabId,
777        request_id: u64,
778        accept: bool,
779        prompt_text: Option<&str>,
780    ) -> Result<(), BridgeError> {
781        self.ensure_ready()?;
782
783        let prompt_text = to_optional_cstring(&prompt_text.map(ToOwned::to_owned))
784            .map_err(|_| BridgeError::InvalidInput)?;
785
786        bridge_ok(
787            "respond_javascript_dialog",
788            bridge_call!(cbf_bridge_client_respond_javascript_dialog(
789                self.inner,
790                browsing_context_id.get(),
791                request_id,
792                accept,
793                prompt_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
794            )),
795        )
796    }
797
798    /// Respond to a JavaScript dialog request for an extension popup.
799    pub fn respond_extension_popup_javascript_dialog(
800        &mut self,
801        popup_id: PopupId,
802        request_id: u64,
803        accept: bool,
804        prompt_text: Option<&str>,
805    ) -> Result<(), BridgeError> {
806        self.ensure_ready()?;
807
808        let prompt_text = to_optional_cstring(&prompt_text.map(ToOwned::to_owned))
809            .map_err(|_| BridgeError::InvalidInput)?;
810
811        bridge_ok(
812            "respond_extension_popup_javascript_dialog",
813            bridge_call!(cbf_bridge_client_respond_extension_popup_javascript_dialog(
814                self.inner,
815                popup_id.get(),
816                request_id,
817                accept,
818                prompt_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
819            )),
820        )
821    }
822
823    /// Navigate the page to the provided URL.
824    pub fn navigate(&mut self, browsing_context_id: TabId, url: &str) -> Result<(), BridgeError> {
825        self.ensure_ready()?;
826
827        let url = CString::new(url).map_err(|_| BridgeError::InvalidInput)?;
828
829        bridge_ok(
830            "navigate",
831            bridge_call!(cbf_bridge_client_navigate(
832                self.inner,
833                browsing_context_id.get(),
834                url.as_ptr()
835            )),
836        )
837    }
838
839    /// Navigate back in history for the page.
840    pub fn go_back(&mut self, browsing_context_id: TabId) -> Result<(), BridgeError> {
841        self.ensure_ready()?;
842
843        bridge_ok(
844            "go_back",
845            bridge_call!(cbf_bridge_client_go_back(
846                self.inner,
847                browsing_context_id.get()
848            )),
849        )
850    }
851
852    /// Navigate forward in history for the page.
853    pub fn go_forward(&mut self, browsing_context_id: TabId) -> Result<(), BridgeError> {
854        self.ensure_ready()?;
855
856        bridge_ok(
857            "go_forward",
858            bridge_call!(cbf_bridge_client_go_forward(
859                self.inner,
860                browsing_context_id.get()
861            )),
862        )
863    }
864
865    /// Reload the page, optionally ignoring caches.
866    pub fn reload(
867        &mut self,
868        browsing_context_id: TabId,
869        ignore_cache: bool,
870    ) -> Result<(), BridgeError> {
871        self.ensure_ready()?;
872
873        bridge_ok(
874            "reload",
875            bridge_call!(cbf_bridge_client_reload(
876                self.inner,
877                browsing_context_id.get(),
878                ignore_cache
879            )),
880        )
881    }
882
883    /// Open print preview for the page.
884    pub fn print_preview(&mut self, browsing_context_id: TabId) -> Result<(), BridgeError> {
885        self.ensure_ready()?;
886
887        bridge_ok(
888            "print_preview",
889            bridge_call!(cbf_bridge_client_print_preview(
890                self.inner,
891                browsing_context_id.get()
892            )),
893        )
894    }
895
896    /// Open DevTools for the specified page.
897    pub fn open_dev_tools(&mut self, browsing_context_id: TabId) -> Result<(), BridgeError> {
898        self.ensure_ready()?;
899
900        bridge_ok(
901            "open_dev_tools",
902            bridge_call!(cbf_bridge_client_open_dev_tools(
903                self.inner,
904                browsing_context_id.get()
905            )),
906        )
907    }
908
909    /// Open DevTools and inspect the element at the given coordinates.
910    pub fn inspect_element(
911        &mut self,
912        browsing_context_id: TabId,
913        x: i32,
914        y: i32,
915    ) -> Result<(), BridgeError> {
916        self.ensure_ready()?;
917
918        bridge_ok(
919            "inspect_element",
920            bridge_call!(cbf_bridge_client_inspect_element(
921                self.inner,
922                browsing_context_id.get(),
923                x,
924                y
925            )),
926        )
927    }
928
929    /// Request the DOM HTML for the specified page.
930    pub fn get_tab_dom_html(
931        &mut self,
932        browsing_context_id: TabId,
933        request_id: u64,
934    ) -> Result<(), BridgeError> {
935        self.ensure_ready()?;
936
937        bridge_ok(
938            "get_tab_dom_html",
939            bridge_call!(cbf_bridge_client_get_tab_dom_html(
940                self.inner,
941                browsing_context_id.get(),
942                request_id,
943            )),
944        )
945    }
946
947    /// Request a navigation history snapshot for the specified page.
948    pub fn get_navigation_history(
949        &mut self,
950        browsing_context_id: TabId,
951        request_id: u64,
952    ) -> Result<(), BridgeError> {
953        self.ensure_ready()?;
954
955        bridge_ok(
956            "get_navigation_history",
957            bridge_call!(cbf_bridge_client_get_navigation_history(
958                self.inner,
959                browsing_context_id.get(),
960                request_id,
961            )),
962        )
963    }
964
965    /// Traverse directly to a specific navigation history entry.
966    pub fn traverse_history_to_entry(
967        &mut self,
968        browsing_context_id: TabId,
969        entry_id: ChromeNavigationEntryId,
970    ) -> Result<(), BridgeError> {
971        self.ensure_ready()?;
972
973        let success = bridge_call!(cbf_bridge_client_traverse_history_to_entry(
974            self.inner,
975            browsing_context_id.get(),
976            entry_id.get(),
977        ));
978
979        if success {
980            Ok(())
981        } else {
982            Err(BridgeError::InvalidInput)
983        }
984    }
985
986    /// Traverse history relative to the current committed entry.
987    pub fn traverse_history_by_offset(
988        &mut self,
989        browsing_context_id: TabId,
990        delta: i32,
991    ) -> Result<(), BridgeError> {
992        self.ensure_ready()?;
993
994        bridge_ok(
995            "traverse_history_by_offset",
996            bridge_call!(cbf_bridge_client_traverse_history_by_offset(
997                self.inner,
998                browsing_context_id.get(),
999                delta,
1000            )),
1001        )
1002    }
1003
1004    pub fn find_in_page(
1005        &mut self,
1006        browsing_context_id: TabId,
1007        request_id: u64,
1008        options: &ChromeFindInPageOptions,
1009    ) -> Result<(), BridgeError> {
1010        self.ensure_ready()?;
1011
1012        let query = CString::new(options.query.as_str()).map_err(|_| BridgeError::InvalidInput)?;
1013        bridge_ok(
1014            "find_in_page",
1015            bridge_call!(cbf_bridge_client_find_in_page(
1016                self.inner,
1017                browsing_context_id.get(),
1018                request_id,
1019                query.as_ptr(),
1020                options.forward,
1021                options.match_case,
1022                options.new_session,
1023                options.find_match,
1024            )),
1025        )
1026    }
1027
1028    pub fn stop_finding(
1029        &mut self,
1030        browsing_context_id: TabId,
1031        action: ChromeStopFindAction,
1032    ) -> Result<(), BridgeError> {
1033        self.ensure_ready()?;
1034
1035        bridge_ok(
1036            "stop_finding",
1037            bridge_call!(cbf_bridge_client_stop_finding(
1038                self.inner,
1039                browsing_context_id.get(),
1040                action.to_ffi(),
1041            )),
1042        )
1043    }
1044
1045    /// Open Chromium default PromptUi for pending request.
1046    pub fn open_default_prompt_ui(
1047        &mut self,
1048        profile_id: &str,
1049        request_id: u64,
1050    ) -> Result<(), BridgeError> {
1051        self.ensure_ready()?;
1052        let profile_id = CString::new(profile_id).map_err(|_| BridgeError::InvalidInput)?;
1053        bridge_ok(
1054            "open_default_prompt_ui",
1055            bridge_call!(cbf_bridge_client_open_default_prompt_ui(
1056                self.inner,
1057                profile_id.as_ptr(),
1058                request_id,
1059            )),
1060        )
1061    }
1062
1063    /// Respond to a pending chrome-specific PromptUi request.
1064    pub fn respond_prompt_ui(
1065        &mut self,
1066        profile_id: &str,
1067        request_id: u64,
1068        response: &PromptUiResponse,
1069    ) -> Result<(), BridgeError> {
1070        self.ensure_ready()?;
1071        let profile_id = CString::new(profile_id).map_err(|_| BridgeError::InvalidInput)?;
1072        let (prompt_ui_kind, proceed, destination_path, report_abuse) = match response {
1073            PromptUiResponse::PermissionPrompt { allow } => (
1074                CbfPromptUiKind_kCbfPromptUiKindPermissionPrompt,
1075                *allow,
1076                None,
1077                false,
1078            ),
1079            PromptUiResponse::DownloadPrompt {
1080                allow,
1081                destination_path,
1082            } => (
1083                CbfPromptUiKind_kCbfPromptUiKindDownloadPrompt,
1084                *allow,
1085                to_optional_cstring(destination_path)?,
1086                false,
1087            ),
1088            PromptUiResponse::ExtensionInstallPrompt { proceed } => (
1089                CbfPromptUiKind_kCbfPromptUiKindExtensionInstallPrompt,
1090                *proceed,
1091                None,
1092                false,
1093            ),
1094            PromptUiResponse::ExtensionUninstallPrompt {
1095                proceed,
1096                report_abuse,
1097            } => (
1098                CbfPromptUiKind_kCbfPromptUiKindExtensionUninstallPrompt,
1099                *proceed,
1100                None,
1101                *report_abuse,
1102            ),
1103            PromptUiResponse::PrintPreviewDialog { proceed } => (
1104                CbfPromptUiKind_kCbfPromptUiKindPrintPreviewDialog,
1105                *proceed,
1106                None,
1107                false,
1108            ),
1109            PromptUiResponse::FormResubmissionPrompt { proceed } => (
1110                CbfPromptUiKind_kCbfPromptUiKindFormResubmissionPrompt,
1111                *proceed,
1112                None,
1113                false,
1114            ),
1115            PromptUiResponse::Unknown => {
1116                (CbfPromptUiKind_kCbfPromptUiKindUnknown, false, None, false)
1117            }
1118        };
1119        bridge_ok(
1120            "respond_prompt_ui",
1121            bridge_call!(cbf_bridge_client_respond_prompt_ui(
1122                self.inner,
1123                profile_id.as_ptr(),
1124                request_id,
1125                prompt_ui_kind as u8,
1126                proceed,
1127                report_abuse,
1128                destination_path
1129                    .as_ref()
1130                    .map_or(ptr::null(), |path| path.as_ptr()),
1131            )),
1132        )
1133    }
1134
1135    /// Respond to a page-originated prompt by resolving profile on the bridge side.
1136    pub fn respond_prompt_ui_for_tab(
1137        &mut self,
1138        browsing_context_id: TabId,
1139        request_id: u64,
1140        response: &PromptUiResponse,
1141    ) -> Result<(), BridgeError> {
1142        self.ensure_ready()?;
1143        let (prompt_ui_kind, proceed, destination_path, report_abuse) = match response {
1144            PromptUiResponse::PermissionPrompt { allow } => (
1145                CbfPromptUiKind_kCbfPromptUiKindPermissionPrompt,
1146                *allow,
1147                None,
1148                false,
1149            ),
1150            PromptUiResponse::DownloadPrompt {
1151                allow,
1152                destination_path,
1153            } => (
1154                CbfPromptUiKind_kCbfPromptUiKindDownloadPrompt,
1155                *allow,
1156                to_optional_cstring(destination_path)?,
1157                false,
1158            ),
1159            PromptUiResponse::ExtensionInstallPrompt { proceed } => (
1160                CbfPromptUiKind_kCbfPromptUiKindExtensionInstallPrompt,
1161                *proceed,
1162                None,
1163                false,
1164            ),
1165            PromptUiResponse::ExtensionUninstallPrompt {
1166                proceed,
1167                report_abuse,
1168            } => (
1169                CbfPromptUiKind_kCbfPromptUiKindExtensionUninstallPrompt,
1170                *proceed,
1171                None,
1172                *report_abuse,
1173            ),
1174            PromptUiResponse::PrintPreviewDialog { proceed } => (
1175                CbfPromptUiKind_kCbfPromptUiKindPrintPreviewDialog,
1176                *proceed,
1177                None,
1178                false,
1179            ),
1180            PromptUiResponse::FormResubmissionPrompt { proceed } => (
1181                CbfPromptUiKind_kCbfPromptUiKindFormResubmissionPrompt,
1182                *proceed,
1183                None,
1184                false,
1185            ),
1186            PromptUiResponse::Unknown => {
1187                (CbfPromptUiKind_kCbfPromptUiKindUnknown, false, None, false)
1188            }
1189        };
1190        bridge_ok(
1191            "respond_prompt_ui_for_tab",
1192            bridge_call!(cbf_bridge_client_respond_prompt_ui_for_tab(
1193                self.inner,
1194                browsing_context_id.get(),
1195                request_id,
1196                prompt_ui_kind as u8,
1197                proceed,
1198                report_abuse,
1199                destination_path
1200                    .as_ref()
1201                    .map_or(ptr::null(), |path| path.as_ptr()),
1202            )),
1203        )
1204    }
1205
1206    /// Close a backend-managed PromptUi surface.
1207    pub fn close_prompt_ui(
1208        &mut self,
1209        profile_id: &str,
1210        prompt_ui_id: PromptUiId,
1211    ) -> Result<(), BridgeError> {
1212        self.ensure_ready()?;
1213        let profile_id = CString::new(profile_id).map_err(|_| BridgeError::InvalidInput)?;
1214        bridge_ok(
1215            "close_prompt_ui",
1216            bridge_call!(cbf_bridge_client_close_prompt_ui(
1217                self.inner,
1218                profile_id.as_ptr(),
1219                prompt_ui_id.get(),
1220            )),
1221        )
1222    }
1223
1224    /// Pause an in-progress download.
1225    pub fn pause_download(&mut self, download_id: ChromeDownloadId) -> Result<(), BridgeError> {
1226        self.ensure_ready()?;
1227        bridge_ok(
1228            "pause_download",
1229            bridge_call!(cbf_bridge_client_pause_download(
1230                self.inner,
1231                download_id.get()
1232            )),
1233        )
1234    }
1235
1236    /// Resume a paused download.
1237    pub fn resume_download(&mut self, download_id: ChromeDownloadId) -> Result<(), BridgeError> {
1238        self.ensure_ready()?;
1239        bridge_ok(
1240            "resume_download",
1241            bridge_call!(cbf_bridge_client_resume_download(
1242                self.inner,
1243                download_id.get()
1244            )),
1245        )
1246    }
1247
1248    /// Cancel an active download.
1249    pub fn cancel_download(&mut self, download_id: ChromeDownloadId) -> Result<(), BridgeError> {
1250        self.ensure_ready()?;
1251        bridge_ok(
1252            "cancel_download",
1253            bridge_call!(cbf_bridge_client_cancel_download(
1254                self.inner,
1255                download_id.get()
1256            )),
1257        )
1258    }
1259
1260    /// Respond to host-mediated tab-open request.
1261    pub fn respond_tab_open(
1262        &mut self,
1263        request_id: u64,
1264        response: &ChromeBrowsingContextOpenResponse,
1265    ) -> Result<(), BridgeError> {
1266        self.ensure_ready()?;
1267        let (response_kind, target_tab_id, activate) = match response {
1268            ChromeBrowsingContextOpenResponse::AllowNewContext { activate } => (
1269                CbfTabOpenResponseKind_kCbfTabOpenResponseAllowNewContext,
1270                0,
1271                *activate,
1272            ),
1273            ChromeBrowsingContextOpenResponse::AllowExistingContext { tab_id, activate } => (
1274                CbfTabOpenResponseKind_kCbfTabOpenResponseAllowExistingContext,
1275                tab_id.get(),
1276                *activate,
1277            ),
1278            ChromeBrowsingContextOpenResponse::Deny => {
1279                (CbfTabOpenResponseKind_kCbfTabOpenResponseDeny, 0, false)
1280            }
1281        };
1282        bridge_ok(
1283            "respond_tab_open",
1284            bridge_call!(cbf_bridge_client_respond_tab_open(
1285                self.inner,
1286                request_id,
1287                response_kind as u8,
1288                target_tab_id,
1289                activate,
1290            )),
1291        )
1292    }
1293
1294    /// Respond to host-mediated window open request.
1295    ///
1296    /// Current bridge path reuses tab-open response semantics.
1297    pub fn respond_window_open(
1298        &mut self,
1299        request_id: u64,
1300        response: &WindowOpenResponse,
1301    ) -> Result<(), BridgeError> {
1302        let tab_open_response = match response {
1303            WindowOpenResponse::AllowExistingWindow { .. }
1304            | WindowOpenResponse::AllowNewWindow { .. } => {
1305                ChromeBrowsingContextOpenResponse::AllowNewContext { activate: true }
1306            }
1307            WindowOpenResponse::Deny => ChromeBrowsingContextOpenResponse::Deny,
1308        };
1309        self.respond_tab_open(request_id, &tab_open_response)
1310    }
1311
1312    /// Send a Chromium-shaped keyboard event to the page.
1313    pub fn send_key_event_raw(
1314        &mut self,
1315        browsing_context_id: TabId,
1316        event: &ChromeKeyEvent,
1317        commands: &[String],
1318    ) -> Result<(), BridgeError> {
1319        self.ensure_ready()?;
1320
1321        let dom_code = to_optional_cstring(&event.dom_code)?;
1322        let dom_key = to_optional_cstring(&event.dom_key)?;
1323        let text = to_optional_cstring(&event.text)?;
1324        let unmodified_text = to_optional_cstring(&event.unmodified_text)?;
1325
1326        let command_cstrings = commands
1327            .iter()
1328            .map(|command| CString::new(command.as_str()).map_err(|_| BridgeError::InvalidInput))
1329            .collect::<Result<Vec<_>, _>>()?;
1330        let command_ptrs: Vec<*const std::os::raw::c_char> =
1331            command_cstrings.iter().map(|cstr| cstr.as_ptr()).collect();
1332
1333        let ffi_event = CbfKeyEvent {
1334            tab_id: browsing_context_id.get(),
1335            type_: key_event_type_to_ffi(event.type_),
1336            modifiers: event.modifiers,
1337            windows_key_code: event.windows_key_code,
1338            native_key_code: event.native_key_code,
1339            dom_code: dom_code.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1340            dom_key: dom_key.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1341            text: text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1342            unmodified_text: unmodified_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1343            auto_repeat: event.auto_repeat,
1344            is_keypad: event.is_keypad,
1345            is_system_key: event.is_system_key,
1346            location: event.location,
1347        };
1348
1349        let ffi_commands = CbfCommandList {
1350            items: if command_ptrs.is_empty() {
1351                ptr::null_mut()
1352            } else {
1353                command_ptrs.as_ptr() as *mut *const c_char
1354            },
1355            len: command_ptrs.len() as u32,
1356        };
1357
1358        bridge_ok(
1359            "send_key_event",
1360            bridge_call!(cbf_bridge_client_send_key_event(
1361                self.inner,
1362                &ffi_event,
1363                &ffi_commands
1364            )),
1365        )
1366    }
1367
1368    /// Send a Chromium-shaped keyboard event to an extension popup.
1369    pub fn send_extension_popup_key_event_raw(
1370        &mut self,
1371        popup_id: PopupId,
1372        event: &ChromeKeyEvent,
1373        commands: &[String],
1374    ) -> Result<(), BridgeError> {
1375        self.ensure_ready()?;
1376
1377        let dom_code = to_optional_cstring(&event.dom_code)?;
1378        let dom_key = to_optional_cstring(&event.dom_key)?;
1379        let text = to_optional_cstring(&event.text)?;
1380        let unmodified_text = to_optional_cstring(&event.unmodified_text)?;
1381
1382        let command_cstrings = commands
1383            .iter()
1384            .map(|command| CString::new(command.as_str()).map_err(|_| BridgeError::InvalidInput))
1385            .collect::<Result<Vec<_>, _>>()?;
1386        let command_ptrs: Vec<*const std::os::raw::c_char> =
1387            command_cstrings.iter().map(|cstr| cstr.as_ptr()).collect();
1388
1389        let ffi_event = CbfKeyEvent {
1390            tab_id: 0,
1391            type_: key_event_type_to_ffi(event.type_),
1392            modifiers: event.modifiers,
1393            windows_key_code: event.windows_key_code,
1394            native_key_code: event.native_key_code,
1395            dom_code: dom_code.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1396            dom_key: dom_key.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1397            text: text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1398            unmodified_text: unmodified_text.as_ref().map_or(ptr::null(), |v| v.as_ptr()),
1399            auto_repeat: event.auto_repeat,
1400            is_keypad: event.is_keypad,
1401            is_system_key: event.is_system_key,
1402            location: event.location,
1403        };
1404
1405        let ffi_commands = CbfCommandList {
1406            items: if command_ptrs.is_empty() {
1407                ptr::null_mut()
1408            } else {
1409                command_ptrs.as_ptr() as *mut *const c_char
1410            },
1411            len: command_ptrs.len() as u32,
1412        };
1413
1414        bridge_ok(
1415            "send_extension_popup_key_event_raw",
1416            bridge_call!(cbf_bridge_client_send_extension_popup_key_event(
1417                self.inner,
1418                popup_id.get(),
1419                &ffi_event,
1420                &ffi_commands,
1421            )),
1422        )
1423    }
1424
1425    /// Send a mouse event to the page.
1426    pub fn send_mouse_event(
1427        &mut self,
1428        browsing_context_id: TabId,
1429        event: &ChromeMouseEvent,
1430    ) -> Result<(), BridgeError> {
1431        self.ensure_ready()?;
1432
1433        let ffi_event = CbfMouseEvent {
1434            tab_id: browsing_context_id.get(),
1435            type_: mouse_event_type_to_ffi(event.type_),
1436            modifiers: event.modifiers,
1437            button: mouse_button_to_ffi(event.button),
1438            click_count: event.click_count,
1439            position_in_widget_x: event.position_in_widget_x,
1440            position_in_widget_y: event.position_in_widget_y,
1441            position_in_screen_x: event.position_in_screen_x,
1442            position_in_screen_y: event.position_in_screen_y,
1443            movement_x: event.movement_x,
1444            movement_y: event.movement_y,
1445            is_raw_movement_event: event.is_raw_movement_event,
1446            pointer_type: pointer_type_to_ffi(event.pointer_type),
1447        };
1448
1449        bridge_ok(
1450            "send_mouse_event",
1451            bridge_call!(cbf_bridge_client_send_mouse_event(self.inner, &ffi_event)),
1452        )
1453    }
1454
1455    /// Send a mouse event to an extension popup.
1456    pub fn send_extension_popup_mouse_event(
1457        &mut self,
1458        popup_id: PopupId,
1459        event: &ChromeMouseEvent,
1460    ) -> Result<(), BridgeError> {
1461        self.ensure_ready()?;
1462
1463        let ffi_event = CbfMouseEvent {
1464            tab_id: 0,
1465            type_: mouse_event_type_to_ffi(event.type_),
1466            modifiers: event.modifiers,
1467            button: mouse_button_to_ffi(event.button),
1468            click_count: event.click_count,
1469            position_in_widget_x: event.position_in_widget_x,
1470            position_in_widget_y: event.position_in_widget_y,
1471            position_in_screen_x: event.position_in_screen_x,
1472            position_in_screen_y: event.position_in_screen_y,
1473            movement_x: event.movement_x,
1474            movement_y: event.movement_y,
1475            is_raw_movement_event: event.is_raw_movement_event,
1476            pointer_type: pointer_type_to_ffi(event.pointer_type),
1477        };
1478
1479        bridge_ok(
1480            "send_extension_popup_mouse_event",
1481            bridge_call!(cbf_bridge_client_send_extension_popup_mouse_event(
1482                self.inner,
1483                popup_id.get(),
1484                &ffi_event,
1485            )),
1486        )
1487    }
1488
1489    /// Send a Chromium-shaped mouse wheel event to the page.
1490    pub fn send_mouse_wheel_event_raw(
1491        &mut self,
1492        browsing_context_id: TabId,
1493        event: &ChromeMouseWheelEvent,
1494    ) -> Result<(), BridgeError> {
1495        self.ensure_ready()?;
1496
1497        let ffi_event = CbfMouseWheelEvent {
1498            tab_id: browsing_context_id.get(),
1499            modifiers: event.modifiers,
1500            position_in_widget_x: event.position_in_widget_x,
1501            position_in_widget_y: event.position_in_widget_y,
1502            position_in_screen_x: event.position_in_screen_x,
1503            position_in_screen_y: event.position_in_screen_y,
1504            movement_x: event.movement_x,
1505            movement_y: event.movement_y,
1506            is_raw_movement_event: event.is_raw_movement_event,
1507            delta_x: event.delta_x,
1508            delta_y: event.delta_y,
1509            wheel_ticks_x: event.wheel_ticks_x,
1510            wheel_ticks_y: event.wheel_ticks_y,
1511            phase: event.phase,
1512            momentum_phase: event.momentum_phase,
1513            delta_units: scroll_granularity_to_ffi(event.delta_units),
1514        };
1515
1516        bridge_ok(
1517            "send_mouse_wheel_event",
1518            bridge_call!(cbf_bridge_client_send_mouse_wheel_event(
1519                self.inner, &ffi_event
1520            )),
1521        )
1522    }
1523
1524    /// Send a Chromium-shaped mouse wheel event to an extension popup.
1525    pub fn send_extension_popup_mouse_wheel_event_raw(
1526        &mut self,
1527        popup_id: PopupId,
1528        event: &ChromeMouseWheelEvent,
1529    ) -> Result<(), BridgeError> {
1530        self.ensure_ready()?;
1531
1532        let ffi_event = CbfMouseWheelEvent {
1533            tab_id: 0,
1534            modifiers: event.modifiers,
1535            position_in_widget_x: event.position_in_widget_x,
1536            position_in_widget_y: event.position_in_widget_y,
1537            position_in_screen_x: event.position_in_screen_x,
1538            position_in_screen_y: event.position_in_screen_y,
1539            movement_x: event.movement_x,
1540            movement_y: event.movement_y,
1541            is_raw_movement_event: event.is_raw_movement_event,
1542            delta_x: event.delta_x,
1543            delta_y: event.delta_y,
1544            wheel_ticks_x: event.wheel_ticks_x,
1545            wheel_ticks_y: event.wheel_ticks_y,
1546            phase: event.phase,
1547            momentum_phase: event.momentum_phase,
1548            delta_units: scroll_granularity_to_ffi(event.delta_units),
1549        };
1550
1551        bridge_ok(
1552            "send_extension_popup_mouse_wheel_event_raw",
1553            bridge_call!(cbf_bridge_client_send_extension_popup_mouse_wheel_event(
1554                self.inner,
1555                popup_id.get(),
1556                &ffi_event,
1557            )),
1558        )
1559    }
1560
1561    /// Send a drag update event for host-owned drag session.
1562    pub fn send_drag_update(&mut self, update: &ChromeDragUpdate) -> Result<(), BridgeError> {
1563        self.ensure_ready()?;
1564
1565        let ffi_update = CbfDragUpdate {
1566            session_id: update.session_id,
1567            tab_id: update.browsing_context_id.get(),
1568            allowed_operations: update.allowed_operations.bits(),
1569            modifiers: update.modifiers,
1570            position_in_widget_x: update.position_in_widget_x,
1571            position_in_widget_y: update.position_in_widget_y,
1572            position_in_screen_x: update.position_in_screen_x,
1573            position_in_screen_y: update.position_in_screen_y,
1574        };
1575
1576        bridge_ok(
1577            "send_drag_update",
1578            bridge_call!(cbf_bridge_client_send_drag_update(self.inner, &ffi_update)),
1579        )
1580    }
1581
1582    /// Send a drag drop event for host-owned drag session.
1583    pub fn send_drag_drop(&mut self, drop: &ChromeDragDrop) -> Result<(), BridgeError> {
1584        self.ensure_ready()?;
1585
1586        let ffi_drop = CbfDragDrop {
1587            session_id: drop.session_id,
1588            tab_id: drop.browsing_context_id.get(),
1589            modifiers: drop.modifiers,
1590            position_in_widget_x: drop.position_in_widget_x,
1591            position_in_widget_y: drop.position_in_widget_y,
1592            position_in_screen_x: drop.position_in_screen_x,
1593            position_in_screen_y: drop.position_in_screen_y,
1594        };
1595
1596        bridge_ok(
1597            "send_drag_drop",
1598            bridge_call!(cbf_bridge_client_send_drag_drop(self.inner, &ffi_drop)),
1599        )
1600    }
1601
1602    /// Cancel a host-owned drag session.
1603    pub fn send_drag_cancel(
1604        &mut self,
1605        session_id: u64,
1606        browsing_context_id: TabId,
1607    ) -> Result<(), BridgeError> {
1608        self.ensure_ready()?;
1609
1610        bridge_ok(
1611            "send_drag_cancel",
1612            bridge_call!(cbf_bridge_client_send_drag_cancel(
1613                self.inner,
1614                session_id,
1615                browsing_context_id.get(),
1616            )),
1617        )
1618    }
1619
1620    /// Send an external drag enter event for a native drag destination.
1621    pub fn send_external_drag_enter(
1622        &mut self,
1623        event: &ChromeExternalDragEnter,
1624    ) -> Result<(), BridgeError> {
1625        self.ensure_ready()?;
1626
1627        let mut owned_data = OwnedDragData::new(&event.data)?;
1628        let ffi_event = CbfExternalDragEnter {
1629            tab_id: event.browsing_context_id.get(),
1630            data: owned_data.as_ffi(),
1631            allowed_operations: event.allowed_operations.bits(),
1632            modifiers: event.modifiers,
1633            position_in_widget_x: event.position_in_widget_x,
1634            position_in_widget_y: event.position_in_widget_y,
1635            position_in_screen_x: event.position_in_screen_x,
1636            position_in_screen_y: event.position_in_screen_y,
1637        };
1638
1639        bridge_ok(
1640            "send_external_drag_enter",
1641            bridge_call!(cbf_bridge_client_send_external_drag_enter(
1642                self.inner, &ffi_event
1643            )),
1644        )
1645    }
1646
1647    /// Send an external drag update event for a native drag destination.
1648    pub fn send_external_drag_update(
1649        &mut self,
1650        event: &ChromeExternalDragUpdate,
1651    ) -> Result<(), BridgeError> {
1652        self.ensure_ready()?;
1653
1654        let ffi_event = CbfExternalDragUpdate {
1655            tab_id: event.browsing_context_id.get(),
1656            allowed_operations: event.allowed_operations.bits(),
1657            modifiers: event.modifiers,
1658            position_in_widget_x: event.position_in_widget_x,
1659            position_in_widget_y: event.position_in_widget_y,
1660            position_in_screen_x: event.position_in_screen_x,
1661            position_in_screen_y: event.position_in_screen_y,
1662        };
1663
1664        bridge_ok(
1665            "send_external_drag_update",
1666            bridge_call!(cbf_bridge_client_send_external_drag_update(
1667                self.inner, &ffi_event
1668            )),
1669        )
1670    }
1671
1672    /// Notify the backend that the active external drag left the page.
1673    pub fn send_external_drag_leave(
1674        &mut self,
1675        browsing_context_id: TabId,
1676    ) -> Result<(), BridgeError> {
1677        self.ensure_ready()?;
1678
1679        bridge_ok(
1680            "send_external_drag_leave",
1681            bridge_call!(cbf_bridge_client_send_external_drag_leave(
1682                self.inner,
1683                browsing_context_id.get()
1684            )),
1685        )
1686    }
1687
1688    /// Send an external drag drop event for a native drag destination.
1689    pub fn send_external_drag_drop(
1690        &mut self,
1691        event: &ChromeExternalDragDrop,
1692    ) -> Result<(), BridgeError> {
1693        self.ensure_ready()?;
1694
1695        let ffi_event = CbfExternalDragDrop {
1696            tab_id: event.browsing_context_id.get(),
1697            modifiers: event.modifiers,
1698            position_in_widget_x: event.position_in_widget_x,
1699            position_in_widget_y: event.position_in_widget_y,
1700            position_in_screen_x: event.position_in_screen_x,
1701            position_in_screen_y: event.position_in_screen_y,
1702        };
1703
1704        bridge_ok(
1705            "send_external_drag_drop",
1706            bridge_call!(cbf_bridge_client_send_external_drag_drop(
1707                self.inner, &ffi_event
1708            )),
1709        )
1710    }
1711
1712    /// Update the IME composition state.
1713    pub fn set_composition(
1714        &mut self,
1715        composition: &ChromeImeComposition,
1716    ) -> Result<(), BridgeError> {
1717        self.ensure_ready()?;
1718
1719        let text =
1720            CString::new(composition.text.as_str()).map_err(|_| BridgeError::InvalidInput)?;
1721        let spans = to_ffi_ime_text_spans(&composition.spans);
1722        let span_list = CbfImeTextSpanList {
1723            items: if spans.is_empty() {
1724                ptr::null()
1725            } else {
1726                spans.as_ptr()
1727            },
1728            len: spans.len() as u32,
1729        };
1730        let (replacement_start, replacement_end) = ime_range_to_ffi(&composition.replacement_range);
1731
1732        let ffi_composition = CbfImeComposition {
1733            tab_id: composition.browsing_context_id.get(),
1734            text: text.as_ptr(),
1735            selection_start: composition.selection_start,
1736            selection_end: composition.selection_end,
1737            replacement_range_start: replacement_start,
1738            replacement_range_end: replacement_end,
1739            spans: span_list,
1740        };
1741
1742        bridge_ok(
1743            "set_composition",
1744            bridge_call!(cbf_bridge_client_set_composition(
1745                self.inner,
1746                &ffi_composition
1747            )),
1748        )
1749    }
1750
1751    /// Update the IME composition state for an extension popup.
1752    pub fn set_extension_popup_composition(
1753        &mut self,
1754        composition: &ChromeTransientImeComposition,
1755    ) -> Result<(), BridgeError> {
1756        self.ensure_ready()?;
1757
1758        let text =
1759            CString::new(composition.text.as_str()).map_err(|_| BridgeError::InvalidInput)?;
1760        let spans = to_ffi_ime_text_spans(&composition.spans);
1761        let span_list = CbfImeTextSpanList {
1762            items: if spans.is_empty() {
1763                ptr::null()
1764            } else {
1765                spans.as_ptr()
1766            },
1767            len: spans.len() as u32,
1768        };
1769        let (replacement_start, replacement_end) = ime_range_to_ffi(&composition.replacement_range);
1770
1771        let ffi_composition = CbfImeComposition {
1772            tab_id: 0,
1773            text: text.as_ptr(),
1774            selection_start: composition.selection_start,
1775            selection_end: composition.selection_end,
1776            replacement_range_start: replacement_start,
1777            replacement_range_end: replacement_end,
1778            spans: span_list,
1779        };
1780
1781        bridge_ok(
1782            "set_extension_popup_composition",
1783            bridge_call!(cbf_bridge_client_set_extension_popup_composition(
1784                self.inner,
1785                composition.popup_id.get(),
1786                &ffi_composition,
1787            )),
1788        )
1789    }
1790
1791    /// Commit IME text input to the page.
1792    pub fn commit_text(&mut self, commit: &ChromeImeCommitText) -> Result<(), BridgeError> {
1793        self.ensure_ready()?;
1794
1795        let text = CString::new(commit.text.as_str()).map_err(|_| BridgeError::InvalidInput)?;
1796        let spans = to_ffi_ime_text_spans(&commit.spans);
1797        let span_list = CbfImeTextSpanList {
1798            items: if spans.is_empty() {
1799                ptr::null()
1800            } else {
1801                spans.as_ptr()
1802            },
1803            len: spans.len() as u32,
1804        };
1805        let (replacement_start, replacement_end) = ime_range_to_ffi(&commit.replacement_range);
1806
1807        let ffi_commit = CbfImeCommitText {
1808            tab_id: commit.browsing_context_id.get(),
1809            text: text.as_ptr(),
1810            relative_caret_position: commit.relative_caret_position,
1811            replacement_range_start: replacement_start,
1812            replacement_range_end: replacement_end,
1813            spans: span_list,
1814        };
1815
1816        bridge_ok(
1817            "commit_text",
1818            bridge_call!(cbf_bridge_client_commit_text(self.inner, &ffi_commit)),
1819        )
1820    }
1821
1822    /// Commit IME text input to an extension popup.
1823    pub fn commit_extension_popup_text(
1824        &mut self,
1825        commit: &ChromeTransientImeCommitText,
1826    ) -> Result<(), BridgeError> {
1827        self.ensure_ready()?;
1828
1829        let text = CString::new(commit.text.as_str()).map_err(|_| BridgeError::InvalidInput)?;
1830        let spans = to_ffi_ime_text_spans(&commit.spans);
1831        let span_list = CbfImeTextSpanList {
1832            items: if spans.is_empty() {
1833                ptr::null()
1834            } else {
1835                spans.as_ptr()
1836            },
1837            len: spans.len() as u32,
1838        };
1839        let (replacement_start, replacement_end) = ime_range_to_ffi(&commit.replacement_range);
1840
1841        let ffi_commit = CbfImeCommitText {
1842            tab_id: 0,
1843            text: text.as_ptr(),
1844            relative_caret_position: commit.relative_caret_position,
1845            replacement_range_start: replacement_start,
1846            replacement_range_end: replacement_end,
1847            spans: span_list,
1848        };
1849
1850        bridge_ok(
1851            "commit_extension_popup_text",
1852            bridge_call!(cbf_bridge_client_commit_extension_popup_text(
1853                self.inner,
1854                commit.popup_id.get(),
1855                &ffi_commit,
1856            )),
1857        )
1858    }
1859
1860    /// Finish composing IME text with the specified behavior.
1861    pub fn finish_composing_text(
1862        &mut self,
1863        browsing_context_id: TabId,
1864        behavior: ChromeConfirmCompositionBehavior,
1865    ) -> Result<(), BridgeError> {
1866        self.ensure_ready()?;
1867
1868        let behavior = match behavior {
1869            ChromeConfirmCompositionBehavior::DoNotKeepSelection => {
1870                CbfConfirmCompositionBehavior_kCbfConfirmCompositionDoNotKeepSelection
1871            }
1872            ChromeConfirmCompositionBehavior::KeepSelection => {
1873                CbfConfirmCompositionBehavior_kCbfConfirmCompositionKeepSelection
1874            }
1875        } as u8;
1876
1877        bridge_ok(
1878            "finish_composing_text",
1879            bridge_call!(cbf_bridge_client_finish_composing_text(
1880                self.inner,
1881                browsing_context_id.get(),
1882                behavior,
1883            )),
1884        )
1885    }
1886
1887    /// Finish composing IME text inside an extension popup.
1888    pub fn finish_extension_popup_composing_text(
1889        &mut self,
1890        popup_id: PopupId,
1891        behavior: ChromeConfirmCompositionBehavior,
1892    ) -> Result<(), BridgeError> {
1893        self.ensure_ready()?;
1894
1895        let behavior = match behavior {
1896            ChromeConfirmCompositionBehavior::DoNotKeepSelection => {
1897                CbfConfirmCompositionBehavior_kCbfConfirmCompositionDoNotKeepSelection
1898            }
1899            ChromeConfirmCompositionBehavior::KeepSelection => {
1900                CbfConfirmCompositionBehavior_kCbfConfirmCompositionKeepSelection
1901            }
1902        } as u8;
1903
1904        bridge_ok(
1905            "finish_extension_popup_composing_text",
1906            bridge_call!(cbf_bridge_client_finish_extension_popup_composing_text(
1907                self.inner,
1908                popup_id.get(),
1909                behavior,
1910            )),
1911        )
1912    }
1913
1914    pub fn set_extension_popup_focus(
1915        &mut self,
1916        popup_id: PopupId,
1917        focused: bool,
1918    ) -> Result<(), BridgeError> {
1919        self.ensure_ready()?;
1920
1921        bridge_ok(
1922            "set_extension_popup_focus",
1923            bridge_call!(cbf_bridge_client_set_extension_popup_focus(
1924                self.inner,
1925                popup_id.get(),
1926                focused
1927            )),
1928        )
1929    }
1930
1931    pub fn set_extension_popup_size(
1932        &mut self,
1933        popup_id: PopupId,
1934        width: u32,
1935        height: u32,
1936    ) -> Result<(), BridgeError> {
1937        self.ensure_ready()?;
1938
1939        bridge_ok(
1940            "set_extension_popup_size",
1941            bridge_call!(cbf_bridge_client_set_extension_popup_size(
1942                self.inner,
1943                popup_id.get(),
1944                width,
1945                height,
1946            )),
1947        )
1948    }
1949
1950    /// Update the page background policy of the specified extension popup.
1951    pub fn set_extension_popup_background_policy(
1952        &mut self,
1953        popup_id: PopupId,
1954        policy: ChromeBackgroundPolicy,
1955    ) -> Result<(), BridgeError> {
1956        self.ensure_ready()?;
1957
1958        let transparent = matches!(policy, ChromeBackgroundPolicy::Transparent);
1959
1960        bridge_ok(
1961            "set_extension_popup_background_policy",
1962            bridge_call!(cbf_bridge_client_set_extension_popup_background_policy(
1963                self.inner,
1964                popup_id.get(),
1965                transparent,
1966            )),
1967        )
1968    }
1969
1970    pub fn close_extension_popup(&mut self, popup_id: PopupId) -> Result<(), BridgeError> {
1971        self.ensure_ready()?;
1972
1973        bridge_ok(
1974            "close_extension_popup",
1975            bridge_call!(cbf_bridge_client_close_extension_popup(
1976                self.inner,
1977                popup_id.get()
1978            )),
1979        )
1980    }
1981
1982    /// Execute a browser-generic edit action for the given page.
1983    pub fn execute_edit_action(
1984        &mut self,
1985        browsing_context_id: TabId,
1986        action: EditAction,
1987    ) -> Result<(), BridgeError> {
1988        self.ensure_ready()?;
1989
1990        bridge_ok(
1991            "execute_edit_action",
1992            bridge_call!(cbf_bridge_client_execute_edit_action(
1993                self.inner,
1994                browsing_context_id.get(),
1995                edit_action_to_ffi(action),
1996            )),
1997        )
1998    }
1999
2000    /// Execute a browser-generic edit action for the given extension popup.
2001    pub fn execute_extension_popup_edit_action(
2002        &mut self,
2003        popup_id: PopupId,
2004        action: EditAction,
2005    ) -> Result<(), BridgeError> {
2006        self.ensure_ready()?;
2007
2008        bridge_ok(
2009            "execute_extension_popup_edit_action",
2010            bridge_call!(cbf_bridge_client_execute_extension_popup_edit_action(
2011                self.inner,
2012                popup_id.get(),
2013                edit_action_to_ffi(action),
2014            )),
2015        )
2016    }
2017
2018    /// Execute a context menu command for the given menu.
2019    pub fn execute_context_menu_command(
2020        &mut self,
2021        menu_id: u64,
2022        command_id: i32,
2023        event_flags: i32,
2024    ) -> Result<(), BridgeError> {
2025        self.ensure_ready()?;
2026
2027        bridge_ok(
2028            "execute_context_menu_command",
2029            bridge_call!(cbf_bridge_client_execute_context_menu_command(
2030                self.inner,
2031                menu_id,
2032                command_id,
2033                event_flags,
2034            )),
2035        )
2036    }
2037
2038    /// Accept a host-owned choice menu selection.
2039    pub fn accept_choice_menu_selection(
2040        &mut self,
2041        request_id: u64,
2042        indices: &[i32],
2043    ) -> Result<(), BridgeError> {
2044        self.ensure_ready()?;
2045
2046        let ffi_indices = CbfChoiceMenuSelectedIndices {
2047            items: indices.as_ptr(),
2048            len: indices.len() as u32,
2049        };
2050        bridge_ok(
2051            "accept_choice_menu_selection",
2052            bridge_call!(cbf_bridge_client_accept_choice_menu_selection(
2053                self.inner,
2054                request_id,
2055                &ffi_indices
2056            )),
2057        )
2058    }
2059
2060    /// Dismiss a host-owned choice menu without a selection.
2061    pub fn dismiss_choice_menu(&mut self, request_id: u64) -> Result<(), BridgeError> {
2062        self.ensure_ready()?;
2063
2064        bridge_ok(
2065            "dismiss_choice_menu",
2066            bridge_call!(cbf_bridge_client_dismiss_choice_menu(
2067                self.inner, request_id
2068            )),
2069        )
2070    }
2071
2072    /// Dismiss the context menu with the given id.
2073    pub fn dismiss_context_menu(&mut self, menu_id: u64) -> Result<(), BridgeError> {
2074        self.ensure_ready()?;
2075
2076        bridge_ok(
2077            "dismiss_context_menu",
2078            bridge_call!(cbf_bridge_client_dismiss_context_menu(self.inner, menu_id)),
2079        )
2080    }
2081
2082    /// Request a graceful shutdown from the backend.
2083    pub fn request_shutdown(&mut self, request_id: u64) -> Result<(), BridgeError> {
2084        self.ensure_ready()?;
2085
2086        bridge_ok(
2087            "request_shutdown",
2088            bridge_call!(cbf_bridge_client_request_shutdown(self.inner, request_id)),
2089        )
2090    }
2091
2092    /// Respond to a shutdown confirmation request.
2093    pub fn confirm_shutdown(&mut self, request_id: u64, proceed: bool) -> Result<(), BridgeError> {
2094        self.ensure_ready()?;
2095
2096        bridge_ok(
2097            "confirm_shutdown",
2098            bridge_call!(cbf_bridge_client_confirm_shutdown(
2099                self.inner, request_id, proceed
2100            )),
2101        )
2102    }
2103
2104    /// Force an immediate shutdown without confirmations.
2105    pub fn force_shutdown(&mut self) -> Result<(), BridgeError> {
2106        self.ensure_ready()?;
2107
2108        bridge_ok(
2109            "force_shutdown",
2110            bridge_call!(cbf_bridge_client_force_shutdown(self.inner)),
2111        )
2112    }
2113
2114    /// Tear down the IPC client and free native resources.
2115    pub fn shutdown(&mut self) {
2116        if !self.inner.is_null() {
2117            cleanup_bridge_call("shutdown bridge client", |bridge| {
2118                unsafe { bridge.cbf_bridge_client_shutdown(self.inner) };
2119            });
2120        }
2121    }
2122}
2123
2124impl IpcEventWaitHandle {
2125    pub(crate) fn wait_for_event(
2126        &self,
2127        timeout: Option<Duration>,
2128    ) -> Result<EventWaitResult, BridgeError> {
2129        wait_for_event_inner(self.inner, timeout)
2130    }
2131}
2132
2133fn wait_for_event_inner(
2134    inner: *mut CbfBridgeClientHandle,
2135    timeout: Option<Duration>,
2136) -> Result<EventWaitResult, BridgeError> {
2137    if inner.is_null() {
2138        return Ok(EventWaitResult::Closed);
2139    }
2140
2141    let timeout_ms = timeout
2142        .map(|value| value.as_millis().min(i64::MAX as u128) as i64)
2143        .unwrap_or(-1);
2144    let status = bridge_call!(cbf_bridge_client_wait_for_event(inner, timeout_ms));
2145
2146    match status {
2147        CbfBridgeEventWaitStatus_kCbfBridgeEventWaitStatusEventAvailable => {
2148            Ok(EventWaitResult::EventAvailable)
2149        }
2150        CbfBridgeEventWaitStatus_kCbfBridgeEventWaitStatusTimedOut => Ok(EventWaitResult::TimedOut),
2151        CbfBridgeEventWaitStatus_kCbfBridgeEventWaitStatusDisconnected => {
2152            Ok(EventWaitResult::Disconnected)
2153        }
2154        CbfBridgeEventWaitStatus_kCbfBridgeEventWaitStatusClosed => Ok(EventWaitResult::Closed),
2155        _ => Err(BridgeError::InvalidEvent),
2156    }
2157}
2158
2159fn edit_action_to_ffi(action: EditAction) -> u8 {
2160    (match action {
2161        EditAction::Undo => CbfEditAction_kCbfEditActionUndo,
2162        EditAction::Redo => CbfEditAction_kCbfEditActionRedo,
2163        EditAction::Cut => CbfEditAction_kCbfEditActionCut,
2164        EditAction::Copy => CbfEditAction_kCbfEditActionCopy,
2165        EditAction::Paste => CbfEditAction_kCbfEditActionPaste,
2166        EditAction::SelectAll => CbfEditAction_kCbfEditActionSelectAll,
2167    }) as u8
2168}
2169
2170fn as_ffi_string_ptr(value: &CString) -> *mut c_char {
2171    value.as_ptr().cast_mut()
2172}
2173
2174struct OwnedStringList {
2175    strings: Vec<CString>,
2176    ptrs: Vec<*mut c_char>,
2177}
2178
2179impl OwnedStringList {
2180    fn new(values: &[String]) -> Result<Self, BridgeError> {
2181        let strings = values
2182            .iter()
2183            .map(|value| CString::new(value.as_str()).map_err(|_| BridgeError::InvalidInput))
2184            .collect::<Result<Vec<_>, _>>()?;
2185        let ptrs = strings.iter().map(as_ffi_string_ptr).collect::<Vec<_>>();
2186        Ok(Self { strings, ptrs })
2187    }
2188
2189    fn as_ffi(&mut self) -> CbfStringList {
2190        let _ = &self.strings;
2191        CbfStringList {
2192            items: if self.ptrs.is_empty() {
2193                ptr::null_mut()
2194            } else {
2195                self.ptrs.as_mut_ptr()
2196            },
2197            len: self.ptrs.len() as u32,
2198        }
2199    }
2200}
2201
2202struct OwnedDragUrlList {
2203    strings: Vec<(CString, CString)>,
2204    items: Vec<CbfDragUrlInfo>,
2205}
2206
2207impl OwnedDragUrlList {
2208    fn new(values: &[crate::data::drag::ChromeDragUrlInfo]) -> Result<Self, BridgeError> {
2209        let strings = values
2210            .iter()
2211            .map(|value| {
2212                Ok((
2213                    CString::new(value.url.as_str()).map_err(|_| BridgeError::InvalidInput)?,
2214                    CString::new(value.title.as_str()).map_err(|_| BridgeError::InvalidInput)?,
2215                ))
2216            })
2217            .collect::<Result<Vec<_>, BridgeError>>()?;
2218        let items = strings
2219            .iter()
2220            .map(|(url, title)| CbfDragUrlInfo {
2221                url: as_ffi_string_ptr(url),
2222                title: as_ffi_string_ptr(title),
2223            })
2224            .collect();
2225        Ok(Self { strings, items })
2226    }
2227
2228    fn as_ffi(&self) -> CbfDragUrlInfoList {
2229        let _ = &self.strings;
2230        CbfDragUrlInfoList {
2231            items: if self.items.is_empty() {
2232                ptr::null()
2233            } else {
2234                self.items.as_ptr()
2235            },
2236            len: self.items.len() as u32,
2237        }
2238    }
2239}
2240
2241struct OwnedStringPairList {
2242    strings: Vec<(CString, CString)>,
2243    items: Vec<CbfStringPair>,
2244}
2245
2246impl OwnedStringPairList {
2247    fn new(values: &std::collections::BTreeMap<String, String>) -> Result<Self, BridgeError> {
2248        let strings = values
2249            .iter()
2250            .map(|(key, value)| {
2251                Ok((
2252                    CString::new(key.as_str()).map_err(|_| BridgeError::InvalidInput)?,
2253                    CString::new(value.as_str()).map_err(|_| BridgeError::InvalidInput)?,
2254                ))
2255            })
2256            .collect::<Result<Vec<_>, BridgeError>>()?;
2257        let items = strings
2258            .iter()
2259            .map(|(key, value)| CbfStringPair {
2260                key: as_ffi_string_ptr(key),
2261                value: as_ffi_string_ptr(value),
2262            })
2263            .collect();
2264        Ok(Self { strings, items })
2265    }
2266
2267    fn as_ffi(&mut self) -> CbfStringPairList {
2268        let _ = &self.strings;
2269        CbfStringPairList {
2270            items: if self.items.is_empty() {
2271                ptr::null_mut()
2272            } else {
2273                self.items.as_mut_ptr()
2274            },
2275            len: self.items.len() as u32,
2276        }
2277    }
2278}
2279
2280struct OwnedDragData {
2281    text: CString,
2282    html: CString,
2283    html_base_url: CString,
2284    url_infos: OwnedDragUrlList,
2285    filenames: OwnedStringList,
2286    file_mime_types: OwnedStringList,
2287    custom_data: OwnedStringPairList,
2288}
2289
2290impl OwnedDragData {
2291    fn new(data: &ChromeDragData) -> Result<Self, BridgeError> {
2292        Ok(Self {
2293            text: CString::new(data.text.as_str()).map_err(|_| BridgeError::InvalidInput)?,
2294            html: CString::new(data.html.as_str()).map_err(|_| BridgeError::InvalidInput)?,
2295            html_base_url: CString::new(data.html_base_url.as_str())
2296                .map_err(|_| BridgeError::InvalidInput)?,
2297            url_infos: OwnedDragUrlList::new(&data.url_infos)?,
2298            filenames: OwnedStringList::new(&data.filenames)?,
2299            file_mime_types: OwnedStringList::new(&data.file_mime_types)?,
2300            custom_data: OwnedStringPairList::new(&data.custom_data)?,
2301        })
2302    }
2303
2304    fn as_ffi(&mut self) -> CbfDragData {
2305        CbfDragData {
2306            text: as_ffi_string_ptr(&self.text),
2307            html: as_ffi_string_ptr(&self.html),
2308            html_base_url: as_ffi_string_ptr(&self.html_base_url),
2309            url_infos: self.url_infos.as_ffi(),
2310            filenames: self.filenames.as_ffi(),
2311            file_mime_types: self.file_mime_types.as_ffi(),
2312            custom_data: self.custom_data.as_ffi(),
2313        }
2314    }
2315}
2316
2317fn ipc_message_type_to_ffi(message_type: TabIpcMessageType) -> u8 {
2318    (match message_type {
2319        TabIpcMessageType::Request => CbfIpcMessageType_kCbfIpcMessageRequest,
2320        TabIpcMessageType::Response => CbfIpcMessageType_kCbfIpcMessageResponse,
2321        TabIpcMessageType::Event => CbfIpcMessageType_kCbfIpcMessageEvent,
2322    }) as u8
2323}
2324
2325fn ipc_error_code_to_ffi(error_code: TabIpcErrorCode) -> u8 {
2326    (match error_code {
2327        TabIpcErrorCode::Timeout => CbfIpcErrorCode_kCbfIpcErrorTimeout,
2328        TabIpcErrorCode::Aborted => CbfIpcErrorCode_kCbfIpcErrorAborted,
2329        TabIpcErrorCode::Disconnected => CbfIpcErrorCode_kCbfIpcErrorDisconnected,
2330        TabIpcErrorCode::IpcDisabled => CbfIpcErrorCode_kCbfIpcErrorIpcDisabled,
2331        TabIpcErrorCode::ContextClosed => CbfIpcErrorCode_kCbfIpcErrorContextClosed,
2332        TabIpcErrorCode::RemoteError => CbfIpcErrorCode_kCbfIpcErrorRemoteError,
2333        TabIpcErrorCode::ProtocolError => CbfIpcErrorCode_kCbfIpcErrorProtocolError,
2334    }) as u8
2335}
2336
2337impl Drop for IpcClient {
2338    fn drop(&mut self) {
2339        if !self.inner.is_null() {
2340            cleanup_bridge_call("destroy bridge client on drop", |bridge| {
2341                unsafe { bridge.cbf_bridge_client_destroy(self.inner) };
2342            });
2343            self.inner = ptr::null_mut();
2344        }
2345    }
2346}
2347
2348impl IpcClient {
2349    fn ensure_ready(&self) -> Result<(), BridgeError> {
2350        if self.inner.is_null() {
2351            Err(BridgeError::InvalidState)
2352        } else {
2353            Ok(())
2354        }
2355    }
2356}
2357
2358fn bridge_api() -> Result<&'static BridgeLibrary, BridgeError> {
2359    bridge().map_err(map_bridge_load_error)
2360}
2361
2362fn map_bridge_load_error(_: BridgeLoadError) -> BridgeError {
2363    BridgeError::BridgeLoadFailed
2364}
2365
2366fn parse_channel_switch_arg(buf: &[u8]) -> Result<String, BridgeError> {
2367    let switch_arg = CStr::from_bytes_until_nul(buf)
2368        .map_err(|_| BridgeError::InvalidChannelArgument)?
2369        .to_str()
2370        .map_err(|_| BridgeError::InvalidChannelArgument)?
2371        .to_owned();
2372    if switch_arg.is_empty() {
2373        return Err(BridgeError::InvalidChannelArgument);
2374    }
2375
2376    Ok(switch_arg)
2377}
2378
2379fn authentication_result(success: bool) -> Result<(), BridgeError> {
2380    if success {
2381        Ok(())
2382    } else {
2383        Err(BridgeError::AuthenticationFailed)
2384    }
2385}
2386
2387fn bridge_ok(operation: &'static str, success: bool) -> Result<(), BridgeError> {
2388    if success {
2389        Ok(())
2390    } else {
2391        Err(BridgeError::OperationFailed { operation })
2392    }
2393}
2394
2395fn cleanup_bridge_call<F>(operation: &'static str, callback: F)
2396where
2397    F: FnOnce(&BridgeLibrary),
2398{
2399    if let Err(error) = bridge().map(callback) {
2400        warn!(operation, error = ?error, "bridge cleanup call failed");
2401    }
2402}
2403
2404#[cfg(test)]
2405mod tests {
2406    use std::mem::MaybeUninit;
2407
2408    use super::{
2409        BridgeError, IpcClient, authentication_result, bridge_ok, execution_state_to_ffi,
2410        parse_channel_switch_arg,
2411    };
2412    use crate::data::execution::ChromeTabExecutionState;
2413
2414    fn null_ipc_client() -> IpcClient {
2415        // SAFETY: `IpcClient` is a raw pointer wrapper; a zeroed value is sufficient
2416        // for testing the null-handle guard.
2417        unsafe { MaybeUninit::zeroed().assume_init() }
2418    }
2419
2420    #[test]
2421    fn parse_channel_switch_arg_rejects_missing_nul() {
2422        assert_eq!(
2423            parse_channel_switch_arg(b"--cbf-ipc-handle=abc"),
2424            Err(BridgeError::InvalidChannelArgument)
2425        );
2426    }
2427
2428    #[test]
2429    fn parse_channel_switch_arg_rejects_invalid_utf8() {
2430        assert_eq!(
2431            parse_channel_switch_arg(b"--cbf-ipc-handle=\xFF\0"),
2432            Err(BridgeError::InvalidChannelArgument)
2433        );
2434    }
2435
2436    #[test]
2437    fn parse_channel_switch_arg_rejects_empty_string() {
2438        assert_eq!(
2439            parse_channel_switch_arg(b"\0"),
2440            Err(BridgeError::InvalidChannelArgument)
2441        );
2442    }
2443
2444    #[test]
2445    fn null_client_handle_reports_invalid_state() {
2446        let client = null_ipc_client();
2447
2448        assert_eq!(client.ensure_ready(), Err(BridgeError::InvalidState));
2449    }
2450
2451    #[test]
2452    fn authentication_result_reports_authentication_failure() {
2453        assert_eq!(
2454            authentication_result(false),
2455            Err(BridgeError::AuthenticationFailed)
2456        );
2457    }
2458
2459    #[test]
2460    fn bridge_ok_reports_operation_name() {
2461        assert_eq!(
2462            bridge_ok("navigate", false),
2463            Err(BridgeError::OperationFailed {
2464                operation: "navigate",
2465            })
2466        );
2467    }
2468
2469    #[test]
2470    fn execution_state_to_ffi_encodes_expected_values() {
2471        assert_eq!(
2472            execution_state_to_ffi(ChromeTabExecutionState::Running),
2473            super::CbfTabExecutionState_kCbfTabExecutionStateRunning as u8
2474        );
2475        assert_eq!(
2476            execution_state_to_ffi(ChromeTabExecutionState::Suspended),
2477            super::CbfTabExecutionState_kCbfTabExecutionStateSuspended as u8
2478        );
2479    }
2480}