1use crate::{LogLevel, WebViewError, WebViewInputError, WebViewScriptError};
2use async_trait::async_trait;
3use serde::{Deserialize, Serialize};
4use std::future::Future;
5use std::path::PathBuf;
6use std::pin::Pin;
7use std::sync::Arc;
8
9#[derive(Debug)]
11pub enum SchemeOutcome {
12 Handled(WebResourceResponse),
14 PassThrough,
16}
17
18pub(crate) type AsyncSchemeFuture = Pin<Box<dyn Future<Output = SchemeOutcome> + Send + 'static>>;
20pub(crate) type AsyncSchemeHandler =
21 Arc<dyn Fn(http::Request<Vec<u8>>) -> AsyncSchemeFuture + Send + Sync>;
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum NavigationPolicy {
26 Allow,
28 Cancel,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct NavigationRequest {
36 pub url: String,
37 pub has_user_gesture: bool,
38 pub is_main_frame: bool,
39}
40
41impl NavigationRequest {
42 pub fn new(url: impl Into<String>, has_user_gesture: bool, is_main_frame: bool) -> Self {
43 Self {
44 url: url.into(),
45 has_user_gesture,
46 is_main_frame,
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum NewWindowPolicy {
54 LoadInSelf,
56 Cancel,
58}
59
60pub type NavigationHandler = Box<dyn Fn(&NavigationRequest) -> NavigationPolicy + Send + Sync>;
61pub type NewWindowHandler = Box<dyn Fn(&str) -> NewWindowPolicy + Send + Sync>;
62
63#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum UserAgentOverride {
66 Default,
68 Custom(String),
72}
73
74impl UserAgentOverride {
75 pub fn validate(&self) -> Result<(), WebViewError> {
77 if let Self::Custom(value) = self {
78 if value.trim().is_empty() {
79 return Err(WebViewError::WebView(
80 "custom user-agent override must not be empty".to_string(),
81 ));
82 }
83 if value.contains(['\r', '\n', '\0']) {
84 return Err(WebViewError::WebView(
85 "custom user-agent override must not contain CR, LF, or NUL".to_string(),
86 ));
87 }
88 }
89 Ok(())
90 }
91}
92
93#[cfg(test)]
94mod user_agent_override_tests {
95 use super::*;
96
97 #[test]
98 fn custom_user_agent_must_not_be_blank() {
99 assert!(UserAgentOverride::Custom(String::new()).validate().is_err());
100 assert!(UserAgentOverride::Custom(" ".into()).validate().is_err());
101 assert!(
102 UserAgentOverride::Custom("Mozilla/5.0 valid".into())
103 .validate()
104 .is_ok()
105 );
106 for invalid in [
107 "Mozilla/5.0\rInjected",
108 "Mozilla/5.0\nInjected",
109 "Mozilla\0/5.0",
110 ] {
111 assert!(
112 UserAgentOverride::Custom(invalid.into())
113 .validate()
114 .is_err()
115 );
116 }
117 assert!(UserAgentOverride::Default.validate().is_ok());
118 }
119}
120
121#[derive(Debug, Clone, PartialEq, Eq)]
122pub struct DownloadRequest {
123 pub url: String,
125 pub user_agent: Option<String>,
127 pub content_disposition: Option<String>,
129 pub mime_type: Option<String>,
131 pub content_length: Option<u64>,
133 pub suggested_filename: Option<String>,
135 pub source_page_url: Option<String>,
137 pub cookie: Option<String>,
139}
140
141pub type DownloadHandler = Box<dyn Fn(DownloadRequest) + Send + Sync>;
146
147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
148#[serde(rename_all = "lowercase")]
149pub enum WebViewCookieSameSite {
150 Lax,
151 Strict,
152 None,
153}
154
155impl WebViewCookieSameSite {
156 pub fn as_str(self) -> &'static str {
157 match self {
158 Self::Lax => "lax",
159 Self::Strict => "strict",
160 Self::None => "none",
161 }
162 }
163}
164
165#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
166pub struct WebViewCookie {
167 pub name: String,
168 pub value: String,
169 pub domain: String,
170 pub path: String,
171 #[serde(default, skip_serializing_if = "is_false")]
172 pub host_only: bool,
173 #[serde(default)]
174 pub secure: bool,
175 #[serde(default)]
176 pub http_only: bool,
177 #[serde(default)]
178 pub session: bool,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
180 pub expires_unix_ms: Option<i64>,
181 #[serde(default, skip_serializing_if = "Option::is_none")]
182 pub same_site: Option<WebViewCookieSameSite>,
183}
184
185#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
186pub struct WebViewCookieSetRequest {
187 #[serde(default)]
188 pub url: String,
189 pub name: String,
190 pub value: String,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
192 pub domain: Option<String>,
193 #[serde(default = "default_cookie_path")]
194 pub path: String,
195 #[serde(default)]
196 pub secure: bool,
197 #[serde(default)]
198 pub http_only: bool,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub expires_unix_ms: Option<i64>,
201 #[serde(default, skip_serializing_if = "Option::is_none")]
202 pub same_site: Option<WebViewCookieSameSite>,
203}
204
205fn default_cookie_path() -> String {
206 "/".to_string()
207}
208
209fn is_false(value: &bool) -> bool {
210 !*value
211}
212
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct FileChooserRequest {
215 pub accept_types: Vec<String>,
217 pub allow_multiple: bool,
219 pub allow_directories: bool,
221 pub capture: bool,
223 pub source_page_url: Option<String>,
225}
226
227#[derive(Debug, Clone, PartialEq, Eq)]
228pub struct FileChooserFile {
229 pub path: Option<String>,
230 pub uri: Option<String>,
231}
232
233#[derive(Debug, Clone, PartialEq, Eq)]
234pub enum FileChooserResponse {
235 Cancel,
236 Error(String),
237 Files(Vec<FileChooserFile>),
238}
239
240#[derive(Debug)]
242pub enum WebResourceBody {
243 Path(PathBuf),
245 Pipe(SystemPipeReader),
247 Bytes(Vec<u8>),
249}
250
251#[derive(Debug)]
253pub struct SystemPipeReader {
254 #[cfg(unix)]
255 fd: std::os::fd::RawFd,
256 #[cfg(windows)]
257 handle: std::os::windows::io::RawHandle,
258}
259
260impl SystemPipeReader {
261 #[cfg(unix)]
264 pub fn into_raw_fd(self) -> std::os::fd::RawFd {
265 self.fd
266 }
267
268 #[cfg(unix)]
274 pub unsafe fn from_raw_fd(fd: std::os::fd::RawFd) -> Self {
275 Self { fd }
276 }
277
278 #[cfg(unix)]
280 pub fn into_file(self) -> std::fs::File {
281 use std::os::fd::FromRawFd;
282 unsafe { std::fs::File::from_raw_fd(self.into_raw_fd()) }
283 }
284
285 #[cfg(windows)]
288 pub fn into_raw_handle(self) -> std::os::windows::io::RawHandle {
289 self.handle
290 }
291
292 #[cfg(windows)]
298 pub unsafe fn from_raw_handle(handle: std::os::windows::io::RawHandle) -> Self {
299 Self { handle }
300 }
301
302 #[cfg(windows)]
304 pub fn into_file(self) -> std::fs::File {
305 use std::os::windows::io::FromRawHandle;
306 unsafe { std::fs::File::from_raw_handle(self.into_raw_handle()) }
307 }
308}
309
310#[async_trait]
312pub trait WebViewController: Send + Sync {
313 fn load_url(&self, url: &str) -> Result<(), WebViewError>;
315
316 fn load_data(&self, request: LoadDataRequest<'_>) -> Result<(), WebViewError>;
318
319 fn exec_js(&self, js: &str) -> Result<(), WebViewError>;
321
322 async fn eval_js(&self, js: &str) -> Result<serde_json::Value, WebViewScriptError>;
332
333 async fn current_url(&self) -> Result<Option<String>, WebViewError> {
335 Err(WebViewError::WebView(
336 "current_url is not implemented for this platform".to_string(),
337 ))
338 }
339
340 fn post_message(&self, message: &str) -> Result<(), WebViewError>;
342
343 fn clear_browsing_data(&self) -> Result<(), WebViewError>;
345
346 fn set_user_agent_override(&self, user_agent: UserAgentOverride) -> Result<(), WebViewError>;
348
349 fn reload(&self) -> Result<(), WebViewError> {
351 Err(WebViewError::WebView(
352 "reload is not implemented for this platform".to_string(),
353 ))
354 }
355
356 fn go_back(&self) -> Result<(), WebViewError> {
358 Err(WebViewError::WebView(
359 "go_back is not implemented for this platform".to_string(),
360 ))
361 }
362
363 fn go_forward(&self) -> Result<(), WebViewError> {
365 Err(WebViewError::WebView(
366 "go_forward is not implemented for this platform".to_string(),
367 ))
368 }
369
370 async fn list_cookies(&self) -> Result<Vec<WebViewCookie>, WebViewError> {
372 Err(WebViewError::WebView(
373 "cookie store is not implemented for this platform".to_string(),
374 ))
375 }
376
377 async fn set_cookie(&self, _request: WebViewCookieSetRequest) -> Result<(), WebViewError> {
379 Err(WebViewError::WebView(
380 "cookie store is not implemented for this platform".to_string(),
381 ))
382 }
383
384 async fn delete_cookie(
386 &self,
387 _name: &str,
388 _domain: &str,
389 _path: &str,
390 ) -> Result<(), WebViewError> {
391 Err(WebViewError::WebView(
392 "cookie store is not implemented for this platform".to_string(),
393 ))
394 }
395
396 async fn clear_cookies(&self) -> Result<(), WebViewError> {
398 Err(WebViewError::WebView(
399 "cookie store is not implemented for this platform".to_string(),
400 ))
401 }
402
403 async fn clear_site_data(
407 &self,
408 _url: &str,
409 _options: ClearSiteDataOptions,
410 ) -> Result<ClearSiteDataResult, WebViewError> {
411 Err(WebViewError::WebView(
412 "site-scoped data clearing is not implemented for this platform".to_string(),
413 ))
414 }
415
416 async fn take_screenshot(&self) -> Result<Vec<u8>, WebViewError> {
419 Err(WebViewError::WebView(
420 "screenshot is not implemented for this platform".to_string(),
421 ))
422 }
423
424 async fn start_network_capture(&self) -> Result<(), WebViewError> {
429 Err(WebViewError::WebView(
430 "network capture is not implemented for this platform".to_string(),
431 ))
432 }
433
434 async fn stop_network_capture(&self) -> Result<(), WebViewError> {
437 Err(WebViewError::WebView(
438 "network capture is not implemented for this platform".to_string(),
439 ))
440 }
441
442 async fn network_entries(&self) -> Result<NetworkCaptureSnapshot, WebViewError> {
445 Err(WebViewError::WebView(
446 "network capture is not implemented for this platform".to_string(),
447 ))
448 }
449
450 async fn clear_network_capture(&self) -> Result<(), WebViewError> {
452 Err(WebViewError::WebView(
453 "network capture is not implemented for this platform".to_string(),
454 ))
455 }
456}
457
458#[derive(Debug, Clone, Copy)]
461pub struct ClearSiteDataOptions {
462 pub cache: bool,
463 pub site_data: bool,
464}
465
466#[derive(Debug, Clone, Copy)]
474pub struct ClearSiteDataResult {
475 pub cache_cleared: bool,
476 pub site_data_cleared: bool,
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct NetworkEntry {
482 pub request_id: String,
484 pub url: String,
485 pub method: String,
486 pub resource_type: Option<String>,
489 pub request_headers: Vec<(String, String)>,
490 pub request_body: Option<String>,
492 pub status: Option<u16>,
493 pub response_headers: Vec<(String, String)>,
494 pub mime_type: Option<String>,
495 pub response_body: NetworkBody,
496 pub from_cache: bool,
497 pub failed: Option<String>,
500 pub wall_time: Option<f64>,
502 pub started: f64,
504 pub finished: Option<f64>,
505}
506
507impl NetworkEntry {
508 pub fn duration_ms(&self) -> Option<f64> {
510 self.finished
511 .filter(|finished| *finished >= self.started)
512 .map(|finished| (finished - self.started) * 1000.0)
513 }
514}
515
516#[derive(Debug, Clone, Default, Serialize, Deserialize)]
518#[serde(tag = "kind", rename_all = "snake_case")]
519pub enum NetworkBody {
520 #[default]
522 None,
523 Text { text: String },
525 Base64 { base64: String },
527 Skipped { reason: String },
530}
531
532#[derive(Debug, Clone, Default, Serialize, Deserialize)]
534pub struct NetworkCaptureSnapshot {
535 pub entries: Vec<NetworkEntry>,
536 pub dropped: u64,
539}
540
541#[derive(Debug, Clone, Default, Serialize, Deserialize)]
542pub struct ClickOptions {
543 #[serde(default, skip_serializing_if = "Option::is_none")]
544 pub index: Option<usize>,
545}
546
547#[derive(Debug, Clone, Default, Serialize, Deserialize)]
548pub struct TypeOptions {
549 #[serde(default, skip_serializing_if = "Option::is_none")]
550 pub index: Option<usize>,
551 #[serde(default)]
552 pub replace: bool,
553}
554
555#[derive(Debug, Clone, Default, Serialize, Deserialize)]
556pub struct FillOptions {
557 #[serde(default, skip_serializing_if = "Option::is_none")]
558 pub index: Option<usize>,
559}
560
561#[derive(Debug, Clone, Default, Serialize, Deserialize)]
562pub struct PressOptions {
563 #[serde(default, skip_serializing_if = "Option::is_none")]
564 pub selector: Option<String>,
565 #[serde(default, skip_serializing_if = "Option::is_none")]
566 pub index: Option<usize>,
567}
568
569#[derive(Debug, Clone, Default, Serialize, Deserialize)]
570pub struct ScrollOptions;
571
572#[async_trait]
573pub trait WebViewInputController: WebViewController {
574 async fn click(
575 &self,
576 _selector: &str,
577 _options: ClickOptions,
578 ) -> Result<(), WebViewInputError> {
579 Err(WebViewInputError::Unsupported(
580 "input control is not implemented for this platform",
581 ))
582 }
583
584 async fn type_text(
585 &self,
586 _selector: &str,
587 _text: &str,
588 _options: TypeOptions,
589 ) -> Result<(), WebViewInputError> {
590 Err(WebViewInputError::Unsupported(
591 "input control is not implemented for this platform",
592 ))
593 }
594
595 async fn fill(
596 &self,
597 _selector: &str,
598 _text: &str,
599 _options: FillOptions,
600 ) -> Result<(), WebViewInputError> {
601 Err(WebViewInputError::Unsupported(
602 "input control is not implemented for this platform",
603 ))
604 }
605
606 async fn press(&self, _key: &str, _options: PressOptions) -> Result<(), WebViewInputError> {
607 Err(WebViewInputError::Unsupported(
608 "input control is not implemented for this platform",
609 ))
610 }
611
612 async fn scroll(
613 &self,
614 _dx: f64,
615 _dy: f64,
616 _options: ScrollOptions,
617 ) -> Result<(), WebViewInputError> {
618 Err(WebViewInputError::Unsupported(
619 "input control is not implemented for this platform",
620 ))
621 }
622
623 async fn scroll_to(
624 &self,
625 _selector: &str,
626 _options: ScrollOptions,
627 ) -> Result<(), WebViewInputError> {
628 Err(WebViewInputError::Unsupported(
629 "input control is not implemented for this platform",
630 ))
631 }
632}
633
634#[derive(Debug, Clone, Copy)]
635pub struct LoadDataRequest<'a> {
636 pub data: &'a str,
637 pub base_url: &'a str,
638 pub history_url: Option<&'a str>,
639}
640
641impl<'a> LoadDataRequest<'a> {
642 pub fn new(data: &'a str, base_url: &'a str) -> Self {
643 Self {
644 data,
645 base_url,
646 history_url: None,
647 }
648 }
649
650 pub fn with_history_url(mut self, history_url: &'a str) -> Self {
651 self.history_url = Some(history_url);
652 self
653 }
654}
655
656#[derive(Debug, Clone, Copy, PartialEq, Eq)]
661pub enum LoadErrorKind {
662 Dns,
663 Network,
664 Timeout,
665 Security,
666 InvalidUrl,
667 NotFound,
668 Unknown,
669}
670
671#[derive(Debug, Clone, PartialEq, Eq)]
677pub struct LoadError {
678 pub failing_url: Option<String>,
680 pub kind: LoadErrorKind,
682 pub description: String,
684}
685
686pub trait WebViewDelegate: Send + Sync {
719 fn on_navigation_event(&self, event: crate::events::NavigationEvent);
724
725 fn on_webview_state_change(&self, _change: crate::events::WebViewStateChange) {}
729
730 fn handle_post_message(&self, msg: String);
732
733 fn handle_native_component_message(&self, _message_json: String) {}
739
740 fn log(&self, level: LogLevel, message: &str);
742}
743
744#[derive(Debug)]
746pub struct WebResourceResponse {
747 parts: http::response::Parts,
748 body: WebResourceBody,
749}
750
751impl From<Option<WebResourceResponse>> for SchemeOutcome {
752 fn from(value: Option<WebResourceResponse>) -> Self {
753 match value {
754 Some(response) => SchemeOutcome::Handled(response),
755 None => SchemeOutcome::PassThrough,
756 }
757 }
758}
759
760impl WebResourceResponse {
761 pub fn parts(&self) -> &http::response::Parts {
763 &self.parts
764 }
765
766 pub fn into_parts(self) -> (http::response::Parts, WebResourceBody) {
768 (self.parts, self.body)
769 }
770}
771
772impl From<(http::response::Parts, PathBuf)> for WebResourceResponse {
774 fn from(value: (http::response::Parts, PathBuf)) -> Self {
775 WebResourceResponse {
776 parts: value.0,
777 body: WebResourceBody::Path(value.1),
778 }
779 }
780}
781
782impl From<(http::response::Parts, SystemPipeReader)> for WebResourceResponse {
784 fn from(value: (http::response::Parts, SystemPipeReader)) -> Self {
785 WebResourceResponse {
786 parts: value.0,
787 body: WebResourceBody::Pipe(value.1),
788 }
789 }
790}
791
792impl From<(http::response::Parts, Vec<u8>)> for WebResourceResponse {
794 fn from(value: (http::response::Parts, Vec<u8>)) -> Self {
795 WebResourceResponse {
796 parts: value.0,
797 body: WebResourceBody::Bytes(value.1),
798 }
799 }
800}
801
802impl WebResourceResponse {
803 fn response_parts_with_status(status: u16) -> http::response::Parts {
804 let response = match http::Response::builder().status(status).body(()) {
805 Ok(response) => response,
806 Err(_) => http::Response::new(()),
807 };
808 let (parts, _) = response.into_parts();
809 parts
810 }
811
812 pub fn file(path: impl Into<PathBuf>) -> Self {
814 let path = path.into();
815 let content_length = std::fs::metadata(&path).ok().map(|m| m.len());
816 let mut parts = Self::response_parts_with_status(200);
817 if let Some(len) = content_length {
818 parts
819 .headers
820 .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
821 }
822 Self {
823 parts,
824 body: WebResourceBody::Path(path),
825 }
826 }
827
828 pub fn bytes(data: impl Into<Vec<u8>>) -> Self {
830 let data = data.into();
831 let len = data.len();
832 let mut parts = Self::response_parts_with_status(200);
833 parts
834 .headers
835 .insert(http::header::CONTENT_LENGTH, http::HeaderValue::from(len));
836 Self {
837 parts,
838 body: WebResourceBody::Bytes(data),
839 }
840 }
841
842 pub fn stream(reader: SystemPipeReader) -> Self {
844 let parts = Self::response_parts_with_status(200);
845 Self {
846 parts,
847 body: WebResourceBody::Pipe(reader),
848 }
849 }
850
851 pub fn mime(mut self, content_type: &str) -> Self {
853 if let Ok(value) = http::HeaderValue::from_str(content_type) {
854 self.parts.headers.insert(http::header::CONTENT_TYPE, value);
855 }
856 self
857 }
858
859 pub fn status(mut self, code: u16) -> Self {
861 self.parts.status = http::StatusCode::from_u16(code).unwrap_or(self.parts.status);
862 self
863 }
864
865 pub fn header(mut self, name: &str, value: &str) -> Self {
867 if let (Ok(header_name), Ok(header_value)) = (
868 name.parse::<http::header::HeaderName>(),
869 http::HeaderValue::from_str(value),
870 ) {
871 self.parts.headers.insert(header_name, header_value);
872 }
873 self
874 }
875
876 pub fn cors(self) -> Self {
878 self.header("access-control-allow-origin", "null")
879 }
880}