1use crate::error::BrowserError;
18use crate::page::PageHandle;
19use dashmap::DashMap;
22use mozjs::rooted;
23use std::cell::RefCell;
24use std::ptr::{self, NonNull};
25use std::sync::atomic::{AtomicBool, Ordering};
26use std::sync::mpsc;
27use std::sync::{Arc, OnceLock};
28
29use std::time::Duration;
30
31#[derive(Debug, Clone, PartialEq, Eq, Default)]
41pub struct EvaluateResult {
42 pub value: Option<String>,
44 pub error: Option<String>,
46}
47
48impl EvaluateResult {
49 pub fn ok(value: String) -> Self {
51 EvaluateResult {
52 value: Some(value),
53 error: None,
54 }
55 }
56
57 pub fn err(error: String) -> Self {
59 EvaluateResult {
60 value: None,
61 error: Some(error),
62 }
63 }
64
65 pub fn is_ok(&self) -> bool {
67 self.error.is_none()
68 }
69
70 pub fn is_err(&self) -> bool {
72 self.error.is_some()
73 }
74}
75
76static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>> = OnceLock::new();
116static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>> = OnceLock::new();
117
118fn node_realm_by_webview() -> &'static DashMap<servo::WebViewId, usize> {
119 NODE_REALM_BY_WEBVIEW.get_or_init(DashMap::new)
120}
121
122fn page_global_by_webview() -> &'static DashMap<servo::WebViewId, usize> {
123 PAGE_GLOBAL_BY_WEBVIEW.get_or_init(DashMap::new)
124}
125
126thread_local! {
131 static PER_THREAD_PAGE_GLOBAL: RefCell<*mut mozjs::jsapi::JSObject> =
132 const { RefCell::new(ptr::null_mut()) };
133}
134
135fn store_node_realm(
141 webview_id: servo::WebViewId,
142 page_global: *mut mozjs::jsapi::JSObject,
143 node_global: *mut mozjs::jsapi::JSObject,
144) {
145 node_realm_by_webview().insert(webview_id, node_global as usize);
146 page_global_by_webview().insert(webview_id, page_global as usize);
147}
148
149fn get_node_realm_by_id(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
155 match node_realm_by_webview().get(&webview_id) {
156 Some(v) => *v as *mut mozjs::jsapi::JSObject,
157 None => ptr::null_mut(),
158 }
159}
160
161fn get_page_global_by_id(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
163 match page_global_by_webview().get(&webview_id) {
164 Some(v) => *v as *mut mozjs::jsapi::JSObject,
165 None => ptr::null_mut(),
166 }
167}
168
169pub fn remove_node_realm_by_id(webview_id: servo::WebViewId) {
171 node_realm_by_webview().remove(&webview_id);
172 page_global_by_webview().remove(&webview_id);
173}
174
175fn clear_all_node_realms() {
177 node_realm_by_webview().clear();
178 page_global_by_webview().clear();
179}
180
181static TEST_SERIAL_LOCK: std::sync::OnceLock<std::sync::Mutex<()>> = std::sync::OnceLock::new();
186
187fn test_serial_lock() -> &'static std::sync::Mutex<()> {
188 TEST_SERIAL_LOCK.get_or_init(|| std::sync::Mutex::new(()))
189}
190
191pub fn register_refresh_dom_proxies(
198 webview_id: servo::WebViewId,
199 _old_page_global: *mut mozjs::jsapi::JSObject,
200) {
201 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
205 Box::new(move |_cx_ptr, new_page_global_ptr| unsafe {
206 refresh_dom_proxies_native(webview_id, new_page_global_ptr);
207 });
208
209 servo::register_script_thread_callback(webview_id, callback);
210}
211
212unsafe fn refresh_dom_proxies_native(
217 webview_id: servo::WebViewId,
218 new_page_global_ptr: *mut std::ffi::c_void,
219) {
220 use mozjs::jsapi::JSObject;
221
222 let new_page_global = new_page_global_ptr as *mut JSObject;
223
224 if new_page_global.is_null() {
225 return;
226 }
227
228 let old_page_global_opt = page_global_by_webview().get(&webview_id).map(|v| *v);
231 page_global_by_webview().insert(webview_id, new_page_global as usize);
232
233 if let Some(old_addr) = old_page_global_opt {
239 bao_stealth::engine_props::register_global_alias(old_addr, new_page_global as usize);
240 }
241}
242
243pub fn create_node_realm(webview_id: servo::WebViewId) -> bool {
265 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
266 Box::new(move |cx_ptr, page_global_ptr| unsafe {
267 create_node_realm_native(webview_id, cx_ptr, page_global_ptr);
268 });
269
270 servo::register_script_thread_callback(webview_id, callback);
271
272 true
273}
274
275pub fn get_node_realm_global(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
283 get_node_realm_by_id(webview_id)
284}
285
286pub fn get_page_global(webview_id: servo::WebViewId) -> *mut mozjs::jsapi::JSObject {
291 get_page_global_by_id(webview_id)
292}
293
294pub unsafe fn evaluate_in_node_realm(
313 cx_ptr: *mut std::ffi::c_void,
314 node_global: *mut mozjs::jsapi::JSObject,
315 script: &str,
316 result_out: Arc<OnceLock<EvaluateResult>>,
317) {
318 use mozjs::context::JSContext;
319 use mozjs::jsapi::JSContext as RawJSContext;
320 use mozjs::jsval::UndefinedValue;
321 use mozjs::realm::AutoRealm;
322 use mozjs::rust::evaluate_script;
323 use mozjs::rust::CompileOptionsWrapper;
324
325 if node_global.is_null() {
326 let _ = result_out.set(EvaluateResult::err("node_global is null".into()));
327 return;
328 }
329
330 let raw_cx = cx_ptr as *mut RawJSContext;
331 let cx_nn = match NonNull::new(raw_cx) {
332 Some(nn) => nn,
333 None => {
334 let _ = result_out.set(EvaluateResult::err("JSContext pointer is null".into()));
335 return;
336 }
337 };
338
339 let mut cx = JSContext::from_ptr(cx_nn);
340
341 let mut realm = AutoRealm::new(&mut cx, NonNull::new(node_global).unwrap());
352 let (node_global_handle, realm) = realm.global_and_reborrow();
353
354 let filename = c"bao_evaluate_js".to_owned();
355 let mut options = CompileOptionsWrapper::new(realm, filename, 1);
356 options.set_hide_script_from_debugger(true);
365
366 rooted!(&in(realm) let mut rval = UndefinedValue());
367 let eval_result = evaluate_script(
368 realm,
369 node_global_handle,
370 script,
371 rval.handle_mut(),
372 options,
373 );
374
375 if eval_result.is_err() {
376 let _ = result_out.set(EvaluateResult {
377 value: None,
378 error: Some("evaluate_script returned Err (JS exception thrown)".into()),
379 });
380 return;
381 }
382
383 bun_runtime::web_api::ws_pump_all(raw_cx);
389
390 let rval_val = rval.get();
392 let value = if rval_val.is_undefined() {
393 None
394 } else if rval_val.is_string() {
395 let js_str = rval_val.to_string();
397 if js_str.is_null() {
398 Some(String::new())
399 } else {
400 let raw_cx = realm.raw_cx();
403 match NonNull::new(js_str) {
404 Some(nn) => Some(mozjs::conversions::unsafe_jsstr_to_string(raw_cx, nn)),
405 None => Some(String::new()),
406 }
407 }
408 } else if rval_val.is_number() {
409 Some(rval_val.to_number().to_string())
410 } else if rval_val.is_boolean() {
411 Some(rval_val.to_boolean().to_string())
412 } else if rval_val.is_null() {
413 Some("null".into())
414 } else {
415 Some("[JSValue:object]".into())
417 };
418 let _ = result_out.set(EvaluateResult { value, error: None });
419}
420
421pub fn evaluate_js_via_node_realm(
445 webview_id: servo::WebViewId,
446 script: &str,
447) -> Arc<OnceLock<EvaluateResult>> {
448 let result = Arc::new(OnceLock::new());
449 let result_clone = result.clone();
450 let script_owned = script.to_string();
451
452 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> = Box::new(
453 move |cx_ptr: *mut std::ffi::c_void, _page_global: *mut std::ffi::c_void| {
454 let node_global = get_node_realm_by_id(webview_id);
458 unsafe {
459 evaluate_in_node_realm(cx_ptr, node_global, &script_owned, result_clone);
460 }
461 },
462 );
463
464 servo::register_script_thread_callback(webview_id, callback);
465 result
466}
467
468unsafe fn create_node_realm_native(
488 webview_id: servo::WebViewId,
489 cx_ptr: *mut std::ffi::c_void,
490 page_global_ptr: *mut std::ffi::c_void,
491) {
492 use mozjs::context::JSContext;
493 use mozjs::jsapi::{
494 JSContext as RawJSContext, JSObject, JS_FireOnNewGlobalObject, OnNewGlobalHookOption,
495 };
496 use mozjs::realm::AutoRealm;
497 use mozjs::rust::wrappers2::{JS_NewGlobalObject, JS_SetProperty, JS_WrapObject};
498 use mozjs::rust::{Handle, MutableHandle, RealmOptions, SIMPLE_GLOBAL_CLASS};
499
500 let raw_cx = cx_ptr as *mut RawJSContext;
501 let page_global = page_global_ptr as *mut JSObject;
502 let cx_nn = match NonNull::new(raw_cx) {
503 Some(nn) => nn,
504 None => return,
505 };
506
507 let mut cx = JSContext::from_ptr(cx_nn);
508
509 let mut options = RealmOptions::default();
510 options.creationOptions_.compSpec_ =
511 mozjs::jsapi::JS::CompartmentSpecifier::NewCompartmentAndZone;
512
513 rooted!(&in(cx) let global = JS_NewGlobalObject(
514 &mut cx,
515 &SIMPLE_GLOBAL_CLASS,
516 ptr::null_mut(),
517 OnNewGlobalHookOption::DontFireOnNewGlobalHook,
518 &*options,
519 ));
520
521 if global.get().is_null() {
522 return;
523 }
524
525 let mut realm = AutoRealm::new_from_handle(&mut cx, global.handle());
526 let realm_cx: &mut JSContext = &mut realm;
527 JS_FireOnNewGlobalObject(realm_cx.raw_cx(), global.handle().into());
528
529 bun_runtime::globals::install_node_apis(realm_cx, global.handle());
530 bun_runtime::globals::install_web_apis(realm_cx, global.handle());
531
532 if !page_global.is_null() {
533 install_lazy_dom_getters(realm_cx, global.handle());
536 }
537
538 if !page_global.is_null() {
541 PER_THREAD_PAGE_GLOBAL.with(|cell| {
542 *cell.borrow_mut() = page_global;
543 });
544 }
545
546 store_node_realm(webview_id, page_global, global.get());
548
549 if !page_global.is_null() {
557 bao_stealth::engine_props::register_global_alias(
558 page_global as usize,
559 global.get() as usize,
560 );
561 }
562}
563
564unsafe fn wrap_and_install_dom_proxy(
577 cx: &mut mozjs::context::JSContext,
578 node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
579 page_global: *mut mozjs::jsapi::JSObject,
580 property_name: &str,
581) {
582 use mozjs::jsapi::{JS_GetProperty, JS_SetProperty};
583 use mozjs::jsval::{ObjectValue, UndefinedValue};
584 use mozjs::rust::wrappers2::JS_WrapObject;
585
586 let raw_cx = cx.raw_cx();
587 rooted!(&in(cx) let page_global_root = page_global);
592
593 let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
595 let mut raw_prop_val = UndefinedValue();
602 let got = bao_stealth::engine_props::get_property_clearing(
603 raw_cx,
604 page_global_root.handle().into(),
605 c_name.as_cstr(),
606 &mut raw_prop_val,
607 );
608 rooted!(&in(cx) let mut prop_val = raw_prop_val);
609
610 if got && prop_val.get().is_object() {
612 rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
614
615 if !JS_WrapObject(cx, prop_obj.handle_mut().into()) {
617 mozjs::jsapi::JS_ClearPendingException(raw_cx);
620 return;
621 }
622
623 rooted!(&in(cx) let mut wrapped_val = ObjectValue(prop_obj.get()));
625 if !JS_SetProperty(
628 raw_cx,
629 node_global.into(),
630 c_name.as_ptr(),
631 wrapped_val.handle_mut().into(),
632 ) {
633 mozjs::jsapi::JS_ClearPendingException(raw_cx);
634 }
635 }
636}
637
638unsafe fn install_lazy_dom_getters(
646 cx: &mut mozjs::context::JSContext,
647 node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
648) {
649 use mozjs::jsapi::JS_DefineProperty1;
650 use mozjs::jsval::UndefinedValue;
651
652 let raw_cx = cx.raw_cx();
653
654 let obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY) as u32;
656 let ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE
660 | mozjs::jsapi::JSPROP_READONLY
661 | mozjs::jsapi::JSPROP_PERMANENT) as u32;
662
663 let obj_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
664 (c"window", Some(lazy_dom_getter_window)),
665 (c"document", Some(lazy_dom_getter_document)),
666 (c"navigator", Some(lazy_dom_getter_navigator)),
667 ];
668 let ctor_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
675 (c"Worker", Some(lazy_dom_getter_worker)),
676 (c"SharedWorker", Some(lazy_dom_getter_shared_worker)),
677 (c"ServiceWorker", Some(lazy_dom_getter_service_worker)),
678 ];
679 for &(name, getter) in obj_getters {
680 if !JS_DefineProperty1(
695 raw_cx,
696 node_global.into(),
697 name.as_ptr(),
698 getter,
699 None,
700 obj_attrs,
701 ) {
702 mozjs::jsapi::JS_ClearPendingException(raw_cx);
703 }
704 }
705 for &(name, getter) in ctor_getters {
706 if !JS_DefineProperty1(
710 raw_cx,
711 node_global.into(),
712 name.as_ptr(),
713 getter,
714 None,
715 ctor_attrs,
716 ) {
717 mozjs::jsapi::JS_ClearPendingException(raw_cx);
718 }
719 }
720}
721
722#[allow(unsafe_op_in_unsafe_fn)]
728unsafe extern "C" fn lazy_dom_getter_window(
729 cx: *mut mozjs::jsapi::JSContext,
730 argc: u32,
731 vp: *mut mozjs::jsval::JSVal,
732) -> bool {
733 lazy_dom_getter_impl(cx, argc, vp, "window")
734}
735
736#[allow(unsafe_op_in_unsafe_fn)]
738unsafe extern "C" fn lazy_dom_getter_document(
739 cx: *mut mozjs::jsapi::JSContext,
740 argc: u32,
741 vp: *mut mozjs::jsval::JSVal,
742) -> bool {
743 lazy_dom_getter_impl(cx, argc, vp, "document")
744}
745
746#[allow(unsafe_op_in_unsafe_fn)]
748unsafe extern "C" fn lazy_dom_getter_navigator(
749 cx: *mut mozjs::jsapi::JSContext,
750 argc: u32,
751 vp: *mut mozjs::jsval::JSVal,
752) -> bool {
753 lazy_dom_getter_impl(cx, argc, vp, "navigator")
754}
755
756#[allow(unsafe_op_in_unsafe_fn)]
780unsafe extern "C" fn lazy_dom_getter_worker(
781 cx: *mut mozjs::jsapi::JSContext,
782 argc: u32,
783 vp: *mut mozjs::jsval::JSVal,
784) -> bool {
785 lazy_constructor_getter_impl(cx, argc, vp, "Worker")
786}
787
788#[allow(unsafe_op_in_unsafe_fn)]
790unsafe extern "C" fn lazy_dom_getter_shared_worker(
791 cx: *mut mozjs::jsapi::JSContext,
792 argc: u32,
793 vp: *mut mozjs::jsval::JSVal,
794) -> bool {
795 lazy_constructor_getter_impl(cx, argc, vp, "SharedWorker")
796}
797
798#[allow(unsafe_op_in_unsafe_fn)]
800unsafe extern "C" fn lazy_dom_getter_service_worker(
801 cx: *mut mozjs::jsapi::JSContext,
802 argc: u32,
803 vp: *mut mozjs::jsval::JSVal,
804) -> bool {
805 lazy_constructor_getter_impl(cx, argc, vp, "ServiceWorker")
806}
807
808#[allow(unsafe_op_in_unsafe_fn)]
828unsafe fn lazy_constructor_getter_impl(
829 raw_cx: *mut mozjs::jsapi::JSContext,
830 _argc: u32,
831 vp: *mut mozjs::jsval::JSVal,
832 property_name: &str,
833) -> bool {
834 use mozjs::context::JSContext;
835 use mozjs::jsapi::{JSObject, JS_DefineProperty1, JS_GetProperty};
836 use mozjs::jsval::{ObjectValue, UndefinedValue};
837 use mozjs::rust::wrappers2::JS_WrapObject;
838 use std::ptr::NonNull;
839
840 let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
841 args.rval().set(UndefinedValue());
842
843 let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
844 if node_global.is_null() {
845 return true;
846 }
847
848 let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
849 if page_global.is_null() {
850 let msg = format!(
852 "Cannot access {} constructor: no browser page is currently loaded",
853 property_name
854 );
855 report_reference_error(raw_cx, &msg);
856 return false;
857 }
858
859 let cx_nn = match NonNull::new(raw_cx) {
860 Some(nn) => nn,
861 None => return true,
862 };
863 let mut cx = JSContext::from_ptr(cx_nn);
864
865 rooted!(&in(cx) let page_global_root = page_global);
867 let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
868 let mut raw_prop_val = UndefinedValue();
874 let got = bao_stealth::engine_props::get_property_clearing(
875 raw_cx,
876 page_global_root.handle().into(),
877 c_name.as_cstr(),
878 &mut raw_prop_val,
879 );
880 rooted!(&in(cx) let mut prop_val = raw_prop_val);
881
882 if !got || !prop_val.get().is_object() {
883 let msg = format!(
887 "Cannot access {} constructor: not available in the current browser context",
888 property_name
889 );
890 report_reference_error(raw_cx, &msg);
891 return false;
892 }
893
894 rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
895
896 if !mozjs::jsapi::IsConstructor(*prop_obj.handle()) {
902 let msg = format!(
903 "{} is not a constructor — the browser context does not support this API",
904 property_name
905 );
906 report_reference_error(raw_cx, &msg);
907 return false;
908 }
909
910 if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
918 return false;
919 }
920
921 args.rval().set(ObjectValue(prop_obj.get()));
939 true
940}
941
942#[allow(unsafe_op_in_unsafe_fn)]
952unsafe fn report_reference_error(cx: *mut mozjs::jsapi::JSContext, message: &str) {
953 use std::ffi::CString;
954 use std::os::raw::c_void;
955
956 let c_msg = match CString::new(message.as_bytes()) {
957 Ok(s) => s,
958 Err(_) => return,
959 };
960
961 static FORMAT_STRING: &std::ffi::CStr = c"{0}";
966
967 unsafe extern "C" fn get_reference_error_format(
970 _user_ref: *mut std::os::raw::c_void,
971 _error_number: u32,
972 ) -> *const mozjs::jsapi::JSErrorFormatString {
973 static mut FORMAT: mozjs::jsapi::JSErrorFormatString = mozjs::jsapi::JSErrorFormatString {
974 name: c"RUSTMSG_REFERENCE_ERROR".as_ptr(),
975 format: FORMAT_STRING.as_ptr(),
976 argCount: 1,
977 exnType: mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as i16,
978 };
979 unsafe { &raw const FORMAT }
982 }
983
984 mozjs::jsapi::JS_ReportErrorNumberUTF8(
988 cx,
989 Some(get_reference_error_format),
990 std::ptr::null_mut(),
991 mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as u32,
992 c_msg.as_ptr(),
993 );
994}
995
996#[allow(unsafe_op_in_unsafe_fn)]
1012unsafe fn lazy_dom_getter_impl(
1013 raw_cx: *mut mozjs::jsapi::JSContext,
1014 _argc: u32,
1015 vp: *mut mozjs::jsval::JSVal,
1016 property_name: &str,
1017) -> bool {
1018 use mozjs::context::JSContext;
1019 use mozjs::jsapi::{JSObject, JS_GetProperty};
1020 use mozjs::jsval::{ObjectValue, UndefinedValue};
1021 use mozjs::rust::wrappers2::JS_WrapObject;
1022 use std::ptr::NonNull;
1023
1024 let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
1025 args.rval().set(UndefinedValue());
1026
1027 let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
1028 if node_global.is_null() {
1029 return true;
1030 }
1031
1032 let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
1037 if page_global.is_null() {
1038 return true;
1039 }
1040
1041 let cx_nn = match NonNull::new(raw_cx) {
1043 Some(nn) => nn,
1044 None => return true,
1045 };
1046 let mut cx = JSContext::from_ptr(cx_nn);
1047
1048 rooted!(&in(cx) let page_global_root = page_global);
1054
1055 let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
1056 let mut raw_prop_val = UndefinedValue();
1066 let got = bao_stealth::engine_props::get_property_clearing(
1067 raw_cx,
1068 page_global_root.handle().into(),
1069 c_name.as_cstr(),
1070 &mut raw_prop_val,
1071 );
1072 rooted!(&in(cx) let mut prop_val = raw_prop_val);
1073
1074 if !got || !prop_val.get().is_object() {
1075 return true;
1076 }
1077
1078 rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
1080 if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
1081 mozjs::jsapi::JS_ClearPendingException(raw_cx);
1084 return true;
1085 }
1086
1087 args.rval().set(ObjectValue(prop_obj.get()));
1088 true
1089}
1090
1091pub fn inject_node_apis(page: &PageHandle) -> Result<(), BrowserError> {
1104 inject_node_apis_with_stealth(page, None)
1105}
1106
1107pub fn inject_node_apis_with_stealth(
1118 page: &PageHandle,
1119 stealth_profile: Option<bao_stealth::StealthProfile>,
1120) -> Result<(), BrowserError> {
1121 let webview_id = page
1122 .webview_id()
1123 .ok_or_else(|| BrowserError::Init("page has no webview".into()))?;
1124
1125 let registered = register_native_host_functions(webview_id, stealth_profile);
1126
1127 let node_realm_registered = create_node_realm(webview_id);
1130 debug_assert!(
1131 node_realm_registered,
1132 "create_node_realm registration failed"
1133 );
1134
1135 page.drain_callbacks()?;
1143
1144 let page_global = get_page_global(webview_id);
1148 let node_global = get_node_realm_global(webview_id);
1149 page.set_page_global(page_global, node_global);
1150
1151 if !registered {
1152 page.evaluate_js_web(WEB_POLYFILLS)?;
1154 }
1155
1156 Ok(())
1157}
1158
1159fn register_native_host_functions(
1171 webview_id: servo::WebViewId,
1172 stealth_profile: Option<bao_stealth::StealthProfile>,
1173) -> bool {
1174 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1175 Box::new(move |cx_ptr, global_ptr| {
1176 unsafe {
1178 install_all_native(webview_id, cx_ptr, global_ptr, &stealth_profile);
1179 }
1180 });
1181
1182 servo::register_script_thread_callback(webview_id, callback);
1183 true
1184}
1185
1186unsafe fn install_all_native(
1209 webview_id: servo::WebViewId,
1210 cx_ptr: *mut std::ffi::c_void,
1211 global_ptr: *mut std::ffi::c_void,
1212 stealth_profile: &Option<bao_stealth::StealthProfile>,
1213) {
1214 use mozjs::context::JSContext;
1215 use mozjs::jsapi::{JSContext as RawJSContext, JSObject};
1216 use std::ptr::NonNull;
1217
1218 let raw_cx = cx_ptr as *mut RawJSContext;
1219 let raw_global = global_ptr as *mut JSObject;
1220
1221 if raw_cx.is_null() || raw_global.is_null() {
1222 return;
1223 }
1224
1225 PER_THREAD_PAGE_GLOBAL.with(|cell| {
1230 *cell.borrow_mut() = raw_global;
1231 });
1232
1233 page_global_by_webview().insert(webview_id, raw_global as usize);
1237
1238 if let Some(profile) = stealth_profile {
1240 bao_stealth::engine_props::set_profile_for_global(raw_global as usize, profile);
1241 bao_stealth::engine_props::set_profile(profile);
1242 bun_runtime::fetch_api::set_fetch_stealth_profile(Some(profile.clone()));
1243 servo::set_canvas_noise_seed(
1245 bao_stealth::engine_props::canvas_seed(),
1246 bao_stealth::engine_props::canvas_amplitude(),
1247 );
1248 let stc = bao_stealth::StealthTlsWireConfig::from_profile(profile);
1255 servo::set_stealth_tls_config(Some(servo::StealthTlsWireConfig {
1256 tls12_cipher_suites: stc.tls12_cipher_suites,
1257 tls13_cipher_suites: stc.tls13_cipher_suites,
1258 signature_algorithms: stc.signature_algorithms,
1259 supported_groups: stc.supported_groups,
1260 alpn_protocols: stc.alpn_protocols,
1261 h2_settings_payload: stc.h2_settings_payload,
1262 h2_initial_stream_size: stc.h2_initial_stream_size,
1263 h2_initial_connection_window_size: stc.h2_initial_connection_window_size,
1264 h2_max_frame_size: stc.h2_max_frame_size,
1265 h2_max_header_list_size: stc.h2_max_header_list_size,
1266 }));
1267 bao_stealth::set_global_http2_fingerprint(Some(&profile.http2));
1272 } else {
1273 bun_runtime::fetch_api::set_fetch_stealth_profile(None);
1274 servo::set_stealth_tls_config(None);
1275 bao_stealth::set_global_http2_fingerprint(None);
1276 }
1277
1278
1279 bao_stealth::engine_props::install_stealth_props(raw_cx, raw_global);
1281
1282 let cx_nn = match NonNull::new(raw_cx) {
1284 Some(nn) => nn,
1285 None => return,
1286 };
1287 let mut cx = JSContext::from_ptr(cx_nn);
1288 rooted!(in(raw_cx) let mut rooted_global = raw_global);
1289 let global_handle = rooted_global.handle();
1290
1291 bun_runtime::fetch_api::install_fetch_global(&mut cx, global_handle);
1293 bun_runtime::timers::install_timer_globals(&mut cx, global_handle);
1294 bun_runtime::web_api::install_performance(&mut cx, global_handle);
1295 bun_runtime::web_api::install_websocket_constructor(&mut cx, global_handle);
1296 bun_runtime::globals::install_crypto_global(&mut cx, global_handle);
1297 bun_runtime::web_api::install_web_encodings(&mut cx, global_handle);
1298 bun_runtime::web_api::install_atob_btoa(&mut cx, global_handle);
1299 bun_runtime::web_api::install_queue_microtask(&mut cx, global_handle);
1300 bun_runtime::globals::install_structured_clone(&mut cx, global_handle);
1301 bun_runtime::globals::install_web_api_constructors(&mut cx, global_handle);
1302 bun_runtime::web_fetch_classes::install_fetch_classes(&mut cx, global_handle);
1307
1308 {
1314 use mozjs::rust::CompileOptionsWrapper;
1315 let wasm_check = r#"(function(){ try { return typeof WebAssembly; } catch(e) { return 'undefined'; } })()"#;
1316 let c_filename = c"<wasm-init>".to_owned();
1317 let mut options = CompileOptionsWrapper::new(&mut cx, c_filename, 1);
1318 options.set_hide_script_from_debugger(true);
1321 rooted!(&in(cx) let mut wasm_rval = mozjs::jsval::UndefinedValue());
1322 let wasm_probe = mozjs::rust::evaluate_script(
1323 &mut cx,
1324 global_handle,
1325 wasm_check,
1326 wasm_rval.handle_mut(),
1327 options,
1328 );
1329 if wasm_probe.is_err() {
1343 mozjs::jsapi::JS_ClearPendingException(cx.raw_cx());
1344 }
1345 }
1346}
1347
1348pub fn inject_all(page: &PageHandle, stealth: bool) -> Result<(), BrowserError> {
1350 let profile = if stealth {
1351 page.stealth_profile()
1352 } else {
1353 None
1354 };
1355 inject_node_apis_with_stealth(page, profile)
1356}
1357
1358pub fn inject_all_with_profile(
1375 page: &PageHandle,
1376 profile: &Option<bao_stealth::StealthProfile>,
1377) -> Result<(), BrowserError> {
1378 inject_node_apis_with_stealth(page, profile.clone())?;
1379
1380 register_worker_scope_callback_native(profile.clone());
1387
1388 Ok(())
1389}
1390
1391pub fn register_worker_scope_callback_native(profile: Option<bao_stealth::StealthProfile>) {
1418 let config = match profile.as_ref() {
1423 Some(p) => crate::delegate::WorkerScopeConfig::from(p),
1424 None => crate::delegate::WorkerScopeConfig::default(),
1425 };
1426
1427 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1428 Box::new(move |cx_ptr, global_ptr| {
1429 let raw_cx = cx_ptr as *mut mozjs::jsapi::JSContext;
1433 let raw_global = global_ptr as *mut mozjs::jsapi::JSObject;
1434 if raw_cx.is_null() || raw_global.is_null() {
1435 log::warn!(
1436 "[register_worker_scope_callback_native] NULL cx/global — \
1437 skipping Worker scope init (DEC-WK-001)"
1438 );
1439 return;
1440 }
1441 eprintln!(
1443 "[TASK-63-DIAG] worker scope callback FIRED (worker thread alive, scope created)"
1444 );
1445 log::debug!(
1446 "[register_worker_scope_callback_native] servo-native Worker \
1447 scope created — installing bao stealth + Web APIs (DEC-WK-001 / \
1448 DEC-WK-003 dual-track)"
1449 );
1450 unsafe {
1451 worker_scope_init_native(raw_cx, raw_global, &config);
1452 }
1453 });
1454
1455 servo::register_worker_scope_callback(callback);
1456}
1457
1458pub type WorkerScopeInitFn =
1482 Box<dyn FnOnce(*mut mozjs::jsapi::JSContext, *mut mozjs::jsapi::JSObject) + Send>;
1483
1484unsafe fn worker_scope_init_native(
1497 raw_cx: *mut mozjs::jsapi::JSContext,
1498 global: *mut mozjs::jsapi::JSObject,
1499 config: &crate::delegate::WorkerScopeConfig,
1500) {
1501 use mozjs::context::JSContext;
1502 use std::ptr::NonNull;
1503
1504 if raw_cx.is_null() || global.is_null() {
1505 return;
1506 }
1507
1508 if let Some(ref profile) = config.stealth_profile {
1516 bao_stealth::engine_props::set_profile_for_global(global as usize, profile);
1518 bao_stealth::engine_props::install_stealth_props(raw_cx, global);
1519 servo::set_canvas_noise_seed(
1521 bao_stealth::engine_props::canvas_seed(),
1522 bao_stealth::engine_props::canvas_amplitude(),
1523 );
1524 }
1525
1526 }
1531
1532const WEB_POLYFILLS: &str = r#"(function() {
1540 // @trace REQ-SEC-003 Web-only polyfills (no Node.js APIs)
1541
1542 // TextEncoder / TextDecoder
1543 if (typeof TextEncoder === 'undefined') {
1544 TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
1545 }
1546 if (typeof TextDecoder === 'undefined') {
1547 TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
1548 }
1549
1550 // URL / URLSearchParams
1551 if (typeof URL === 'undefined') {
1552 URL = function(url, base) { throw new Error('URL not available'); };
1553 }
1554 if (typeof URLSearchParams === 'undefined') {
1555 URLSearchParams = function(init) {
1556 this._params = [];
1557 this.append = function(k,v) { this._params.push([k,v]); };
1558 this.get = function(k) { for(var i=0;i<this._params.length;i++) if(this._params[i][0]===k) return this._params[i][1]; return null; };
1559 this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
1560 };
1561 }
1562
1563 // btoa / atob
1564 if (typeof btoa === 'undefined') {
1565 var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
1566 btoa = function(str) {
1567 var out = '';
1568 for (var i = 0; i < str.length; i += 3) {
1569 var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
1570 out += _b64chars[a>>2] + _b64chars[((a&3)<<4)|(b>>4)] + (isNaN(b)?'=':_b64chars[((b&15)<<2)|(c>>6)]) + (isNaN(b)||isNaN(c)?'=':_b64chars[c&63]);
1571 }
1572 return out;
1573 };
1574 atob = function(str) {
1575 var out = '';
1576 str = str.replace(/=+$/, '');
1577 for (var i = 0; i < str.length; i += 4) {
1578 var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
1579 var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
1580 out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
1581 }
1582 return out;
1583 };
1584 }
1585
1586 // setImmediate / clearImmediate (Web API extensions used by many libraries)
1587 if (typeof setImmediate === 'undefined') {
1588 setImmediate = function(fn) {
1589 var args = Array.prototype.slice.call(arguments, 1);
1590 return setTimeout(function() { fn.apply(null, args); }, 0);
1591 };
1592 clearImmediate = function(id) { clearTimeout(id); };
1593 }
1594})();"#;
1595
1596const NODE_POLYFILLS: &str = r#"(function() {
1597 // @trace REQ-ENG-007 Node.js API polyfills for Node Realm context
1598
1599 // global alias
1600 if (typeof global === 'undefined') {
1601 global = globalThis;
1602 }
1603
1604 // process
1605 if (typeof process === 'undefined') {
1606 process = {
1607 argv: ['bao', typeof __filename !== 'undefined' ? __filename : ''],
1608 argv0: 'bao',
1609 execArgv: [],
1610 execPath: '/usr/local/bin/bao',
1611 env: (function() {
1612 var e = {};
1613 if (typeof navigator !== 'undefined' && navigator.userAgent) {
1614 e.NODE_VERSION = '20.11.0';
1615 e.BAO_VERSION = '0.1.0';
1616 }
1617 e.HOME = '/';
1618 e.PATH = '/usr/local/bin:/usr/bin:/bin';
1619 e.TERM = 'xterm-256color';
1620 return e;
1621 })(),
1622 version: 'v20.11.0',
1623 versions: {
1624 node: '20.11.0',
1625 v8: '12.4.254.14',
1626 uv: '1.27.0',
1627 zlib: '1.2.13',
1628 brotli: '1.0.9',
1629 ares: '1.19.1',
1630 modules: '115',
1631 openssl: '3.0.12',
1632 icu: '74.2',
1633 bun: '1.0.25',
1634 bao: '0.1.0',
1635 },
1636 pid: 1,
1637 ppid: 0,
1638 title: 'bao',
1639 arch: (function() {
1640 if (typeof navigator !== 'undefined') {
1641 var p = navigator.platform || '';
1642 if (p.indexOf('Win') >= 0) return 'x64';
1643 if (p.indexOf('Mac') >= 0) return 'arm64';
1644 if (p.indexOf('Linux') >= 0) return 'x64';
1645 }
1646 return 'x64';
1647 })(),
1648 platform: (function() {
1649 if (typeof navigator !== 'undefined') {
1650 var p = navigator.platform || '';
1651 if (p.indexOf('Win') >= 0) return 'win32';
1652 if (p.indexOf('Mac') >= 0) return 'darwin';
1653 }
1654 return 'linux';
1655 })(),
1656 cwd: function() { return '/'; },
1657 chdir: function() {},
1658 exit: function(code) { throw new Error('process.exit(' + (code||0) + ')'); },
1659 hrtime: (function() {
1660 var origin = performance.now() * 1e-3;
1661 return function bigtime() {
1662 var diff = performance.now() * 1e-3 - origin;
1663 var sec = Math.floor(diff);
1664 var nsec = Math.round((diff - sec) * 1e9);
1665 if (arguments.length > 0) {
1666 sec += arguments[0][0];
1667 nsec += arguments[0][1];
1668 sec += Math.floor(nsec / 1e9);
1669 nsec = nsec % 1e9;
1670 if (nsec < 0) { nsec += 1e9; sec -= 1; }
1671 }
1672 var result = [sec, nsec];
1673 result.bigint = function() { return BigInt(sec) * 1000000000n + BigInt(nsec); };
1674 return result;
1675 };
1676 })(),
1677 uptime: function() { return performance.now() / 1000; },
1678 memoryUsage: function() {
1679 return { rss: 64*1024*1024, heapTotal: 32*1024*1024, heapUsed: 16*1024*1024, external: 2*1024*1024, arrayBuffers: 1*1024*1024 };
1680 },
1681 cpuUsage: function() { return { user: 100000, system: 50000 }; },
1682 nextTick: function(fn) {
1683 var args = Array.prototype.slice.call(arguments, 1);
1684 Promise.resolve().then(function() { fn.apply(null, args); });
1685 },
1686 binding: function(name) { return {}; },
1687 dlopen: function() { throw new Error('process.dlopen not available in browser context'); },
1688 stdout: { write: function(d) { console.log(d); return true; }, end: function() {} },
1689 stderr: { write: function(d) { console.error(d); return true; }, end: function() {} },
1690 stdin: { on: function() {}, resume: function() { return this; }, pipe: function() {} },
1691 on: function(event, fn) { return this; },
1692 off: function() {},
1693 once: function(event, fn) { return this; },
1694 emit: function(event) { return false; },
1695 removeAllListeners: function() { return this; },
1696 setUncaughtExceptionCallback: function() {},
1697 };
1698 }
1699
1700 // Buffer — browser-compatible implementation backed by Uint8Array
1701 if (typeof Buffer === 'undefined') {
1702 Buffer = (function() {
1703 function B(data, encoding) {
1704 if (!(this instanceof B)) return new B(data, encoding);
1705 if (data instanceof Uint8Array) {
1706 this._buf = new Uint8Array(data);
1707 } else if (data instanceof ArrayBuffer) {
1708 this._buf = new Uint8Array(data);
1709 } else if (typeof data === 'string') {
1710 this._buf = new Uint8Array(Array.from(data).map(function(c) { return c.charCodeAt(0); }));
1711 } else if (Array.isArray(data)) {
1712 this._buf = new Uint8Array(data);
1713 } else {
1714 this._buf = new Uint8Array(0);
1715 }
1716 this.length = this._buf.length;
1717 }
1718
1719 B.isBuffer = function(obj) { return obj instanceof B; };
1720
1721 B.from = function(data, encoding) {
1722 if (data instanceof B) return new B(data._buf);
1723 if (data instanceof Uint8Array) return new B(data);
1724 if (data instanceof ArrayBuffer) return new B(data);
1725 if (typeof data === 'string') {
1726 if (encoding === 'hex') {
1727 var bytes = [];
1728 for (var i = 0; i < data.length; i += 2) {
1729 bytes.push(parseInt(data.substr(i, 2), 16));
1730 }
1731 return new B(bytes);
1732 }
1733 if (encoding === 'base64') {
1734 var bin = atob(data);
1735 var bytes = [];
1736 for (var i = 0; i < bin.length; i++) bytes.push(bin.charCodeAt(i));
1737 return new B(bytes);
1738 }
1739 return new B(data);
1740 }
1741 return new B(data);
1742 };
1743
1744 B.alloc = function(size, fill, encoding) {
1745 var buf = new B(new Uint8Array(size));
1746 if (fill !== undefined) buf.fill(fill);
1747 return buf;
1748 };
1749
1750 B.allocUnsafe = function(size) {
1751 return new B(new Uint8Array(size));
1752 };
1753
1754 B.allocUnsafeSlow = function(size) {
1755 return new B(new Uint8Array(size));
1756 };
1757
1758 B.concat = function(list, totalLength) {
1759 if (!Array.isArray(list) || list.length === 0) return new B(new Uint8Array(0));
1760 var len = totalLength !== undefined ? totalLength : list.reduce(function(a, b) { return a + b.length; }, 0);
1761 var result = new Uint8Array(len);
1762 var offset = 0;
1763 for (var i = 0; i < list.length; i++) {
1764 var buf = list[i] instanceof B ? list[i]._buf : new Uint8Array(list[i]);
1765 result.set(buf, offset);
1766 offset += buf.length;
1767 }
1768 return new B(result);
1769 };
1770
1771 B.byteLength = function(str, encoding) {
1772 if (typeof str === 'string') {
1773 if (encoding === 'base64') return atob(str).length;
1774 if (encoding === 'hex') return str.length / 2;
1775 return new TextEncoder().encode(str).length;
1776 }
1777 if (str instanceof ArrayBuffer) return str.byteLength;
1778 if (str instanceof Uint8Array) return str.length;
1779 return 0;
1780 };
1781
1782 B.compare = function(a, b) {
1783 for (var i = 0; i < Math.min(a.length, b.length); i++) {
1784 if (a._buf[i] < b._buf[i]) return -1;
1785 if (a._buf[i] > b._buf[i]) return 1;
1786 }
1787 return a.length - b.length;
1788 };
1789
1790 B.prototype.slice = function(start, end) {
1791 return new B(this._buf.slice(start || 0, end));
1792 };
1793
1794 B.prototype.subarray = function(start, end) {
1795 return new B(this._buf.subarray(start || 0, end));
1796 };
1797
1798 B.prototype.toString = function(encoding, start, end) {
1799 var s = start || 0;
1800 var e = end !== undefined ? end : this._buf.length;
1801 var slice = this._buf.slice(s, e);
1802 if (encoding === 'hex') {
1803 return Array.from(slice).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
1804 }
1805 if (encoding === 'base64') {
1806 var bin = Array.from(slice).map(function(b) { return String.fromCharCode(b); }).join('');
1807 return btoa(bin);
1808 }
1809 return new TextDecoder().decode(slice);
1810 };
1811
1812 B.prototype.toJSON = function() {
1813 return { type: 'Buffer', data: Array.from(this._buf) };
1814 };
1815
1816 B.prototype.equals = function(other) {
1817 if (!(other instanceof B) || this.length !== other.length) return false;
1818 for (var i = 0; i < this.length; i++) {
1819 if (this._buf[i] !== other._buf[i]) return false;
1820 }
1821 return true;
1822 };
1823
1824 B.prototype.compare = function(other, targetStart, targetEnd, sourceStart, sourceEnd) {
1825 var a = this._buf.slice(sourceStart || 0, sourceEnd);
1826 var b = other._buf.slice(targetStart || 0, targetEnd);
1827 for (var i = 0; i < Math.min(a.length, b.length); i++) {
1828 if (a[i] < b[i]) return -1;
1829 if (a[i] > b[i]) return 1;
1830 }
1831 return a.length - b.length;
1832 };
1833
1834 B.prototype.copy = function(target, targetStart, sourceStart, sourceEnd) {
1835 var src = this._buf.slice(sourceStart || 0, sourceEnd);
1836 for (var i = 0; i < src.length; i++) {
1837 if (target._buf) target._buf[targetStart + i] = src[i];
1838 }
1839 return src.length;
1840 };
1841
1842 B.prototype.fill = function(value, start, end) {
1843 var s = start || 0;
1844 var e = end !== undefined ? end : this._buf.length;
1845 var v = typeof value === 'number' ? value : 0;
1846 for (var i = s; i < e; i++) this._buf[i] = v;
1847 return this;
1848 };
1849
1850 B.prototype.write = function(str, offset, length, encoding) {
1851 var o = offset || 0;
1852 var bytes = new TextEncoder().encode(str);
1853 var len = Math.min(bytes.length, length !== undefined ? length : this._buf.length - o);
1854 for (var i = 0; i < len; i++) this._buf[o + i] = bytes[i];
1855 return len;
1856 };
1857
1858 B.prototype.includes = function(value, offset) {
1859 return this.indexOf(value, offset) !== -1;
1860 };
1861
1862 B.prototype.indexOf = function(value, offset) {
1863 var o = offset || 0;
1864 var search = typeof value === 'number' ? [value] : Array.from(new TextEncoder().encode(String(value)));
1865 for (var i = o; i <= this._buf.length - search.length; i++) {
1866 var found = true;
1867 for (var j = 0; j < search.length; j++) {
1868 if (this._buf[i + j] !== search[j]) { found = false; break; }
1869 }
1870 if (found) return i;
1871 }
1872 return -1;
1873 };
1874
1875 B.prototype.readUInt8 = function(offset) { return this._buf[offset || 0]; };
1876 B.prototype.readUInt16LE = function(offset) { var o = offset||0; return this._buf[o] | (this._buf[o+1]<<8); };
1877 B.prototype.readUInt16BE = function(offset) { var o = offset||0; return (this._buf[o]<<8) | this._buf[o+1]; };
1878 B.prototype.readUInt32LE = function(offset) {
1879 var o = offset||0;
1880 return (this._buf[o]) | (this._buf[o+1]<<8) | (this._buf[o+2]<<16) | (this._buf[o+3]<<24);
1881 };
1882 B.prototype.readInt8 = function(offset) { var v = this._buf[offset||0]; return v > 127 ? v - 256 : v; };
1883 B.prototype.readInt16LE = function(offset) { var v = this.readUInt16LE(offset); return v > 32767 ? v - 65536 : v; };
1884 B.prototype.readInt32LE = function(offset) { var v = this.readUInt32LE(offset); return v > 2147483647 ? v - 4294967296 : v; };
1885 B.prototype.readFloatLE = function(offset) {
1886 var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = 0;
1887 new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+4));
1888 return new Float32Array(buf)[0];
1889 };
1890 B.prototype.readDoubleLE = function(offset) {
1891 var buf = new ArrayBuffer(8);
1892 new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+8));
1893 return new Float64Array(buf)[0];
1894 };
1895
1896 B.prototype.writeUInt8 = function(v, offset) { this._buf[offset||0] = v & 0xFF; return (offset||0)+1; };
1897 B.prototype.writeUInt16LE = function(v, offset) { var o = offset||0; this._buf[o]=v&0xFF; this._buf[o+1]=(v>>8)&0xFF; return o+2; };
1898 B.prototype.writeUInt32LE = function(v, offset) { var o = offset||0; this._buf[o]=v&0xFF; this._buf[o+1]=(v>>8)&0xFF; this._buf[o+2]=(v>>16)&0xFF; this._buf[o+3]=(v>>24)&0xFF; return o+4; };
1899 B.prototype.writeInt8 = function(v, offset) { return this.writeUInt8(v < 0 ? v + 256 : v, offset); };
1900 B.prototype.writeInt16LE = function(v, offset) { return this.writeUInt16LE(v < 0 ? v + 65536 : v, offset); };
1901 B.prototype.writeInt32LE = function(v, offset) { return this.writeUInt32LE(v < 0 ? v + 4294967296 : v, offset); };
1902 B.prototype.writeFloatLE = function(v, offset) {
1903 var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = v;
1904 this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+4;
1905 };
1906 B.prototype.writeDoubleLE = function(v, offset) {
1907 var buf = new ArrayBuffer(8); new Float64Array(buf)[0] = v;
1908 this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+8;
1909 };
1910
1911 B.prototype[Symbol.iterator] = function() {
1912 var idx = 0; var buf = this._buf;
1913 return { next: function() { return idx < buf.length ? { value: buf[idx++], done: false } : { done: true }; } };
1914 };
1915
1916 return B;
1917 })();
1918 }
1919
1920 // require — basic module loader for browser context
1921 if (typeof require === 'undefined') {
1922 var _module_cache = {};
1923 var _module_builtin = {
1924 'fs': { readFileSync: function() { throw new Error('fs not available in browser context'); }, existsSync: function() { return false; } },
1925 'path': {
1926 join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1927 resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1928 dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1929 basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1930 extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1931 sep: '/', delimiter: ':',
1932 posix: {
1933 join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1934 resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1935 dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1936 basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1937 extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1938 sep: '/', delimiter: ':',
1939 },
1940 win32: { sep: '\\', delimiter: ';' },
1941 },
1942 'url': {
1943 parse: function(u) { try { var p = new URL(u); return { href: p.href, protocol: p.protocol, host: p.host, hostname: p.hostname, pathname: p.pathname, search: p.search, hash: p.hash }; } catch(e) { return {}; } },
1944 format: function(u) { return typeof u === 'string' ? u : (u.protocol||'http:') + '//' + (u.host||u.hostname||'localhost') + (u.pathname||'/'); },
1945 resolve: function(from, to) { try { return new URL(to, from).href; } catch(e) { return to; } },
1946 URL: typeof URL !== 'undefined' ? URL : function() {},
1947 URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : function() {},
1948 },
1949 'querystring': {
1950 parse: function(str, sep, eq) {
1951 sep = sep || '&'; eq = eq || '=';
1952 var obj = {};
1953 if (!str) return obj;
1954 str.split(sep).forEach(function(pair) {
1955 var idx = pair.indexOf(eq);
1956 var key = idx >= 0 ? pair.substring(0, idx) : pair;
1957 var val = idx >= 0 ? pair.substring(idx + 1) : '';
1958 obj[decodeURIComponent(key)] = decodeURIComponent(val);
1959 });
1960 return obj;
1961 },
1962 stringify: function(obj, sep, eq) {
1963 sep = sep || '&'; eq = eq || '=';
1964 return Object.keys(obj || {}).map(function(k) {
1965 return encodeURIComponent(k) + eq + encodeURIComponent(obj[k]);
1966 }).join(sep);
1967 },
1968 escape: encodeURIComponent,
1969 unescape: decodeURIComponent,
1970 },
1971 'events': {
1972 EventEmitter: (function() {
1973 function EE() { this._events = {}; }
1974 EE.prototype.on = function(e, fn) { (this._events[e] = this._events[e] || []).push(fn); return this; };
1975 EE.prototype.once = function(e, fn) { var self = this; function g() { self.off(e, g); fn.apply(this, arguments); } g._orig = fn; this.on(e, g); return this; };
1976 EE.prototype.off = function(e, fn) {
1977 if (!this._events[e]) return this;
1978 if (!fn) { delete this._events[e]; return this; }
1979 this._events[e] = this._events[e].filter(function(f) { return f !== fn && f._orig !== fn; });
1980 return this;
1981 };
1982 EE.prototype.emit = function(e) {
1983 var args = Array.prototype.slice.call(arguments, 1);
1984 (this._events[e] || []).forEach(function(fn) { fn.apply(null, args); });
1985 return this;
1986 };
1987 EE.prototype.removeListener = EE.prototype.off;
1988 EE.prototype.removeAllListeners = function(e) { if (e) delete this._events[e]; else this._events = {}; return this; };
1989 EE.prototype.listeners = function(e) { return this._events[e] || []; };
1990 EE.prototype.listenerCount = function(e) { return (this._events[e] || []).length; };
1991 return EE;
1992 })(),
1993 },
1994 'util': {
1995 inspect: function(obj) { return JSON.stringify(obj, null, 2); },
1996 inherits: function(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype); ctor.prototype.constructor = ctor; },
1997 isFunction: function(v) { return typeof v === 'function'; },
1998 isNull: function(v) { return v === null; },
1999 isUndefined: function(v) { return v === undefined; },
2000 isObject: function(v) { return v !== null && typeof v === 'object'; },
2001 isString: function(v) { return typeof v === 'string'; },
2002 promisify: function(fn) {
2003 return function() {
2004 var args = Array.prototype.slice.call(arguments);
2005 return new Promise(function(resolve, reject) {
2006 args.push(function(err, result) { if (err) reject(err); else resolve(result); });
2007 fn.apply(null, args);
2008 });
2009 };
2010 },
2011 format: function(fmt) {
2012 var args = Array.prototype.slice.call(arguments, 1);
2013 return fmt.replace(/%[sdjifo]/g, function(m) { return args.length ? String(args.shift()) : m; });
2014 },
2015 types: {
2016 isDate: function(v) { return v instanceof Date; },
2017 isRegExp: function(v) { return v instanceof RegExp; },
2018 isArray: function(v) { return Array.isArray(v); },
2019 isPromise: function(v) { return v && typeof v.then === 'function'; },
2020 },
2021 },
2022 'stream': { Readable: function(){}, Writable: function(){}, Duplex: function(){}, Transform: function(){} },
2023 'buffer': { Buffer: typeof Buffer !== 'undefined' ? Buffer : function(){} },
2024 'crypto': {
2025 randomBytes: function(size, cb) {
2026 var arr = new Uint8Array(size);
2027 if (typeof crypto !== 'undefined' && crypto.getRandomValues) crypto.getRandomValues(arr);
2028 if (cb) cb(null, Buffer.from(arr));
2029 return Buffer.from(arr);
2030 },
2031 createHash: function(algo) {
2032 var chunks = [];
2033 return {
2034 update: function(data) { chunks.push(typeof data === 'string' ? data : String(data)); return this; },
2035 digest: function(enc) {
2036 var str = chunks.join('');
2037 if (typeof crypto !== 'undefined' && crypto.subtle) {
2038 return crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)).then(function(buf) {
2039 var arr = new Uint8Array(buf); return enc === 'hex' ? Array.from(arr).map(function(b){return b.toString(16).padStart(2,'0');}).join('') : Buffer.from(arr);
2040 });
2041 }
2042 return enc === 'hex' ? '00000000' : Buffer.alloc(0);
2043 },
2044 };
2045 },
2046 },
2047 'os': {
2048 platform: function() { return 'linux'; },
2049 arch: function() { return 'x64'; },
2050 homedir: function() { return '/'; },
2051 tmpdir: function() { return '/tmp'; },
2052 type: function() { return 'Linux'; },
2053 release: function() { return '6.8.0'; },
2054 hostname: function() { return 'bao'; },
2055 cpus: function() { return [{ model: 'bao', speed: 3000 }]; },
2056 totalmem: function() { return 8*1024*1024*1024; },
2057 freemem: function() { return 4*1024*1024*1024; },
2058 uptime: function() { return 3600; },
2059 EOL: '\n',
2060 },
2061 'assert': {
2062 ok: function(v, msg) { if (!v) throw new Error(msg || 'assertion failed'); },
2063 equal: function(a, b, msg) { if (a !== b) throw new Error(msg || a + ' !== ' + b); },
2064 deepEqual: function(a, b, msg) { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'not deep equal'); },
2065 throws: function(fn, msg) { try { fn(); throw new Error(msg || 'expected throw'); } catch(e) { if (e.message === (msg || 'expected throw')) throw e; } },
2066 },
2067 'timers': {
2068 setTimeout: typeof setTimeout !== 'undefined' ? setTimeout : function(fn) { fn(); return 0; },
2069 setInterval: typeof setInterval !== 'undefined' ? setInterval : function(fn) { return 0; },
2070 clearTimeout: typeof clearTimeout !== 'undefined' ? clearTimeout : function() {},
2071 clearInterval: typeof clearInterval !== 'undefined' ? clearInterval : function() {},
2072 setImmediate: typeof setImmediate !== 'undefined' ? setImmediate : function(fn) { return setTimeout(fn, 0); },
2073 clearImmediate: typeof clearImmediate !== 'undefined' ? clearImmediate : function() {},
2074 },
2075 };
2076
2077 require = function(name) {
2078 if (_module_cache[name]) return _module_cache[name];
2079 if (_module_builtin[name]) { _module_cache[name] = _module_builtin[name]; return _module_builtin[name]; }
2080 throw new Error("Cannot find module '" + name + "' in browser context");
2081 };
2082
2083 require.resolve = function(name) { return name; };
2084 require.cache = _module_cache;
2085 }
2086
2087 // setImmediate / clearImmediate
2088 if (typeof setImmediate === 'undefined') {
2089 setImmediate = function(fn) {
2090 var args = Array.prototype.slice.call(arguments, 1);
2091 return setTimeout(function() { fn.apply(null, args); }, 0);
2092 };
2093 clearImmediate = function(id) { clearTimeout(id); };
2094 }
2095
2096 // __dirname / __filename
2097 if (typeof __dirname === 'undefined') {
2098 __dirname = '/';
2099 __filename = '/index.js';
2100 }
2101
2102 // TextEncoder / TextDecoder (most browsers have these, but ensure)
2103 if (typeof TextEncoder === 'undefined') {
2104 TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
2105 }
2106 if (typeof TextDecoder === 'undefined') {
2107 TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
2108 }
2109
2110 // URL / URLSearchParams (most browsers have these, but ensure)
2111 if (typeof URL === 'undefined') {
2112 URL = function(url, base) { throw new Error('URL not available'); };
2113 }
2114 if (typeof URLSearchParams === 'undefined') {
2115 URLSearchParams = function(init) {
2116 this._params = [];
2117 this.append = function(k,v) { this._params.push([k,v]); };
2118 this.get = function(k) { for(var i=0;i<this._params.length;i++) if(this._params[i][0]===k) return this._params[i][1]; return null; };
2119 this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
2120 };
2121 }
2122
2123 // btoa / atob (most browsers have these, but ensure)
2124 if (typeof btoa === 'undefined') {
2125 var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
2126 btoa = function(str) {
2127 var out = '';
2128 for (var i = 0; i < str.length; i += 3) {
2129 var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
2130 out += _b64chars[a>>2] + _b64chars[((a&3)<<4)|(b>>4)] + (isNaN(b)?'=':_b64chars[((b&15)<<2)|(c>>6)]) + (isNaN(b)||isNaN(c)?'=':_b64chars[c&63]);
2131 }
2132 return out;
2133 };
2134 atob = function(str) {
2135 var out = '';
2136 str = str.replace(/=+$/, '');
2137 for (var i = 0; i < str.length; i += 4) {
2138 var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
2139 var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
2140 out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
2141 }
2142 return out;
2143 };
2144 }
2145})();"#;
2146
2147#[derive(Debug, Clone, PartialEq, Eq)]
2157pub enum BridgeCommand {
2158 Navigate(String),
2160 Evaluate(String),
2162 Screenshot,
2164 Close,
2166 Resize(u32, u32),
2168 GetTitle,
2170 GetUrl,
2172}
2173
2174#[derive(Debug, Clone, PartialEq, Eq)]
2178pub enum BridgeResponse {
2179 Ok,
2181 Err(String),
2183 Null,
2185 Value(String),
2187 Binary(Vec<u8>),
2189}
2190
2191impl BridgeResponse {
2192 pub fn is_ok(&self) -> bool {
2194 matches!(self, BridgeResponse::Ok)
2195 }
2196
2197 pub fn is_err(&self) -> bool {
2199 matches!(self, BridgeResponse::Err(_))
2200 }
2201
2202 pub fn ok(self) -> Result<Self, String> {
2205 match self {
2206 BridgeResponse::Err(e) => Err(e),
2207 other => Ok(other),
2208 }
2209 }
2210}
2211
2212pub struct BridgeReceiver {
2218 rx: mpsc::Receiver<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2219 alive: Arc<AtomicBool>,
2220}
2221
2222impl std::fmt::Debug for BridgeReceiver {
2223 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2224 f.debug_struct("BridgeReceiver")
2225 .field("alive", &self.alive)
2226 .finish()
2227 }
2228}
2229
2230impl BridgeReceiver {
2231 pub fn recv(&self) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2233 self.rx.recv().map_err(|_| "channel closed".to_string())
2234 }
2235
2236 pub fn recv_timeout(
2238 &self,
2239 timeout: Duration,
2240 ) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2241 self.rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2242 }
2243
2244 pub fn is_alive(&self) -> bool {
2246 self.alive.load(Ordering::SeqCst)
2247 }
2248}
2249
2250#[derive(Debug)]
2257pub struct BridgeChannel {
2258 tx: mpsc::Sender<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2259 alive: Arc<AtomicBool>,
2260}
2261
2262impl BridgeChannel {
2263 pub fn new() -> (Self, BridgeReceiver) {
2268 let (tx, rx) = mpsc::channel();
2269 let alive = Arc::new(AtomicBool::new(true));
2270 let channel = BridgeChannel {
2271 tx,
2272 alive: alive.clone(),
2273 };
2274 let receiver = BridgeReceiver { rx, alive };
2275 (channel, receiver)
2276 }
2277
2278 pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2280 let (resp_tx, resp_rx) = mpsc::channel();
2281 self.tx
2282 .send((cmd, Some(resp_tx)))
2283 .map_err(|_| "bridge closed".to_string())?;
2284 resp_rx
2285 .recv()
2286 .map_err(|_| "response channel closed".to_string())
2287 }
2288
2289 pub fn send_timeout(
2291 &self,
2292 cmd: BridgeCommand,
2293 timeout: Duration,
2294 ) -> Result<BridgeResponse, String> {
2295 let (resp_tx, resp_rx) = mpsc::channel();
2296 self.tx
2297 .send((cmd, Some(resp_tx)))
2298 .map_err(|_| "bridge closed".to_string())?;
2299 resp_rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2300 }
2301
2302 pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2307 self.tx
2308 .send((cmd, None))
2309 .map_err(|_| "bridge closed".to_string())
2310 }
2311
2312 pub fn is_alive(&self) -> bool {
2314 self.alive.load(Ordering::SeqCst)
2315 }
2316
2317 pub fn close(&self) {
2323 self.alive.store(false, Ordering::SeqCst);
2324 }
2325}
2326
2327#[derive(Debug)]
2336pub struct RuntimeBridge {
2337 channel: BridgeChannel,
2338}
2339
2340impl RuntimeBridge {
2341 pub fn new() -> (Self, BridgeReceiver) {
2343 let (channel, receiver) = BridgeChannel::new();
2344 (RuntimeBridge { channel }, receiver)
2345 }
2346
2347 pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2349 self.channel.send(cmd)
2350 }
2351
2352 pub fn send_timeout(
2355 &self,
2356 cmd: BridgeCommand,
2357 timeout: Duration,
2358 ) -> Result<BridgeResponse, String> {
2359 self.channel.send_timeout(cmd, timeout)
2360 }
2361
2362 pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2365 self.channel.fire_and_forget(cmd)
2366 }
2367
2368 pub fn is_alive(&self) -> bool {
2370 self.channel.is_alive()
2371 }
2372
2373 pub fn close(&self) {
2375 self.channel.close();
2376 }
2377}
2378
2379#[cfg(test)]
2380mod tests {
2381 use std::sync::OnceLock;
2382 #[test]
2386 fn test_polyfills_are_valid_js() {
2387 assert!(!super::NODE_POLYFILLS.is_empty());
2388 assert!(super::NODE_POLYFILLS.contains("Buffer"));
2389 assert!(super::NODE_POLYFILLS.contains("require"));
2390 assert!(super::NODE_POLYFILLS.contains("process"));
2391 }
2392
2393 #[test]
2397 fn bridge_command_navigate_equality() {
2398 let cmd1 = super::BridgeCommand::Navigate("https://example.com".into());
2399 let cmd2 = super::BridgeCommand::Navigate("https://example.com".into());
2400 let cmd3 = super::BridgeCommand::Navigate("https://other.com".into());
2401 assert_eq!(cmd1, cmd2);
2402 assert_ne!(cmd1, cmd3);
2403 }
2404
2405 #[test]
2406 fn bridge_command_evaluate_equality() {
2407 let cmd1 = super::BridgeCommand::Evaluate("1+1".into());
2408 let cmd2 = super::BridgeCommand::Evaluate("1+1".into());
2409 assert_eq!(cmd1, cmd2);
2410 assert_ne!(cmd1, super::BridgeCommand::Evaluate("2+2".into()));
2411 }
2412
2413 #[test]
2414 fn bridge_command_resize_equality() {
2415 assert_eq!(
2416 super::BridgeCommand::Resize(800, 600),
2417 super::BridgeCommand::Resize(800, 600)
2418 );
2419 assert_ne!(
2420 super::BridgeCommand::Resize(800, 600),
2421 super::BridgeCommand::Resize(1024, 768)
2422 );
2423 }
2424
2425 #[test]
2426 fn bridge_command_variants_distinct() {
2427 let cmds = [
2428 super::BridgeCommand::Navigate("x".into()),
2429 super::BridgeCommand::Evaluate("y".into()),
2430 super::BridgeCommand::Screenshot,
2431 super::BridgeCommand::Close,
2432 super::BridgeCommand::Resize(1, 1),
2433 super::BridgeCommand::GetTitle,
2434 super::BridgeCommand::GetUrl,
2435 ];
2436 for i in 0..cmds.len() {
2437 for j in 0..cmds.len() {
2438 if i != j {
2439 assert_ne!(cmds[i], cmds[j]);
2440 }
2441 }
2442 }
2443 }
2444
2445 #[test]
2446 fn bridge_response_ok_is_ok() {
2447 let resp = super::BridgeResponse::Ok;
2448 assert!(resp.is_ok());
2449 assert!(!resp.is_err());
2450 }
2451
2452 #[test]
2453 fn bridge_response_err_is_err() {
2454 let resp = super::BridgeResponse::Err("failed".into());
2455 assert!(!resp.is_ok());
2456 assert!(resp.is_err());
2457 }
2458
2459 #[test]
2460 fn bridge_response_null_not_err() {
2461 let resp = super::BridgeResponse::Null;
2462 assert!(!resp.is_ok()); assert!(!resp.is_err()); }
2465
2466 #[test]
2467 fn bridge_response_value_not_err() {
2468 let resp = super::BridgeResponse::Value("result".into());
2469 assert!(!resp.is_ok()); assert!(!resp.is_err());
2471 }
2472
2473 #[test]
2474 fn bridge_response_binary_not_err() {
2475 let resp = super::BridgeResponse::Binary(vec![1, 2, 3]);
2476 assert!(!resp.is_ok()); assert!(!resp.is_err());
2478 }
2479
2480 #[test]
2481 fn bridge_response_ok_method_wraps_non_err() {
2482 assert!(super::BridgeResponse::Null.ok().is_ok());
2484 assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2485 assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2486 }
2487
2488 #[test]
2489 fn bridge_response_ok_method_on_err() {
2490 let resp = super::BridgeResponse::Err("error msg".into());
2491 let result = resp.ok();
2492 assert!(result.is_err());
2493 assert_eq!(result.unwrap_err(), "error msg");
2494 }
2495
2496 #[test]
2497 fn bridge_response_ok_method_on_ok_variants() {
2498 assert!(super::BridgeResponse::Ok.ok().is_ok());
2499 assert!(super::BridgeResponse::Null.ok().is_ok());
2500 assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2501 assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2502 }
2503
2504 #[test]
2505 fn bridge_channel_new_alive() {
2506 let (channel, _receiver) = super::BridgeChannel::new();
2507 assert!(channel.is_alive());
2508 }
2509
2510 #[test]
2511 fn bridge_channel_close_sets_not_alive() {
2512 let (channel, _receiver) = super::BridgeChannel::new();
2513 channel.close();
2514 assert!(!channel.is_alive());
2515 }
2516
2517 #[test]
2518 fn bridge_receiver_alive_shares_flag() {
2519 let (channel, receiver) = super::BridgeChannel::new();
2520 assert!(receiver.is_alive());
2521 channel.close();
2522 assert!(!receiver.is_alive());
2523 }
2524
2525 #[test]
2526 fn bridge_channel_fire_and_forget() {
2527 let (channel, receiver) = super::BridgeChannel::new();
2528 assert!(channel
2529 .fire_and_forget(super::BridgeCommand::GetTitle)
2530 .is_ok());
2531 let (cmd, responder) = receiver.recv().unwrap();
2532 assert_eq!(cmd, super::BridgeCommand::GetTitle);
2533 assert!(responder.is_none());
2534 }
2535
2536 #[test]
2537 fn bridge_channel_send_with_response() {
2538 let (channel, receiver) = super::BridgeChannel::new();
2539 let worker = std::thread::spawn(move || {
2541 let (_cmd, responder) = receiver.recv().unwrap();
2542 if let Some(resp_tx) = responder {
2543 resp_tx
2544 .send(super::BridgeResponse::Value("title".into()))
2545 .unwrap();
2546 }
2547 });
2548 let result = channel.send(super::BridgeCommand::GetTitle).unwrap();
2549 assert_eq!(result, super::BridgeResponse::Value("title".into()));
2550 worker.join().unwrap();
2551 }
2552
2553 #[test]
2554 fn runtime_bridge_new_alive() {
2555 let (bridge, _receiver) = super::RuntimeBridge::new();
2556 assert!(bridge.is_alive());
2557 }
2558
2559 #[test]
2560 fn runtime_bridge_close() {
2561 let (bridge, _receiver) = super::RuntimeBridge::new();
2562 bridge.close();
2563 assert!(!bridge.is_alive());
2564 }
2565
2566 #[test]
2567 fn runtime_bridge_fire_and_forget() {
2568 let (bridge, receiver) = super::RuntimeBridge::new();
2569 assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2570 let (cmd, responder) = receiver.recv().unwrap();
2571 assert_eq!(cmd, super::BridgeCommand::Close);
2572 assert!(responder.is_none());
2573 }
2574
2575 #[test]
2583 fn bridge_command_debug_format_navigate() {
2584 let cmd = super::BridgeCommand::Navigate("https://example.com".into());
2585 let debug_str = format!("{:?}", cmd);
2586 assert!(debug_str.contains("Navigate"));
2587 assert!(debug_str.contains("https://example.com"));
2588 }
2589
2590 #[test]
2591 fn bridge_command_debug_format_evaluate() {
2592 let cmd = super::BridgeCommand::Evaluate("return 42".into());
2593 let debug_str = format!("{:?}", cmd);
2594 assert!(debug_str.contains("Evaluate"));
2595 assert!(debug_str.contains("return 42"));
2596 }
2597
2598 #[test]
2599 fn bridge_command_debug_format_screenshot() {
2600 let cmd = super::BridgeCommand::Screenshot;
2601 let debug_str = format!("{:?}", cmd);
2602 assert!(debug_str.contains("Screenshot"));
2603 }
2604
2605 #[test]
2606 fn bridge_command_debug_format_close() {
2607 let cmd = super::BridgeCommand::Close;
2608 let debug_str = format!("{:?}", cmd);
2609 assert!(debug_str.contains("Close"));
2610 }
2611
2612 #[test]
2613 fn bridge_command_debug_format_resize() {
2614 let cmd = super::BridgeCommand::Resize(1920, 1080);
2615 let debug_str = format!("{:?}", cmd);
2616 assert!(debug_str.contains("Resize"));
2617 assert!(debug_str.contains("1920"));
2618 assert!(debug_str.contains("1080"));
2619 }
2620
2621 #[test]
2622 fn bridge_command_debug_format_get_title() {
2623 let cmd = super::BridgeCommand::GetTitle;
2624 let debug_str = format!("{:?}", cmd);
2625 assert!(debug_str.contains("GetTitle"));
2626 }
2627
2628 #[test]
2629 fn bridge_command_debug_format_get_url() {
2630 let cmd = super::BridgeCommand::GetUrl;
2631 let debug_str = format!("{:?}", cmd);
2632 assert!(debug_str.contains("GetUrl"));
2633 }
2634
2635 #[test]
2638 fn bridge_command_clone_navigate() {
2639 let cmd = super::BridgeCommand::Navigate("https://test.com".into());
2640 let cloned = cmd.clone();
2641 assert_eq!(cmd, cloned);
2642 }
2643
2644 #[test]
2645 fn bridge_command_clone_evaluate() {
2646 let cmd = super::BridgeCommand::Evaluate("x + y".into());
2647 let cloned = cmd.clone();
2648 assert_eq!(cmd, cloned);
2649 }
2650
2651 #[test]
2652 fn bridge_command_clone_resize() {
2653 let cmd = super::BridgeCommand::Resize(1024, 768);
2654 let cloned = cmd.clone();
2655 assert_eq!(cmd, cloned);
2656 }
2657
2658 #[test]
2661 fn bridge_response_debug_format_ok() {
2662 let resp = super::BridgeResponse::Ok;
2663 let debug_str = format!("{:?}", resp);
2664 assert!(debug_str.contains("Ok"));
2665 }
2666
2667 #[test]
2668 fn bridge_response_debug_format_err() {
2669 let resp = super::BridgeResponse::Err("something went wrong".into());
2670 let debug_str = format!("{:?}", resp);
2671 assert!(debug_str.contains("Err"));
2672 assert!(debug_str.contains("something went wrong"));
2673 }
2674
2675 #[test]
2676 fn bridge_response_debug_format_null() {
2677 let resp = super::BridgeResponse::Null;
2678 let debug_str = format!("{:?}", resp);
2679 assert!(debug_str.contains("Null"));
2680 }
2681
2682 #[test]
2683 fn bridge_response_debug_format_value() {
2684 let resp = super::BridgeResponse::Value("result string".into());
2685 let debug_str = format!("{:?}", resp);
2686 assert!(debug_str.contains("Value"));
2687 assert!(debug_str.contains("result string"));
2688 }
2689
2690 #[test]
2691 fn bridge_response_debug_format_binary() {
2692 let resp = super::BridgeResponse::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF]);
2693 let debug_str = format!("{:?}", resp);
2694 assert!(debug_str.contains("Binary"));
2695 }
2696
2697 #[test]
2698 fn bridge_response_clone_ok() {
2699 let resp = super::BridgeResponse::Ok;
2700 let cloned = resp.clone();
2701 assert_eq!(resp, cloned);
2702 }
2703
2704 #[test]
2705 fn bridge_response_clone_err() {
2706 let resp = super::BridgeResponse::Err("error".into());
2707 let cloned = resp.clone();
2708 assert_eq!(resp, cloned);
2709 }
2710
2711 #[test]
2712 fn bridge_response_clone_value() {
2713 let resp = super::BridgeResponse::Value("value".into());
2714 let cloned = resp.clone();
2715 assert_eq!(resp, cloned);
2716 }
2717
2718 #[test]
2719 fn bridge_response_clone_binary() {
2720 let resp = super::BridgeResponse::Binary(vec![1, 2, 3, 4]);
2721 let cloned = resp.clone();
2722 assert_eq!(resp, cloned);
2723 }
2724
2725 #[test]
2726 fn bridge_response_equality_ok() {
2727 assert_eq!(super::BridgeResponse::Ok, super::BridgeResponse::Ok);
2728 }
2729
2730 #[test]
2731 fn bridge_response_equality_err() {
2732 assert_eq!(
2733 super::BridgeResponse::Err("same error".into()),
2734 super::BridgeResponse::Err("same error".into())
2735 );
2736 assert_ne!(
2737 super::BridgeResponse::Err("error a".into()),
2738 super::BridgeResponse::Err("error b".into())
2739 );
2740 }
2741
2742 #[test]
2743 fn bridge_response_equality_value() {
2744 assert_eq!(
2745 super::BridgeResponse::Value("same".into()),
2746 super::BridgeResponse::Value("same".into())
2747 );
2748 assert_ne!(
2749 super::BridgeResponse::Value("a".into()),
2750 super::BridgeResponse::Value("b".into())
2751 );
2752 }
2753
2754 #[test]
2755 fn bridge_response_equality_binary() {
2756 assert_eq!(
2757 super::BridgeResponse::Binary(vec![1, 2, 3]),
2758 super::BridgeResponse::Binary(vec![1, 2, 3])
2759 );
2760 assert_ne!(
2761 super::BridgeResponse::Binary(vec![1, 2, 3]),
2762 super::BridgeResponse::Binary(vec![1, 2, 4])
2763 );
2764 }
2765
2766 #[test]
2767 fn bridge_response_variants_distinct() {
2768 let responses = [
2769 super::BridgeResponse::Ok,
2770 super::BridgeResponse::Err("e".into()),
2771 super::BridgeResponse::Null,
2772 super::BridgeResponse::Value("v".into()),
2773 super::BridgeResponse::Binary(vec![1]),
2774 ];
2775 for i in 0..responses.len() {
2776 for j in 0..responses.len() {
2777 if i != j {
2778 assert_ne!(responses[i], responses[j]);
2779 }
2780 }
2781 }
2782 }
2783
2784 #[test]
2787 fn bridge_channel_send_timeout_zero_timeout_returns_err() {
2788 let (channel, receiver) = super::BridgeChannel::new();
2791 let _drainer = std::thread::spawn(move || {
2793 let _ = receiver.recv();
2795 });
2796 let result = channel.send_timeout(
2797 super::BridgeCommand::GetTitle,
2798 std::time::Duration::from_secs(0),
2799 );
2800 assert!(result.is_err());
2801 }
2802
2803 #[test]
2804 fn bridge_channel_send_timeout_short_timeout() {
2805 let (channel, _receiver) = super::BridgeChannel::new();
2806 let result = channel.send_timeout(
2808 super::BridgeCommand::GetTitle,
2809 std::time::Duration::from_millis(1),
2810 );
2811 assert!(result.is_err());
2812 }
2813
2814 #[test]
2815 fn bridge_channel_fire_and_forget_multiple() {
2816 let (channel, receiver) = super::BridgeChannel::new();
2817 assert!(channel
2818 .fire_and_forget(super::BridgeCommand::GetTitle)
2819 .is_ok());
2820 assert!(channel
2821 .fire_and_forget(super::BridgeCommand::GetUrl)
2822 .is_ok());
2823 assert!(channel
2824 .fire_and_forget(super::BridgeCommand::Screenshot)
2825 .is_ok());
2826
2827 let (cmd1, _) = receiver.recv().unwrap();
2828 let (cmd2, _) = receiver.recv().unwrap();
2829 let (cmd3, _) = receiver.recv().unwrap();
2830
2831 assert_eq!(cmd1, super::BridgeCommand::GetTitle);
2832 assert_eq!(cmd2, super::BridgeCommand::GetUrl);
2833 assert_eq!(cmd3, super::BridgeCommand::Screenshot);
2834 }
2835
2836 #[test]
2837 fn bridge_channel_close_then_send_fails() {
2838 let (channel, receiver) = super::BridgeChannel::new();
2839 channel.close();
2840 assert!(!channel.is_alive());
2844 drop(receiver);
2846 let result = channel.send(super::BridgeCommand::GetTitle);
2848 assert!(result.is_err());
2849 }
2850
2851 #[test]
2852 fn bridge_channel_close_then_fire_and_forget_fails() {
2853 let (channel, receiver) = super::BridgeChannel::new();
2854 channel.close();
2855 drop(receiver);
2857 let result = channel.fire_and_forget(super::BridgeCommand::Close);
2858 assert!(result.is_err());
2859 }
2860
2861 #[test]
2862 fn bridge_channel_receiver_sees_close_flag() {
2863 let (channel, receiver) = super::BridgeChannel::new();
2864 assert!(receiver.is_alive());
2865 channel.close();
2866 assert!(!receiver.is_alive());
2867 }
2868
2869 #[test]
2870 fn bridge_channel_multiple_send_response_pairs() {
2871 let (channel, receiver) = super::BridgeChannel::new();
2872
2873 let worker = std::thread::spawn(move || {
2874 for _ in 0..3 {
2875 let (cmd, responder) = receiver.recv().unwrap();
2876 if let Some(resp_tx) = responder {
2877 let resp = match cmd {
2878 super::BridgeCommand::GetTitle => {
2879 super::BridgeResponse::Value("Title".into())
2880 }
2881 super::BridgeCommand::GetUrl => {
2882 super::BridgeResponse::Value("https://url.com".into())
2883 }
2884 _ => super::BridgeResponse::Ok,
2885 };
2886 resp_tx.send(resp).unwrap();
2887 }
2888 }
2889 });
2890
2891 let r1 = channel.send(super::BridgeCommand::GetTitle).unwrap();
2892 let r2 = channel.send(super::BridgeCommand::GetUrl).unwrap();
2893 let r3 = channel.send(super::BridgeCommand::Screenshot).unwrap();
2894
2895 assert_eq!(r1, super::BridgeResponse::Value("Title".into()));
2896 assert_eq!(r2, super::BridgeResponse::Value("https://url.com".into()));
2897 assert_eq!(r3, super::BridgeResponse::Ok);
2898
2899 worker.join().unwrap();
2900 }
2901
2902 #[test]
2905 fn bridge_receiver_recv_timeout_short() {
2906 let (_channel, receiver) = super::BridgeChannel::new();
2907 let result = receiver.recv_timeout(std::time::Duration::from_millis(1));
2909 assert!(result.is_err());
2910 }
2911
2912 #[test]
2913 fn bridge_receiver_recv_after_channel_dropped() {
2914 let (channel, receiver) = super::BridgeChannel::new();
2915 drop(channel);
2916 let result = receiver.recv();
2918 assert!(result.is_err());
2919 assert_eq!(result.unwrap_err(), "channel closed");
2920 }
2921
2922 #[test]
2923 fn bridge_receiver_debug_format() {
2924 let (_channel, receiver) = super::BridgeChannel::new();
2925 let debug_str = format!("{:?}", receiver);
2926 assert!(debug_str.contains("BridgeReceiver"));
2927 assert!(debug_str.contains("alive"));
2928 }
2929
2930 #[test]
2933 fn runtime_bridge_send_timeout() {
2934 let (bridge, receiver) = super::RuntimeBridge::new();
2935
2936 let worker = std::thread::spawn(move || {
2937 let (cmd, responder) = receiver.recv().unwrap();
2938 if let Some(resp_tx) = responder {
2939 let resp = match cmd {
2940 super::BridgeCommand::Evaluate(ref code) => {
2941 super::BridgeResponse::Value(format!("evaluated: {}", code))
2942 }
2943 _ => super::BridgeResponse::Ok,
2944 };
2945 resp_tx.send(resp).unwrap();
2946 }
2947 });
2948
2949 let result = bridge
2950 .send_timeout(
2951 super::BridgeCommand::Evaluate("1+1".into()),
2952 std::time::Duration::from_secs(5),
2953 )
2954 .unwrap();
2955 assert_eq!(
2956 result,
2957 super::BridgeResponse::Value("evaluated: 1+1".into())
2958 );
2959
2960 worker.join().unwrap();
2961 }
2962
2963 #[test]
2964 fn runtime_bridge_close_propagates() {
2965 let (bridge, receiver) = super::RuntimeBridge::new();
2966 assert!(bridge.is_alive());
2967 assert!(receiver.is_alive());
2968 bridge.close();
2969 assert!(!bridge.is_alive());
2970 assert!(!receiver.is_alive());
2971 }
2972
2973 #[test]
2974 fn runtime_bridge_fire_and_forget_after_close_still_works() {
2975 let (bridge, receiver) = super::RuntimeBridge::new();
2976 bridge.close();
2977 assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2980 let (cmd, responder) = receiver.recv().unwrap();
2981 assert_eq!(cmd, super::BridgeCommand::Close);
2982 assert!(responder.is_none());
2983 }
2984
2985 #[test]
2986 fn runtime_bridge_send_after_receiver_dropped() {
2987 let (bridge, receiver) = super::RuntimeBridge::new();
2988 drop(receiver);
2989 let result = bridge.send(super::BridgeCommand::GetTitle);
2990 assert!(result.is_err());
2991 }
2992
2993 #[test]
2994 fn runtime_bridge_debug_format() {
2995 let (bridge, _receiver) = super::RuntimeBridge::new();
2996 let debug_str = format!("{:?}", bridge);
2997 assert!(debug_str.contains("RuntimeBridge"));
2998 }
2999
3000 #[test]
3003 fn node_polyfills_process_version() {
3004 let poly = super::NODE_POLYFILLS;
3005 assert!(poly.contains("version: 'v20.11.0'"));
3006 }
3007
3008 #[test]
3009 fn node_polyfills_process_versions_structure() {
3010 let poly = super::NODE_POLYFILLS;
3011 assert!(poly.contains("node: '20.11.0'"));
3013 assert!(poly.contains("v8: '12.4.254.14'"));
3014 assert!(poly.contains("uv: '1.27.0'"));
3015 assert!(poly.contains("zlib: '1.2.13'"));
3016 assert!(poly.contains("brotli: '1.0.9'"));
3017 assert!(poly.contains("ares: '1.19.1'"));
3018 assert!(poly.contains("modules: '115'"));
3019 assert!(poly.contains("openssl: '3.0.12'"));
3020 assert!(poly.contains("icu: '74.2'"));
3021 assert!(poly.contains("bun: '1.0.25'"));
3022 assert!(poly.contains("bao: '0.1.0'"));
3023 }
3024
3025 #[test]
3026 fn node_polyfills_process_env() {
3027 let poly = super::NODE_POLYFILLS;
3028 assert!(poly.contains("env:"));
3029 assert!(poly.contains("e.HOME = '/'"));
3030 assert!(poly.contains("e.PATH = '/usr/local/bin:/usr/bin:/bin'"));
3031 assert!(poly.contains("e.TERM = 'xterm-256color'"));
3032 assert!(poly.contains("e.NODE_VERSION = '20.11.0'"));
3033 assert!(poly.contains("e.BAO_VERSION = '0.1.0'"));
3034 }
3035
3036 #[test]
3037 fn node_polyfills_process_argv() {
3038 let poly = super::NODE_POLYFILLS;
3039 assert!(poly.contains("argv:"));
3040 assert!(poly.contains("argv0: 'bao'"));
3041 }
3042
3043 #[test]
3044 fn node_polyfills_buffer_from() {
3045 let poly = super::NODE_POLYFILLS;
3046 assert!(poly.contains("B.from = function"));
3047 assert!(poly.contains("if (data instanceof B)"));
3048 assert!(poly.contains("if (encoding === 'hex')"));
3049 assert!(poly.contains("if (encoding === 'base64')"));
3050 }
3051
3052 #[test]
3053 fn node_polyfills_buffer_alloc() {
3054 let poly = super::NODE_POLYFILLS;
3055 assert!(poly.contains("B.alloc = function"));
3056 assert!(poly.contains("B.allocUnsafe = function"));
3057 assert!(poly.contains("B.allocUnsafeSlow = function"));
3058 }
3059
3060 #[test]
3061 fn node_polyfills_buffer_static_methods() {
3062 let poly = super::NODE_POLYFILLS;
3063 assert!(poly.contains("B.isBuffer = function"));
3064 assert!(poly.contains("B.concat = function"));
3065 assert!(poly.contains("B.byteLength = function"));
3066 assert!(poly.contains("B.compare = function"));
3067 }
3068
3069 #[test]
3070 fn node_polyfills_buffer_instance_methods() {
3071 let poly = super::NODE_POLYFILLS;
3072 assert!(poly.contains("B.prototype.slice = function"));
3073 assert!(poly.contains("B.prototype.toString = function"));
3074 assert!(poly.contains("B.prototype.toJSON = function"));
3075 assert!(poly.contains("B.prototype.equals = function"));
3076 assert!(poly.contains("B.prototype.compare = function"));
3077 assert!(poly.contains("B.prototype.copy = function"));
3078 assert!(poly.contains("B.prototype.fill = function"));
3079 assert!(poly.contains("B.prototype.write = function"));
3080 assert!(poly.contains("B.prototype.indexOf = function"));
3081 }
3082
3083 #[test]
3084 fn node_polyfills_buffer_read_methods() {
3085 let poly = super::NODE_POLYFILLS;
3086 assert!(poly.contains("B.prototype.readUInt8 = function"));
3087 assert!(poly.contains("B.prototype.readUInt16LE = function"));
3088 assert!(poly.contains("B.prototype.readUInt16BE = function"));
3089 assert!(poly.contains("B.prototype.readUInt32LE = function"));
3090 assert!(poly.contains("B.prototype.readInt8 = function"));
3091 assert!(poly.contains("B.prototype.readInt16LE = function"));
3092 assert!(poly.contains("B.prototype.readInt32LE = function"));
3093 assert!(poly.contains("B.prototype.readFloatLE = function"));
3094 assert!(poly.contains("B.prototype.readDoubleLE = function"));
3095 }
3096
3097 #[test]
3098 fn node_polyfills_buffer_write_methods() {
3099 let poly = super::NODE_POLYFILLS;
3100 assert!(poly.contains("B.prototype.writeUInt8 = function"));
3101 assert!(poly.contains("B.prototype.writeUInt16LE = function"));
3102 assert!(poly.contains("B.prototype.writeUInt32LE = function"));
3103 assert!(poly.contains("B.prototype.writeInt8 = function"));
3104 assert!(poly.contains("B.prototype.writeInt16LE = function"));
3105 assert!(poly.contains("B.prototype.writeInt32LE = function"));
3106 assert!(poly.contains("B.prototype.writeFloatLE = function"));
3107 assert!(poly.contains("B.prototype.writeDoubleLE = function"));
3108 }
3109
3110 #[test]
3111 fn node_polyfills_require_cache() {
3112 let poly = super::NODE_POLYFILLS;
3113 assert!(poly.contains("require.cache = _module_cache"));
3114 assert!(poly.contains("_module_cache = {}"));
3115 }
3116
3117 #[test]
3118 fn node_polyfills_require_builtin_modules() {
3119 let poly = super::NODE_POLYFILLS;
3120 assert!(poly.contains("'fs':"));
3122 assert!(poly.contains("'path':"));
3123 assert!(poly.contains("'url':"));
3124 assert!(poly.contains("'querystring':"));
3125 assert!(poly.contains("'events':"));
3126 assert!(poly.contains("'util':"));
3127 assert!(poly.contains("'stream':"));
3128 assert!(poly.contains("'buffer':"));
3129 assert!(poly.contains("'crypto':"));
3130 assert!(poly.contains("'os':"));
3131 assert!(poly.contains("'assert':"));
3132 assert!(poly.contains("'timers':"));
3133 }
3134
3135 #[test]
3136 fn node_polyfills_path_module() {
3137 let poly = super::NODE_POLYFILLS;
3138 assert!(poly.contains("join: function"));
3139 assert!(poly.contains("resolve: function"));
3140 assert!(poly.contains("dirname: function"));
3141 assert!(poly.contains("basename: function"));
3142 assert!(poly.contains("extname: function"));
3143 assert!(poly.contains("sep: '/'"));
3144 assert!(poly.contains("posix:"));
3145 assert!(poly.contains("win32:"));
3146 }
3147
3148 #[test]
3149 fn node_polyfills_global_alias() {
3150 let poly = super::NODE_POLYFILLS;
3151 assert!(poly.contains("global = globalThis"));
3152 }
3153
3154 #[test]
3155 fn node_polyfills_text_encoder_decoder() {
3156 let poly = super::NODE_POLYFILLS;
3157 assert!(poly.contains("TextEncoder"));
3158 assert!(poly.contains("TextDecoder"));
3159 }
3160
3161 #[test]
3162 fn node_polyfills_btoa_atob() {
3163 let poly = super::NODE_POLYFILLS;
3164 assert!(poly.contains("btoa = function"));
3165 assert!(poly.contains("atob = function"));
3166 assert!(poly.contains("_b64chars"));
3167 }
3168
3169 #[test]
3172 fn bridge_command_empty_navigate_url() {
3173 let cmd = super::BridgeCommand::Navigate("".into());
3174 let cloned = cmd.clone();
3175 assert_eq!(cmd, cloned);
3176 let debug_str = format!("{:?}", cmd);
3177 assert!(debug_str.contains("Navigate"));
3178 }
3179
3180 #[test]
3181 fn bridge_command_empty_evaluate_string() {
3182 let cmd = super::BridgeCommand::Evaluate("".into());
3183 let cloned = cmd.clone();
3184 assert_eq!(cmd, cloned);
3185 let debug_str = format!("{:?}", cmd);
3186 assert!(debug_str.contains("Evaluate"));
3187 }
3188
3189 #[test]
3190 fn bridge_response_empty_value() {
3191 let resp = super::BridgeResponse::Value("".into());
3192 assert!(!resp.is_ok());
3193 assert!(!resp.is_err());
3194 let result = resp.ok();
3195 assert!(result.is_ok());
3196 assert_eq!(result.unwrap(), super::BridgeResponse::Value("".into()));
3197 }
3198
3199 #[test]
3200 fn bridge_response_empty_binary() {
3201 let resp = super::BridgeResponse::Binary(vec![]);
3202 assert!(!resp.is_ok());
3203 assert!(!resp.is_err());
3204 let cloned = resp.clone();
3205 assert_eq!(resp, cloned);
3206 }
3207
3208 #[test]
3209 fn bridge_response_large_binary_payload() {
3210 let large_data: Vec<u8> = (0..=255).cycle().take(1024 * 1024).collect();
3212 let resp = super::BridgeResponse::Binary(large_data.clone());
3213 assert!(!resp.is_ok());
3214 assert!(!resp.is_err());
3215 let cloned = resp.clone();
3216 assert_eq!(resp, cloned);
3217 if let super::BridgeResponse::Binary(data) = cloned {
3219 assert_eq!(data.len(), 1024 * 1024);
3220 assert_eq!(data[0], 0);
3221 assert_eq!(data[255], 255);
3222 assert_eq!(data[256], 0); } else {
3224 panic!("Expected Binary variant");
3225 }
3226 }
3227
3228 #[test]
3229 fn bridge_command_unicode_navigate_url() {
3230 let unicode_url = "https://例子.测试/路径?查询=值#片段";
3231 let cmd = super::BridgeCommand::Navigate(unicode_url.into());
3232 let cloned = cmd.clone();
3233 assert_eq!(cmd, cloned);
3234 let debug_str = format!("{:?}", cmd);
3235 assert!(debug_str.contains(unicode_url));
3236 }
3237
3238 #[test]
3239 fn bridge_command_unicode_evaluate_string() {
3240 let unicode_code = "console.log('你好世界 🎉')";
3241 let cmd = super::BridgeCommand::Evaluate(unicode_code.into());
3242 let cloned = cmd.clone();
3243 assert_eq!(cmd, cloned);
3244 let debug_str = format!("{:?}", cmd);
3245 assert!(debug_str.contains(unicode_code));
3246 }
3247
3248 #[test]
3249 fn bridge_response_unicode_value() {
3250 let unicode_value = "结果: 成功 ✅ 日本語 한국어 العربية";
3251 let resp = super::BridgeResponse::Value(unicode_value.into());
3252 let cloned = resp.clone();
3253 assert_eq!(resp, cloned);
3254 let debug_str = format!("{:?}", resp);
3255 assert!(debug_str.contains(unicode_value));
3256 }
3257
3258 #[test]
3259 fn bridge_response_unicode_error() {
3260 let unicode_error = "错误: 文件未找到 📁❌";
3261 let resp = super::BridgeResponse::Err(unicode_error.into());
3262 assert!(resp.is_err());
3263 let result = resp.ok();
3264 assert!(result.is_err());
3265 assert_eq!(result.unwrap_err(), unicode_error);
3266 }
3267
3268 #[test]
3269 fn bridge_channel_debug_format() {
3270 let (channel, _receiver) = super::BridgeChannel::new();
3271 let debug_str = format!("{:?}", channel);
3272 assert!(debug_str.contains("BridgeChannel"));
3273 assert!(debug_str.contains("alive"));
3274 }
3275
3276 #[test]
3282 fn runtime_bridge_calls_web_apis_not_install_all() {
3283 let source = include_str!("runtime_bridge.rs");
3284 let func_start = source
3285 .find("unsafe fn install_all_native")
3286 .expect("install_all_native function not found");
3287 let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3289
3290 assert!(
3291 func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3292 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3293 );
3294 assert!(
3295 func_body.contains("bun_runtime::timers::install_timer_globals"),
3296 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3297 );
3298 assert!(
3299 !func_body.contains("globals::install_all("),
3300 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3301 );
3302 assert!(
3303 !func_body.contains("globals::install_node_apis("),
3304 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3305 );
3306 }
3307
3308 #[test]
3316 fn worker_scope_init_native_is_stealth_only() {
3317 let source = include_str!("runtime_bridge.rs");
3318 let func_start = source
3319 .find("unsafe fn worker_scope_init_native")
3320 .expect("worker_scope_init_native function not found");
3321 let search_end = source[func_start..]
3323 .find("\n// @trace REQ-SEC-003 [entity:WebPolyfills]")
3324 .or_else(|| source[func_start..].find("\nconst WEB_POLYFILLS"))
3325 .unwrap_or(5000)
3326 .min(5000);
3327 let func_body = &source[func_start..func_start + search_end];
3328
3329 assert!(
3331 func_body.contains("install_stealth_props"),
3332 "BCE-20260627-009 REGRESSION: worker_scope_init_native must install stealth props (DEC-WK-007)"
3333 );
3334
3335 assert!(
3337 !func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3338 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install fetch (servo native)"
3339 );
3340 assert!(
3341 !func_body.contains("bun_runtime::timers::install_timer_globals"),
3342 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install timers (servo native)"
3343 );
3344 assert!(
3345 !func_body.contains("bun_runtime::web_api::install_performance"),
3346 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install performance (servo native)"
3347 );
3348 assert!(
3349 !func_body.contains("bun_runtime::globals::install_crypto_global"),
3350 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install crypto (servo native)"
3351 );
3352 assert!(
3353 !func_body.contains("bun_runtime::globals::install_structured_clone"),
3354 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install structuredClone (servo native)"
3355 );
3356 }
3357
3358 #[test]
3361 fn runtime_bridge_node_realm_uses_new_compartment() {
3362 let source = include_str!("runtime_bridge.rs");
3363
3364 let func_start = source
3365 .find("unsafe fn create_node_realm_native")
3366 .expect("create_node_realm_native function not found");
3367 let func_body_start = source[func_start..]
3368 .find("{")
3369 .expect("function body start not found");
3370 let search_limit = source[func_start + func_body_start..]
3371 .find("pub fn inject_node_apis")
3372 .or_else(|| {
3373 source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3374 })
3375 .unwrap_or(3000)
3376 .min(3000);
3377 let func_body =
3378 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3379
3380 assert!(
3381 func_body.contains("NewCompartmentAndZone"),
3382 "REQ-SEC-002 REGRESSION: create_node_realm_native must use NewCompartmentAndZone"
3383 );
3384 assert!(
3385 func_body.contains("SIMPLE_GLOBAL_CLASS"),
3386 "REQ-SEC-002 REGRESSION: create_node_realm_native must use SIMPLE_GLOBAL_CLASS"
3387 );
3388 assert!(
3389 func_body.contains("AutoRealm::new_from_handle"),
3390 "REQ-SEC-002 REGRESSION: create_node_realm_native must use AutoRealm"
3391 );
3392 assert!(
3393 func_body.contains("bun_runtime::globals::install_node_apis"),
3394 "REQ-SEC-002 REGRESSION: Node APIs must be installed on Node Realm global"
3395 );
3396 }
3397
3398 #[test]
3401 fn runtime_bridge_evaluate_in_node_realm_uses_auto_realm() {
3402 let source = include_str!("runtime_bridge.rs");
3403
3404 let func_start = source
3405 .find("pub unsafe fn evaluate_in_node_realm")
3406 .expect("evaluate_in_node_realm function not found");
3407 let func_body_start = source[func_start..]
3408 .find("{")
3409 .expect("function body start not found");
3410 let search_limit = source[func_start + func_body_start..]
3411 .find("unsafe fn create_node_realm_native")
3412 .unwrap_or(3000)
3413 .min(3000);
3414 let func_body =
3415 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3416
3417 assert!(
3418 func_body.contains("AutoRealm::new"),
3419 "REQ-SEC-002 REGRESSION: evaluate_in_node_realm must use AutoRealm"
3420 );
3421 assert!(
3422 func_body.contains("evaluate_script"),
3423 "REQ-SEC-002: evaluate_in_node_realm must call evaluate_script"
3424 );
3425 }
3426
3427 #[test]
3434 fn runtime_bridge_has_per_page_node_realm_storage() {
3435 let source = include_str!("runtime_bridge.rs");
3436 assert!(
3437 source.contains("NODE_REALM_BY_WEBVIEW"),
3438 "REQ-SEC-002 REGRESSION: must have NODE_REALM_BY_WEBVIEW per-page storage"
3439 );
3440 assert!(
3441 source.contains("store_node_realm"),
3442 "REQ-SEC-002 REGRESSION: must have store_node_realm accessor"
3443 );
3444 assert!(
3445 source.contains("get_node_realm_by_id"),
3446 "REQ-SEC-002 REGRESSION: must have get_node_realm_by_id accessor (WebViewId-keyed)"
3447 );
3448 assert!(
3449 source.contains("get_node_realm_global"),
3450 "REQ-SEC-002 REGRESSION: must have get_node_realm_global accessor"
3451 );
3452 for line in source.lines() {
3458 let trimmed = line.trim_start();
3459 assert!(
3462 !(trimmed.starts_with("static NODE_REALMS:")
3463 && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
3464 "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed NODE_REALMS must not exist"
3465 );
3466 assert!(
3467 !(trimmed.starts_with("static PAGE_GLOBALS:")
3468 && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
3469 "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed PAGE_GLOBALS must not exist"
3470 );
3471 assert!(
3472 !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
3473 "BCE-20260621-001 REGRESSION: process-wide LAST_PAGE_GLOBAL must not exist"
3474 );
3475 }
3476 }
3477
3478 #[test]
3482 fn runtime_bridge_drain_uses_callbacks_method() {
3483 let source = include_str!("runtime_bridge.rs");
3484
3485 let func_start = source
3486 .find("pub fn inject_node_apis_with_stealth")
3487 .expect("inject_node_apis_with_stealth function not found");
3488 let func_end = source[func_start..]
3489 .find("fn register_native_host_functions")
3490 .expect("end boundary not found");
3491 let func_body = &source[func_start..func_start + func_end];
3492
3493 assert!(
3494 func_body.contains("drain_callbacks"),
3495 "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must use drain_callbacks (not evaluate_js)"
3496 );
3497 assert!(
3498 !func_body.contains("page.evaluate_js(\""),
3499 "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must NOT call evaluate_js with string arg (would cause recursion)"
3500 );
3501 assert!(
3502 !func_body.contains("let _"),
3503 "REQ-SEC-003 REGRESSION: inject_node_apis_with_stealth must NOT swallow errors with let _"
3504 );
3505 }
3506
3507 #[test]
3509 fn node_polyfills_contains_security_sensitive_names() {
3510 let poly = super::NODE_POLYFILLS;
3511 assert!(
3512 poly.contains("require"),
3513 "NODE_POLYFILLS must contain 'require'"
3514 );
3515 assert!(
3516 poly.contains("Buffer"),
3517 "NODE_POLYFILLS must contain 'Buffer'"
3518 );
3519 assert!(
3520 poly.contains("process"),
3521 "NODE_POLYFILLS must contain 'process'"
3522 );
3523 }
3524
3525 #[test]
3530 fn web_polyfills_excludes_node_apis() {
3531 let poly = super::WEB_POLYFILLS;
3532 assert!(
3533 !poly.contains("require"),
3534 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'require'"
3535 );
3536 assert!(
3537 !poly.contains("Buffer"),
3538 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Buffer'"
3539 );
3540 assert!(
3541 !poly.contains("process"),
3542 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'process'"
3543 );
3544 assert!(
3545 !poly.contains("Bun"),
3546 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Bun'"
3547 );
3548 assert!(
3549 !poly.contains("module"),
3550 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'module'"
3551 );
3552 assert!(
3553 !poly.contains("__dirname"),
3554 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__dirname'"
3555 );
3556 assert!(
3557 !poly.contains("__filename"),
3558 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__filename'"
3559 );
3560 }
3561
3562 #[test]
3565 fn web_polyfills_includes_web_apis() {
3566 let poly = super::WEB_POLYFILLS;
3567 assert!(
3568 poly.contains("TextEncoder"),
3569 "WEB_POLYFILLS must contain TextEncoder"
3570 );
3571 assert!(
3572 poly.contains("TextDecoder"),
3573 "WEB_POLYFILLS must contain TextDecoder"
3574 );
3575 assert!(poly.contains("URL"), "WEB_POLYFILLS must contain URL");
3576 assert!(
3577 poly.contains("URLSearchParams"),
3578 "WEB_POLYFILLS must contain URLSearchParams"
3579 );
3580 assert!(poly.contains("btoa"), "WEB_POLYFILLS must contain btoa");
3581 assert!(poly.contains("atob"), "WEB_POLYFILLS must contain atob");
3582 }
3583
3584 #[test]
3587 fn fallback_uses_web_polyfills_not_node_polyfills() {
3588 let source = include_str!("runtime_bridge.rs");
3589
3590 let func_start = source
3591 .find("pub fn inject_node_apis_with_stealth")
3592 .expect("inject_node_apis_with_stealth function not found");
3593 let func_end = source[func_start..]
3594 .find("fn register_native_host_functions")
3595 .expect("end boundary not found");
3596 let func_body = &source[func_start..func_start + func_end];
3597
3598 assert!(
3599 func_body.contains("WEB_POLYFILLS"),
3600 "REQ-SEC-003 REGRESSION: fallback must use WEB_POLYFILLS (not NODE_POLYFILLS)"
3601 );
3602 let fallback_section = func_body
3604 .find("if !registered")
3605 .map(|i| &func_body[i..])
3606 .unwrap_or("");
3607 assert!(
3608 !fallback_section.contains("NODE_POLYFILLS"),
3609 "REQ-SEC-003 REGRESSION: fallback path must NOT reference NODE_POLYFILLS"
3610 );
3611 }
3612
3613 #[test]
3616 fn install_all_native_web_apis_only() {
3617 let source = include_str!("runtime_bridge.rs");
3618
3619 let func_start = source
3620 .find("unsafe fn install_all_native")
3621 .expect("install_all_native function not found");
3622 let func_body_start = source[func_start..]
3623 .find("{")
3624 .expect("function body start not found");
3625 let search_limit = source[func_start + func_body_start..]
3626 .find("const NODE_POLYFILLS")
3627 .or_else(|| {
3628 source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3629 })
3630 .unwrap_or(5000)
3631 .min(5000);
3632 let func_body =
3633 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3634
3635 assert!(
3636 func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3637 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3638 );
3639 assert!(
3640 func_body.contains("bun_runtime::timers::install_timer_globals"),
3641 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3642 );
3643 assert!(
3644 !func_body.contains("globals::install_all("),
3645 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3646 );
3647 assert!(
3648 !func_body.contains("globals::install_node_apis("),
3649 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3650 );
3651 }
3652
3653 #[test]
3656 fn node_apis_installed_in_node_realm_only() {
3657 let source = include_str!("runtime_bridge.rs");
3658
3659 let func_start = source
3660 .find("unsafe fn create_node_realm_native")
3661 .expect("create_node_realm_native function not found");
3662 let func_body_start = source[func_start..]
3663 .find("{")
3664 .expect("function body start not found");
3665 let search_limit = source[func_start + func_body_start..]
3666 .find("unsafe fn wrap_and_install_dom_proxy")
3667 .or_else(|| source[func_start + func_body_start..].find("/// Wrap a DOM property"))
3668 .unwrap_or(3000)
3669 .min(3000);
3670 let func_body =
3671 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3672
3673 assert!(
3674 func_body.contains("bun_runtime::globals::install_node_apis"),
3675 "REQ-SEC-003 REGRESSION: Node Realm must install Node APIs (install_node_apis)"
3676 );
3677 assert!(
3678 func_body.contains("NewCompartmentAndZone"),
3679 "REQ-SEC-003 REGRESSION: Node Realm must be isolated via NewCompartmentAndZone"
3680 );
3681 }
3682
3683 #[test]
3685 fn web_polyfills_is_valid_js() {
3686 let poly = super::WEB_POLYFILLS;
3687 assert!(
3688 poly.starts_with("(function()"),
3689 "WEB_POLYFILLS must be an IIFE"
3690 );
3691 assert!(poly.ends_with("})();"), "WEB_POLYFILLS must close IIFE");
3692 }
3693
3694 #[test]
3697 fn remove_node_realm_by_id_is_safe_no_op() {
3698 let _guard = super::test_serial_lock().lock().unwrap();
3703 super::clear_all_node_realms();
3704 let _f: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3710 }
3711
3712 #[test]
3715 fn webview_id_keyed_storage_api_exists() {
3716 let _store: fn(servo::WebViewId, *mut mozjs::jsapi::JSObject, *mut mozjs::jsapi::JSObject) =
3718 super::store_node_realm;
3719 let _get_node: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3720 super::get_node_realm_by_id;
3721 let _get_page: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3722 super::get_page_global_by_id;
3723 let _get_node_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3724 super::get_node_realm_global;
3725 let _get_page_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3726 super::get_page_global;
3727 let _remove: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3728 }
3729
3730 #[test]
3733 fn clear_all_removes_all_entries() {
3734 let _guard = super::test_serial_lock().lock().unwrap();
3735 super::clear_all_node_realms();
3737 super::clear_all_node_realms();
3738 }
3739
3740 #[test]
3742 fn lazy_dom_getters_are_valid_jsnative() {
3743 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_window);
3745 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_document);
3746 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_navigator);
3747 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_worker);
3749 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_shared_worker);
3750 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_service_worker);
3751 }
3752
3753 #[test]
3759 fn worker_constructor_getters_use_permanent_attribute() {
3760 let source = include_str!("runtime_bridge.rs");
3761 assert!(
3763 source.contains("ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY | mozjs::jsapi::JSPROP_PERMANENT)"),
3764 "REQ-BRW-004 REGRESSION: Worker/SharedWorker/ServiceWorker constructors must use JSPROP_PERMANENT"
3765 );
3766 assert!(
3768 source.contains("obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY)"),
3769 "REQ-BRW-004 REGRESSION: window/document/navigator getters should use obj_attrs without JSPROP_PERMANENT"
3770 );
3771 assert!(
3773 source.contains("(c\"Worker\", Some(lazy_dom_getter_worker))"),
3774 "REQ-BRW-004 REGRESSION: Worker must be in ctor_getters"
3775 );
3776 assert!(
3777 source.contains("(c\"SharedWorker\", Some(lazy_dom_getter_shared_worker))"),
3778 "REQ-BRW-004 REGRESSION: SharedWorker must be in ctor_getters"
3779 );
3780 assert!(
3781 source.contains("(c\"ServiceWorker\", Some(lazy_dom_getter_service_worker))"),
3782 "REQ-BRW-004 REGRESSION: ServiceWorker must be in ctor_getters"
3783 );
3784 }
3785
3786 #[test]
3800 fn constructor_getter_reads_from_page_realm_not_node_realm() {
3801 let source = include_str!("runtime_bridge.rs");
3802 let func_start = source
3803 .find("unsafe fn lazy_constructor_getter_impl")
3804 .expect("lazy_constructor_getter_impl not found");
3805 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3806
3807 assert!(
3809 func_body.contains("PER_THREAD_PAGE_GLOBAL.with"),
3810 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must read Page Realm global from PER_THREAD_PAGE_GLOBAL"
3811 );
3812 assert!(
3814 func_body.contains("page_global_root.handle()"),
3815 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must JS_GetProperty from page_global_root (Page Realm)"
3816 );
3817 }
3818
3819 #[test]
3829 fn constructor_getter_uses_js_wrap_object_for_cross_compartment_proxy() {
3830 let source = include_str!("runtime_bridge.rs");
3831 let func_start = source
3832 .find("unsafe fn lazy_constructor_getter_impl")
3833 .expect("lazy_constructor_getter_impl not found");
3834 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3835
3836 assert!(
3837 func_body.contains("JS_WrapObject"),
3838 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call JS_WrapObject for cross-Compartment proxy"
3839 );
3840 }
3841
3842 #[test]
3852 fn constructor_getter_validates_is_constructor_before_wrapping() {
3853 let source = include_str!("runtime_bridge.rs");
3854 let func_start = source
3855 .find("unsafe fn lazy_constructor_getter_impl")
3856 .expect("lazy_constructor_getter_impl not found");
3857 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3858
3859 assert!(
3860 func_body.contains("mozjs::jsapi::IsConstructor"),
3861 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call IsConstructor to validate the constructor"
3862 );
3863 }
3864
3865 #[test]
3875 fn constructor_getter_throws_reference_error_on_failure() {
3876 let source = include_str!("runtime_bridge.rs");
3877 let func_start = source
3878 .find("unsafe fn lazy_constructor_getter_impl")
3879 .expect("lazy_constructor_getter_impl not found");
3880 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3881
3882 assert!(
3884 func_body.contains("report_reference_error"),
3885 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call report_reference_error for error reporting"
3886 );
3887 assert!(
3889 func_body.contains("property_name"),
3890 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl error messages must reference property_name"
3891 );
3892 }
3893
3894 #[test]
3904 fn constructor_getter_throws_on_null_page_global() {
3905 let source = include_str!("runtime_bridge.rs");
3906 let func_start = source
3907 .find("unsafe fn lazy_constructor_getter_impl")
3908 .expect("lazy_constructor_getter_impl not found");
3909 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3910
3911 assert!(
3913 func_body.contains("page_global.is_null()"),
3914 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must check page_global.is_null()"
3915 );
3916 let page_global_null_pos = func_body
3920 .find("page_global.is_null()")
3921 .expect("DF-WK-11: page_global.is_null() check not found");
3922 let after_null_check = &func_body[page_global_null_pos..page_global_null_pos + 300];
3923 assert!(
3924 after_null_check.contains("report_reference_error"),
3925 "DF-WK-11 REGRESSION: null page_global must trigger report_reference_error, not silent return"
3926 );
3927 }
3928
3929 #[test]
3939 fn install_lazy_dom_getters_uses_js_define_property1_for_constructors() {
3940 let source = include_str!("runtime_bridge.rs");
3944 assert!(
3945 source.find("unsafe fn install_lazy_dom_getters").is_some(),
3946 "install_lazy_dom_getters not found"
3947 );
3948
3949 assert!(
3951 source.contains("JS_DefineProperty1"),
3952 "DF-WK-11 REGRESSION: install_lazy_dom_getters must use JS_DefineProperty1 for property registration"
3953 );
3954 assert!(
3956 source.contains("for &(name, getter) in ctor_getters"),
3957 "DF-WK-11 REGRESSION: ctor_getters must be registered with their own (PERMANENT) attributes"
3958 );
3959 }
3960
3961 #[test]
3972 fn lazy_dom_getters_installed_on_node_realm_not_page_realm() {
3973 let source = include_str!("runtime_bridge.rs");
3974 let func_start = source
3975 .find("unsafe fn create_node_realm_native")
3976 .expect("create_node_realm_native not found");
3977 let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3979
3980 assert!(
3981 func_body.contains("install_lazy_dom_getters(realm_cx, global.handle())"),
3982 "DF-WK-11 REGRESSION: install_lazy_dom_getters must be called from create_node_realm_native on the Node Realm global"
3983 );
3984 assert!(
3987 func_body.contains("JS_NewGlobalObject") && func_body.contains("NewCompartmentAndZone"),
3988 "DF-WK-11 REGRESSION: create_node_realm_native must create Node Realm with NewCompartmentAndZone before installing lazy getters"
3989 );
3990 }
3991
3992 #[test]
4001 fn report_reference_error_uses_reference_err_type() {
4002 let source = include_str!("runtime_bridge.rs");
4003 let func_start = source
4004 .find("unsafe fn report_reference_error")
4005 .expect("report_reference_error not found");
4006 let func_body = &source[func_start..func_start + 2000.min(source.len() - func_start)];
4007
4008 assert!(
4009 func_body.contains("JSEXN_REFERENCEERR"),
4010 "DF-WK-11 REGRESSION: report_reference_error must use JSEXN_REFERENCEERR error type"
4011 );
4012 assert!(
4013 func_body.contains("JS_ReportErrorNumberUTF8"),
4014 "DF-WK-11 REGRESSION: report_reference_error must use JS_ReportErrorNumberUTF8 for proper error reporting"
4015 );
4016 }
4017
4018 #[test]
4024 fn storage_is_webview_id_keyed_dashmap() {
4025 let source = include_str!("runtime_bridge.rs");
4026 assert!(
4027 source.contains(
4028 "static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4029 ),
4030 "BCE-20260621-001 REGRESSION: NODE_REALM_BY_WEBVIEW must be WebViewId-keyed"
4031 );
4032 assert!(
4033 source.contains(
4034 "static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4035 ),
4036 "BCE-20260621-001 REGRESSION: PAGE_GLOBAL_BY_WEBVIEW must be WebViewId-keyed"
4037 );
4038 assert!(
4039 source.contains("thread_local! {\n static PER_THREAD_PAGE_GLOBAL"),
4040 "BCE-20260621-001 REGRESSION: PER_THREAD_PAGE_GLOBAL thread_local must exist for lazy getters"
4041 );
4042 }
4043
4044 #[test]
4048 fn no_cross_thread_raw_jsobject_storage_residual() {
4049 let source = include_str!("runtime_bridge.rs");
4050 for line in source.lines() {
4051 let trimmed = line.trim_start();
4052 assert!(
4053 !(trimmed.starts_with("static NODE_REALMS:")
4054 && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
4055 "BCE-20260621-001 RESIDUAL: NODE_REALMS usize-keyed DashMap still present"
4056 );
4057 assert!(
4058 !(trimmed.starts_with("static PAGE_GLOBALS:")
4059 && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
4060 "BCE-20260621-001 RESIDUAL: PAGE_GLOBALS usize-keyed DashMap still present"
4061 );
4062 assert!(
4063 !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
4064 "BCE-20260621-001 RESIDUAL: LAST_PAGE_GLOBAL AtomicUsize still present"
4065 );
4066 assert!(
4067 !trimmed.starts_with("fn get_last_page_global"),
4068 "BCE-20260621-001 RESIDUAL: get_last_page_global accessor still present"
4069 );
4070 assert!(
4071 !trimmed.starts_with("fn set_last_page_global"),
4072 "BCE-20260621-001 RESIDUAL: set_last_page_global accessor still present"
4073 );
4074 assert!(
4075 !trimmed.starts_with("pub fn get_last_page_global"),
4076 "BCE-20260621-001 RESIDUAL: pub get_last_page_global accessor still present"
4077 );
4078 }
4079 }
4080
4081 #[test]
4083 fn oncelock_evaluate_result_set_and_get() {
4084 use std::sync::Arc;
4085 let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4086 assert!(lock.get().is_none(), "OnceLock should be unset initially");
4087
4088 let result = super::EvaluateResult::ok("42".into());
4089 assert!(lock.set(result).is_ok(), "First set should succeed");
4090
4091 let got = lock.get().unwrap();
4092 assert_eq!(got.value, Some("42".into()));
4093 assert!(got.error.is_none());
4094 }
4095
4096 #[test]
4098 fn oncelock_evaluate_result_second_set_fails() {
4099 use std::sync::Arc;
4100 let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4101
4102 let first = super::EvaluateResult::ok("first".into());
4103 assert!(lock.set(first).is_ok());
4104
4105 let second = super::EvaluateResult::err("second".into());
4106 let set_result = lock.set(second);
4107 assert!(set_result.is_err(), "Second set should return Err");
4108 assert_eq!(lock.get().unwrap().value, Some("first".into()));
4110 }
4111}