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, 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 = bao_engine::node_realm_options();
513 options.creationOptions_.compSpec_ =
514 mozjs::jsapi::JS::CompartmentSpecifier::NewCompartmentAndZone;
515
516 rooted!(&in(cx) let global = JS_NewGlobalObject(
517 &mut cx,
518 &SIMPLE_GLOBAL_CLASS,
519 ptr::null_mut(),
520 OnNewGlobalHookOption::DontFireOnNewGlobalHook,
521 &*options,
522 ));
523
524 if global.get().is_null() {
525 return;
526 }
527
528 let mut realm = AutoRealm::new_from_handle(&mut cx, global.handle());
529 let realm_cx: &mut JSContext = &mut realm;
530 JS_FireOnNewGlobalObject(realm_cx.raw_cx(), global.handle().into());
531
532 bun_runtime::globals::install_node_apis(realm_cx, global.handle());
533 bun_runtime::globals::install_web_apis(realm_cx, global.handle());
534
535 if !page_global.is_null() {
536 install_lazy_dom_getters(realm_cx, global.handle());
539 }
540
541 if !page_global.is_null() {
544 PER_THREAD_PAGE_GLOBAL.with(|cell| {
545 *cell.borrow_mut() = page_global;
546 });
547 }
548
549 store_node_realm(webview_id, page_global, global.get());
551
552 if !page_global.is_null() {
560 bao_stealth::engine_props::register_global_alias(
561 page_global as usize,
562 global.get() as usize,
563 );
564 }
565}
566
567unsafe fn wrap_and_install_dom_proxy(
580 cx: &mut mozjs::context::JSContext,
581 node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
582 page_global: *mut mozjs::jsapi::JSObject,
583 property_name: &str,
584) {
585 use mozjs::jsapi::{JS_GetProperty, JS_SetProperty};
586 use mozjs::jsval::{ObjectValue, UndefinedValue};
587 use mozjs::rust::wrappers2::JS_WrapObject;
588
589 let raw_cx = cx.raw_cx();
590 rooted!(&in(cx) let page_global_root = page_global);
595
596 let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
598 let mut raw_prop_val = UndefinedValue();
605 let got = bao_stealth::engine_props::get_property_clearing(
606 raw_cx,
607 page_global_root.handle().into(),
608 c_name.as_cstr(),
609 &mut raw_prop_val,
610 );
611 rooted!(&in(cx) let mut prop_val = raw_prop_val);
612
613 if got && prop_val.get().is_object() {
615 rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
617
618 if !JS_WrapObject(cx, prop_obj.handle_mut().into()) {
620 mozjs::jsapi::JS_ClearPendingException(raw_cx);
623 return;
624 }
625
626 rooted!(&in(cx) let mut wrapped_val = ObjectValue(prop_obj.get()));
628 if !JS_SetProperty(
631 raw_cx,
632 node_global.into(),
633 c_name.as_ptr(),
634 wrapped_val.handle_mut().into(),
635 ) {
636 mozjs::jsapi::JS_ClearPendingException(raw_cx);
637 }
638 }
639}
640
641unsafe fn install_lazy_dom_getters(
649 cx: &mut mozjs::context::JSContext,
650 node_global: mozjs::rust::Handle<*mut mozjs::jsapi::JSObject>,
651) {
652 use mozjs::jsapi::JS_DefineProperty1;
653 use mozjs::jsval::UndefinedValue;
654
655 let raw_cx = cx.raw_cx();
656
657 let obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY) as u32;
659 let ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE
663 | mozjs::jsapi::JSPROP_READONLY
664 | mozjs::jsapi::JSPROP_PERMANENT) as u32;
665
666 let obj_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
667 (c"window", Some(lazy_dom_getter_window)),
668 (c"document", Some(lazy_dom_getter_document)),
669 (c"navigator", Some(lazy_dom_getter_navigator)),
670 ];
671 let ctor_getters: &[(&std::ffi::CStr, mozjs::jsapi::JSNative)] = &[
678 (c"Worker", Some(lazy_dom_getter_worker)),
679 (c"SharedWorker", Some(lazy_dom_getter_shared_worker)),
680 (c"ServiceWorker", Some(lazy_dom_getter_service_worker)),
681 ];
682 for &(name, getter) in obj_getters {
683 if !JS_DefineProperty1(
698 raw_cx,
699 node_global.into(),
700 name.as_ptr(),
701 getter,
702 None,
703 obj_attrs,
704 ) {
705 mozjs::jsapi::JS_ClearPendingException(raw_cx);
706 }
707 }
708 for &(name, getter) in ctor_getters {
709 if !JS_DefineProperty1(
713 raw_cx,
714 node_global.into(),
715 name.as_ptr(),
716 getter,
717 None,
718 ctor_attrs,
719 ) {
720 mozjs::jsapi::JS_ClearPendingException(raw_cx);
721 }
722 }
723}
724
725#[allow(unsafe_op_in_unsafe_fn)]
731unsafe extern "C" fn lazy_dom_getter_window(
732 cx: *mut mozjs::jsapi::JSContext,
733 argc: u32,
734 vp: *mut mozjs::jsval::JSVal,
735) -> bool {
736 lazy_dom_getter_impl(cx, argc, vp, "window")
737}
738
739#[allow(unsafe_op_in_unsafe_fn)]
741unsafe extern "C" fn lazy_dom_getter_document(
742 cx: *mut mozjs::jsapi::JSContext,
743 argc: u32,
744 vp: *mut mozjs::jsval::JSVal,
745) -> bool {
746 lazy_dom_getter_impl(cx, argc, vp, "document")
747}
748
749#[allow(unsafe_op_in_unsafe_fn)]
751unsafe extern "C" fn lazy_dom_getter_navigator(
752 cx: *mut mozjs::jsapi::JSContext,
753 argc: u32,
754 vp: *mut mozjs::jsval::JSVal,
755) -> bool {
756 lazy_dom_getter_impl(cx, argc, vp, "navigator")
757}
758
759#[allow(unsafe_op_in_unsafe_fn)]
783unsafe extern "C" fn lazy_dom_getter_worker(
784 cx: *mut mozjs::jsapi::JSContext,
785 argc: u32,
786 vp: *mut mozjs::jsval::JSVal,
787) -> bool {
788 lazy_constructor_getter_impl(cx, argc, vp, "Worker")
789}
790
791#[allow(unsafe_op_in_unsafe_fn)]
793unsafe extern "C" fn lazy_dom_getter_shared_worker(
794 cx: *mut mozjs::jsapi::JSContext,
795 argc: u32,
796 vp: *mut mozjs::jsval::JSVal,
797) -> bool {
798 lazy_constructor_getter_impl(cx, argc, vp, "SharedWorker")
799}
800
801#[allow(unsafe_op_in_unsafe_fn)]
803unsafe extern "C" fn lazy_dom_getter_service_worker(
804 cx: *mut mozjs::jsapi::JSContext,
805 argc: u32,
806 vp: *mut mozjs::jsval::JSVal,
807) -> bool {
808 lazy_constructor_getter_impl(cx, argc, vp, "ServiceWorker")
809}
810
811#[allow(unsafe_op_in_unsafe_fn)]
831unsafe fn lazy_constructor_getter_impl(
832 raw_cx: *mut mozjs::jsapi::JSContext,
833 _argc: u32,
834 vp: *mut mozjs::jsval::JSVal,
835 property_name: &str,
836) -> bool {
837 use mozjs::context::JSContext;
838 use mozjs::jsapi::{JSObject, JS_DefineProperty1, JS_GetProperty};
839 use mozjs::jsval::{ObjectValue, UndefinedValue};
840 use mozjs::rust::wrappers2::JS_WrapObject;
841 use std::ptr::NonNull;
842
843 let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
844 args.rval().set(UndefinedValue());
845
846 let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
847 if node_global.is_null() {
848 return true;
849 }
850
851 let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
852 if page_global.is_null() {
853 let msg = format!(
855 "Cannot access {} constructor: no browser page is currently loaded",
856 property_name
857 );
858 report_reference_error(raw_cx, &msg);
859 return false;
860 }
861
862 let cx_nn = match NonNull::new(raw_cx) {
863 Some(nn) => nn,
864 None => return true,
865 };
866 let mut cx = JSContext::from_ptr(cx_nn);
867
868 rooted!(&in(cx) let page_global_root = page_global);
870 let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
871 let mut raw_prop_val = UndefinedValue();
877 let got = bao_stealth::engine_props::get_property_clearing(
878 raw_cx,
879 page_global_root.handle().into(),
880 c_name.as_cstr(),
881 &mut raw_prop_val,
882 );
883 rooted!(&in(cx) let mut prop_val = raw_prop_val);
884
885 if !got || !prop_val.get().is_object() {
886 let msg = format!(
890 "Cannot access {} constructor: not available in the current browser context",
891 property_name
892 );
893 report_reference_error(raw_cx, &msg);
894 return false;
895 }
896
897 rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
898
899 if !mozjs::jsapi::IsConstructor(*prop_obj.handle()) {
905 let msg = format!(
906 "{} is not a constructor — the browser context does not support this API",
907 property_name
908 );
909 report_reference_error(raw_cx, &msg);
910 return false;
911 }
912
913 if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
921 return false;
922 }
923
924 args.rval().set(ObjectValue(prop_obj.get()));
942 true
943}
944
945#[allow(unsafe_op_in_unsafe_fn)]
955unsafe fn report_reference_error(cx: *mut mozjs::jsapi::JSContext, message: &str) {
956 use std::ffi::CString;
957 use std::os::raw::c_void;
958
959 let c_msg = match CString::new(message.as_bytes()) {
960 Ok(s) => s,
961 Err(_) => return,
962 };
963
964 static FORMAT_STRING: &std::ffi::CStr = c"{0}";
969
970 unsafe extern "C" fn get_reference_error_format(
973 _user_ref: *mut std::os::raw::c_void,
974 _error_number: u32,
975 ) -> *const mozjs::jsapi::JSErrorFormatString {
976 static mut FORMAT: mozjs::jsapi::JSErrorFormatString = mozjs::jsapi::JSErrorFormatString {
977 name: c"RUSTMSG_REFERENCE_ERROR".as_ptr(),
978 format: FORMAT_STRING.as_ptr(),
979 argCount: 1,
980 exnType: mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as i16,
981 };
982 unsafe { &raw const FORMAT }
985 }
986
987 mozjs::jsapi::JS_ReportErrorNumberUTF8(
991 cx,
992 Some(get_reference_error_format),
993 std::ptr::null_mut(),
994 mozjs::jsapi::JSExnType::JSEXN_REFERENCEERR as u32,
995 c_msg.as_ptr(),
996 );
997}
998
999#[allow(unsafe_op_in_unsafe_fn)]
1015unsafe fn lazy_dom_getter_impl(
1016 raw_cx: *mut mozjs::jsapi::JSContext,
1017 _argc: u32,
1018 vp: *mut mozjs::jsval::JSVal,
1019 property_name: &str,
1020) -> bool {
1021 use mozjs::context::JSContext;
1022 use mozjs::jsapi::{JSObject, JS_GetProperty};
1023 use mozjs::jsval::{ObjectValue, UndefinedValue};
1024 use mozjs::rust::wrappers2::JS_WrapObject;
1025 use std::ptr::NonNull;
1026
1027 let args = mozjs::jsapi::CallArgs::from_vp(vp, 0);
1028 args.rval().set(UndefinedValue());
1029
1030 let node_global = mozjs::jsapi::CurrentGlobalOrNull(raw_cx);
1031 if node_global.is_null() {
1032 return true;
1033 }
1034
1035 let page_global = PER_THREAD_PAGE_GLOBAL.with(|cell| *cell.borrow());
1040 if page_global.is_null() {
1041 return true;
1042 }
1043
1044 let cx_nn = match NonNull::new(raw_cx) {
1046 Some(nn) => nn,
1047 None => return true,
1048 };
1049 let mut cx = JSContext::from_ptr(cx_nn);
1050
1051 rooted!(&in(cx) let page_global_root = page_global);
1057
1058 let c_name = bun_core::ZBox::from_bytes(property_name.as_bytes());
1059 let mut raw_prop_val = UndefinedValue();
1069 let got = bao_stealth::engine_props::get_property_clearing(
1070 raw_cx,
1071 page_global_root.handle().into(),
1072 c_name.as_cstr(),
1073 &mut raw_prop_val,
1074 );
1075 rooted!(&in(cx) let mut prop_val = raw_prop_val);
1076
1077 if !got || !prop_val.get().is_object() {
1078 return true;
1079 }
1080
1081 rooted!(&in(cx) let mut prop_obj = prop_val.get().to_object());
1083 if !JS_WrapObject(&mut cx, prop_obj.handle_mut().into()) {
1084 mozjs::jsapi::JS_ClearPendingException(raw_cx);
1087 return true;
1088 }
1089
1090 args.rval().set(ObjectValue(prop_obj.get()));
1091 true
1092}
1093
1094pub fn inject_node_apis(page: &PageHandle) -> Result<(), BrowserError> {
1107 inject_node_apis_with_stealth(page, None)
1108}
1109
1110pub fn inject_node_apis_with_stealth(
1121 page: &PageHandle,
1122 stealth_profile: Option<bao_stealth::StealthProfile>,
1123) -> Result<(), BrowserError> {
1124 let webview_id = page
1125 .webview_id()
1126 .ok_or_else(|| BrowserError::Init("page has no webview".into()))?;
1127
1128 let registered = register_native_host_functions(webview_id, stealth_profile);
1129
1130 let node_realm_registered = create_node_realm(webview_id);
1133 debug_assert!(
1134 node_realm_registered,
1135 "create_node_realm registration failed"
1136 );
1137
1138 page.drain_callbacks()?;
1146
1147 let page_global = get_page_global(webview_id);
1151 let node_global = get_node_realm_global(webview_id);
1152 page.set_page_global(page_global, node_global);
1153
1154 if !registered {
1155 page.evaluate_js_web(WEB_POLYFILLS)?;
1157 }
1158
1159 Ok(())
1160}
1161
1162fn register_native_host_functions(
1174 webview_id: servo::WebViewId,
1175 stealth_profile: Option<bao_stealth::StealthProfile>,
1176) -> bool {
1177 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1178 Box::new(move |cx_ptr, global_ptr| {
1179 unsafe {
1181 install_all_native(webview_id, cx_ptr, global_ptr, &stealth_profile);
1182 }
1183 });
1184
1185 servo::register_script_thread_callback(webview_id, callback);
1186 true
1187}
1188
1189unsafe fn install_all_native(
1212 webview_id: servo::WebViewId,
1213 cx_ptr: *mut std::ffi::c_void,
1214 global_ptr: *mut std::ffi::c_void,
1215 stealth_profile: &Option<bao_stealth::StealthProfile>,
1216) {
1217 use mozjs::context::JSContext;
1218 use mozjs::jsapi::{JSContext as RawJSContext, JSObject};
1219 use std::ptr::NonNull;
1220
1221 let raw_cx = cx_ptr as *mut RawJSContext;
1222 let raw_global = global_ptr as *mut JSObject;
1223
1224 if raw_cx.is_null() || raw_global.is_null() {
1225 return;
1226 }
1227
1228 PER_THREAD_PAGE_GLOBAL.with(|cell| {
1233 *cell.borrow_mut() = raw_global;
1234 });
1235
1236 page_global_by_webview().insert(webview_id, raw_global as usize);
1240
1241 if let Some(profile) = stealth_profile {
1243 bao_stealth::engine_props::set_profile_for_global(raw_global as usize, profile);
1244 bao_stealth::engine_props::set_profile(profile);
1245 bun_runtime::fetch_api::set_fetch_stealth_profile(Some(profile.clone()));
1246 servo::set_canvas_noise_seed(
1248 bao_stealth::engine_props::canvas_seed(),
1249 bao_stealth::engine_props::canvas_amplitude(),
1250 );
1251 let stc = bao_stealth::StealthTlsWireConfig::from_profile(profile);
1258 servo::set_stealth_tls_config(Some(servo::StealthTlsWireConfig {
1259 tls12_cipher_suites: stc.tls12_cipher_suites,
1260 tls13_cipher_suites: stc.tls13_cipher_suites,
1261 signature_algorithms: stc.signature_algorithms,
1262 supported_groups: stc.supported_groups,
1263 alpn_protocols: stc.alpn_protocols,
1264 h2_settings_payload: stc.h2_settings_payload,
1265 h2_initial_stream_size: stc.h2_initial_stream_size,
1266 h2_initial_connection_window_size: stc.h2_initial_connection_window_size,
1267 h2_max_frame_size: stc.h2_max_frame_size,
1268 h2_max_header_list_size: stc.h2_max_header_list_size,
1269 }));
1270 bao_stealth::set_global_http2_fingerprint(Some(&profile.http2));
1275 } else {
1276 bun_runtime::fetch_api::set_fetch_stealth_profile(None);
1277 servo::set_stealth_tls_config(None);
1278 bao_stealth::set_global_http2_fingerprint(None);
1279 }
1280
1281
1282 bao_stealth::engine_props::install_stealth_props(raw_cx, raw_global);
1284
1285 let cx_nn = match NonNull::new(raw_cx) {
1287 Some(nn) => nn,
1288 None => return,
1289 };
1290 let mut cx = JSContext::from_ptr(cx_nn);
1291 rooted!(in(raw_cx) let mut rooted_global = raw_global);
1292 let global_handle = rooted_global.handle();
1293
1294 bun_runtime::fetch_api::install_fetch_global(&mut cx, global_handle);
1296 bun_runtime::timers::install_timer_globals(&mut cx, global_handle);
1297 bun_runtime::web_api::install_performance(&mut cx, global_handle);
1298 bun_runtime::web_api::install_websocket_constructor(&mut cx, global_handle);
1299 bun_runtime::globals::install_crypto_global(&mut cx, global_handle);
1300 bun_runtime::web_api::install_web_encodings(&mut cx, global_handle);
1301 bun_runtime::web_api::install_atob_btoa(&mut cx, global_handle);
1302 bun_runtime::web_api::install_queue_microtask(&mut cx, global_handle);
1303 bun_runtime::globals::install_structured_clone(&mut cx, global_handle);
1304 bun_runtime::globals::install_web_api_constructors(&mut cx, global_handle);
1305 bun_runtime::web_fetch_classes::install_fetch_classes(&mut cx, global_handle);
1310
1311 {
1317 use mozjs::rust::CompileOptionsWrapper;
1318 let wasm_check = r#"(function(){ try { return typeof WebAssembly; } catch(e) { return 'undefined'; } })()"#;
1319 let c_filename = c"<wasm-init>".to_owned();
1320 let mut options = CompileOptionsWrapper::new(&mut cx, c_filename, 1);
1321 options.set_hide_script_from_debugger(true);
1324 rooted!(&in(cx) let mut wasm_rval = mozjs::jsval::UndefinedValue());
1325 let wasm_probe = mozjs::rust::evaluate_script(
1326 &mut cx,
1327 global_handle,
1328 wasm_check,
1329 wasm_rval.handle_mut(),
1330 options,
1331 );
1332 if wasm_probe.is_err() {
1346 mozjs::jsapi::JS_ClearPendingException(cx.raw_cx());
1347 }
1348 }
1349}
1350
1351pub fn inject_all(page: &PageHandle, stealth: bool) -> Result<(), BrowserError> {
1353 let profile = if stealth {
1354 page.stealth_profile()
1355 } else {
1356 None
1357 };
1358 inject_node_apis_with_stealth(page, profile)
1359}
1360
1361pub fn inject_all_with_profile(
1378 page: &PageHandle,
1379 profile: &Option<bao_stealth::StealthProfile>,
1380) -> Result<(), BrowserError> {
1381 inject_node_apis_with_stealth(page, profile.clone())?;
1382
1383 register_worker_scope_callback_native(profile.clone());
1390
1391 Ok(())
1392}
1393
1394pub fn register_worker_scope_callback_native(profile: Option<bao_stealth::StealthProfile>) {
1421 let config = match profile.as_ref() {
1426 Some(p) => crate::delegate::WorkerScopeConfig::from(p),
1427 None => crate::delegate::WorkerScopeConfig::default(),
1428 };
1429
1430 let callback: Box<dyn FnOnce(*mut std::ffi::c_void, *mut std::ffi::c_void) + Send> =
1431 Box::new(move |cx_ptr, global_ptr| {
1432 let raw_cx = cx_ptr as *mut mozjs::jsapi::JSContext;
1436 let raw_global = global_ptr as *mut mozjs::jsapi::JSObject;
1437 if raw_cx.is_null() || raw_global.is_null() {
1438 log::warn!(
1439 "[register_worker_scope_callback_native] NULL cx/global — \
1440 skipping Worker scope init (DEC-WK-001)"
1441 );
1442 return;
1443 }
1444 eprintln!(
1446 "[TASK-63-DIAG] worker scope callback FIRED (worker thread alive, scope created)"
1447 );
1448 log::debug!(
1449 "[register_worker_scope_callback_native] servo-native Worker \
1450 scope created — installing bao stealth + Web APIs (DEC-WK-001 / \
1451 DEC-WK-003 dual-track)"
1452 );
1453 unsafe {
1454 worker_scope_init_native(raw_cx, raw_global, &config);
1455 }
1456 });
1457
1458 servo::register_worker_scope_callback(callback);
1459}
1460
1461pub type WorkerScopeInitFn =
1485 Box<dyn FnOnce(*mut mozjs::jsapi::JSContext, *mut mozjs::jsapi::JSObject) + Send>;
1486
1487unsafe fn worker_scope_init_native(
1500 raw_cx: *mut mozjs::jsapi::JSContext,
1501 global: *mut mozjs::jsapi::JSObject,
1502 config: &crate::delegate::WorkerScopeConfig,
1503) {
1504 use mozjs::context::JSContext;
1505 use std::ptr::NonNull;
1506
1507 if raw_cx.is_null() || global.is_null() {
1508 return;
1509 }
1510
1511 if let Some(ref profile) = config.stealth_profile {
1519 bao_stealth::engine_props::set_profile_for_global(global as usize, profile);
1521 bao_stealth::engine_props::install_stealth_props(raw_cx, global);
1522 servo::set_canvas_noise_seed(
1524 bao_stealth::engine_props::canvas_seed(),
1525 bao_stealth::engine_props::canvas_amplitude(),
1526 );
1527 }
1528
1529 }
1534
1535const WEB_POLYFILLS: &str = r#"(function() {
1543 // @trace REQ-SEC-003 Web-only polyfills (no Node.js APIs)
1544
1545 // TextEncoder / TextDecoder
1546 if (typeof TextEncoder === 'undefined') {
1547 TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
1548 }
1549 if (typeof TextDecoder === 'undefined') {
1550 TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
1551 }
1552
1553 // URL / URLSearchParams
1554 if (typeof URL === 'undefined') {
1555 URL = function(url, base) { throw new Error('URL not available'); };
1556 }
1557 if (typeof URLSearchParams === 'undefined') {
1558 URLSearchParams = function(init) {
1559 this._params = [];
1560 this.append = function(k,v) { this._params.push([k,v]); };
1561 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; };
1562 this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
1563 };
1564 }
1565
1566 // btoa / atob
1567 if (typeof btoa === 'undefined') {
1568 var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
1569 btoa = function(str) {
1570 var out = '';
1571 for (var i = 0; i < str.length; i += 3) {
1572 var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
1573 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]);
1574 }
1575 return out;
1576 };
1577 atob = function(str) {
1578 var out = '';
1579 str = str.replace(/=+$/, '');
1580 for (var i = 0; i < str.length; i += 4) {
1581 var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
1582 var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
1583 out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
1584 }
1585 return out;
1586 };
1587 }
1588
1589 // setImmediate / clearImmediate (Web API extensions used by many libraries)
1590 if (typeof setImmediate === 'undefined') {
1591 setImmediate = function(fn) {
1592 var args = Array.prototype.slice.call(arguments, 1);
1593 return setTimeout(function() { fn.apply(null, args); }, 0);
1594 };
1595 clearImmediate = function(id) { clearTimeout(id); };
1596 }
1597})();"#;
1598
1599const NODE_POLYFILLS: &str = r#"(function() {
1600 // @trace REQ-ENG-007 Node.js API polyfills for Node Realm context
1601
1602 // global alias
1603 if (typeof global === 'undefined') {
1604 global = globalThis;
1605 }
1606
1607 // process
1608 if (typeof process === 'undefined') {
1609 process = {
1610 argv: ['bao', typeof __filename !== 'undefined' ? __filename : ''],
1611 argv0: 'bao',
1612 execArgv: [],
1613 execPath: '/usr/local/bin/bao',
1614 env: (function() {
1615 var e = {};
1616 if (typeof navigator !== 'undefined' && navigator.userAgent) {
1617 e.NODE_VERSION = '20.11.0';
1618 e.BAO_VERSION = '0.1.0';
1619 }
1620 e.HOME = '/';
1621 e.PATH = '/usr/local/bin:/usr/bin:/bin';
1622 e.TERM = 'xterm-256color';
1623 return e;
1624 })(),
1625 version: 'v20.11.0',
1626 versions: {
1627 node: '20.11.0',
1628 v8: '12.4.254.14',
1629 uv: '1.27.0',
1630 zlib: '1.2.13',
1631 brotli: '1.0.9',
1632 ares: '1.19.1',
1633 modules: '115',
1634 openssl: '3.0.12',
1635 icu: '74.2',
1636 bun: '1.0.25',
1637 bao: '0.1.0',
1638 },
1639 pid: 1,
1640 ppid: 0,
1641 title: 'bao',
1642 arch: (function() {
1643 if (typeof navigator !== 'undefined') {
1644 var p = navigator.platform || '';
1645 if (p.indexOf('Win') >= 0) return 'x64';
1646 if (p.indexOf('Mac') >= 0) return 'arm64';
1647 if (p.indexOf('Linux') >= 0) return 'x64';
1648 }
1649 return 'x64';
1650 })(),
1651 platform: (function() {
1652 if (typeof navigator !== 'undefined') {
1653 var p = navigator.platform || '';
1654 if (p.indexOf('Win') >= 0) return 'win32';
1655 if (p.indexOf('Mac') >= 0) return 'darwin';
1656 }
1657 return 'linux';
1658 })(),
1659 cwd: function() { return '/'; },
1660 chdir: function() {},
1661 exit: function(code) { throw new Error('process.exit(' + (code||0) + ')'); },
1662 hrtime: (function() {
1663 var origin = performance.now() * 1e-3;
1664 return function bigtime() {
1665 var diff = performance.now() * 1e-3 - origin;
1666 var sec = Math.floor(diff);
1667 var nsec = Math.round((diff - sec) * 1e9);
1668 if (arguments.length > 0) {
1669 sec += arguments[0][0];
1670 nsec += arguments[0][1];
1671 sec += Math.floor(nsec / 1e9);
1672 nsec = nsec % 1e9;
1673 if (nsec < 0) { nsec += 1e9; sec -= 1; }
1674 }
1675 var result = [sec, nsec];
1676 result.bigint = function() { return BigInt(sec) * 1000000000n + BigInt(nsec); };
1677 return result;
1678 };
1679 })(),
1680 uptime: function() { return performance.now() / 1000; },
1681 memoryUsage: function() {
1682 return { rss: 64*1024*1024, heapTotal: 32*1024*1024, heapUsed: 16*1024*1024, external: 2*1024*1024, arrayBuffers: 1*1024*1024 };
1683 },
1684 cpuUsage: function() { return { user: 100000, system: 50000 }; },
1685 nextTick: function(fn) {
1686 var args = Array.prototype.slice.call(arguments, 1);
1687 Promise.resolve().then(function() { fn.apply(null, args); });
1688 },
1689 binding: function(name) { return {}; },
1690 dlopen: function() { throw new Error('process.dlopen not available in browser context'); },
1691 stdout: { write: function(d) { console.log(d); return true; }, end: function() {} },
1692 stderr: { write: function(d) { console.error(d); return true; }, end: function() {} },
1693 stdin: { on: function() {}, resume: function() { return this; }, pipe: function() {} },
1694 on: function(event, fn) { return this; },
1695 off: function() {},
1696 once: function(event, fn) { return this; },
1697 emit: function(event) { return false; },
1698 removeAllListeners: function() { return this; },
1699 setUncaughtExceptionCallback: function() {},
1700 };
1701 }
1702
1703 // Buffer — browser-compatible implementation backed by Uint8Array
1704 if (typeof Buffer === 'undefined') {
1705 Buffer = (function() {
1706 function B(data, encoding) {
1707 if (!(this instanceof B)) return new B(data, encoding);
1708 if (data instanceof Uint8Array) {
1709 this._buf = new Uint8Array(data);
1710 } else if (data instanceof ArrayBuffer) {
1711 this._buf = new Uint8Array(data);
1712 } else if (typeof data === 'string') {
1713 this._buf = new Uint8Array(Array.from(data).map(function(c) { return c.charCodeAt(0); }));
1714 } else if (Array.isArray(data)) {
1715 this._buf = new Uint8Array(data);
1716 } else {
1717 this._buf = new Uint8Array(0);
1718 }
1719 this.length = this._buf.length;
1720 }
1721
1722 B.isBuffer = function(obj) { return obj instanceof B; };
1723
1724 B.from = function(data, encoding) {
1725 if (data instanceof B) return new B(data._buf);
1726 if (data instanceof Uint8Array) return new B(data);
1727 if (data instanceof ArrayBuffer) return new B(data);
1728 if (typeof data === 'string') {
1729 if (encoding === 'hex') {
1730 var bytes = [];
1731 for (var i = 0; i < data.length; i += 2) {
1732 bytes.push(parseInt(data.substr(i, 2), 16));
1733 }
1734 return new B(bytes);
1735 }
1736 if (encoding === 'base64') {
1737 var bin = atob(data);
1738 var bytes = [];
1739 for (var i = 0; i < bin.length; i++) bytes.push(bin.charCodeAt(i));
1740 return new B(bytes);
1741 }
1742 return new B(data);
1743 }
1744 return new B(data);
1745 };
1746
1747 B.alloc = function(size, fill, encoding) {
1748 var buf = new B(new Uint8Array(size));
1749 if (fill !== undefined) buf.fill(fill);
1750 return buf;
1751 };
1752
1753 B.allocUnsafe = function(size) {
1754 return new B(new Uint8Array(size));
1755 };
1756
1757 B.allocUnsafeSlow = function(size) {
1758 return new B(new Uint8Array(size));
1759 };
1760
1761 B.concat = function(list, totalLength) {
1762 if (!Array.isArray(list) || list.length === 0) return new B(new Uint8Array(0));
1763 var len = totalLength !== undefined ? totalLength : list.reduce(function(a, b) { return a + b.length; }, 0);
1764 var result = new Uint8Array(len);
1765 var offset = 0;
1766 for (var i = 0; i < list.length; i++) {
1767 var buf = list[i] instanceof B ? list[i]._buf : new Uint8Array(list[i]);
1768 result.set(buf, offset);
1769 offset += buf.length;
1770 }
1771 return new B(result);
1772 };
1773
1774 B.byteLength = function(str, encoding) {
1775 if (typeof str === 'string') {
1776 if (encoding === 'base64') return atob(str).length;
1777 if (encoding === 'hex') return str.length / 2;
1778 return new TextEncoder().encode(str).length;
1779 }
1780 if (str instanceof ArrayBuffer) return str.byteLength;
1781 if (str instanceof Uint8Array) return str.length;
1782 return 0;
1783 };
1784
1785 B.compare = function(a, b) {
1786 for (var i = 0; i < Math.min(a.length, b.length); i++) {
1787 if (a._buf[i] < b._buf[i]) return -1;
1788 if (a._buf[i] > b._buf[i]) return 1;
1789 }
1790 return a.length - b.length;
1791 };
1792
1793 B.prototype.slice = function(start, end) {
1794 return new B(this._buf.slice(start || 0, end));
1795 };
1796
1797 B.prototype.subarray = function(start, end) {
1798 return new B(this._buf.subarray(start || 0, end));
1799 };
1800
1801 B.prototype.toString = function(encoding, start, end) {
1802 var s = start || 0;
1803 var e = end !== undefined ? end : this._buf.length;
1804 var slice = this._buf.slice(s, e);
1805 if (encoding === 'hex') {
1806 return Array.from(slice).map(function(b) { return b.toString(16).padStart(2, '0'); }).join('');
1807 }
1808 if (encoding === 'base64') {
1809 var bin = Array.from(slice).map(function(b) { return String.fromCharCode(b); }).join('');
1810 return btoa(bin);
1811 }
1812 return new TextDecoder().decode(slice);
1813 };
1814
1815 B.prototype.toJSON = function() {
1816 return { type: 'Buffer', data: Array.from(this._buf) };
1817 };
1818
1819 B.prototype.equals = function(other) {
1820 if (!(other instanceof B) || this.length !== other.length) return false;
1821 for (var i = 0; i < this.length; i++) {
1822 if (this._buf[i] !== other._buf[i]) return false;
1823 }
1824 return true;
1825 };
1826
1827 B.prototype.compare = function(other, targetStart, targetEnd, sourceStart, sourceEnd) {
1828 var a = this._buf.slice(sourceStart || 0, sourceEnd);
1829 var b = other._buf.slice(targetStart || 0, targetEnd);
1830 for (var i = 0; i < Math.min(a.length, b.length); i++) {
1831 if (a[i] < b[i]) return -1;
1832 if (a[i] > b[i]) return 1;
1833 }
1834 return a.length - b.length;
1835 };
1836
1837 B.prototype.copy = function(target, targetStart, sourceStart, sourceEnd) {
1838 var src = this._buf.slice(sourceStart || 0, sourceEnd);
1839 for (var i = 0; i < src.length; i++) {
1840 if (target._buf) target._buf[targetStart + i] = src[i];
1841 }
1842 return src.length;
1843 };
1844
1845 B.prototype.fill = function(value, start, end) {
1846 var s = start || 0;
1847 var e = end !== undefined ? end : this._buf.length;
1848 var v = typeof value === 'number' ? value : 0;
1849 for (var i = s; i < e; i++) this._buf[i] = v;
1850 return this;
1851 };
1852
1853 B.prototype.write = function(str, offset, length, encoding) {
1854 var o = offset || 0;
1855 var bytes = new TextEncoder().encode(str);
1856 var len = Math.min(bytes.length, length !== undefined ? length : this._buf.length - o);
1857 for (var i = 0; i < len; i++) this._buf[o + i] = bytes[i];
1858 return len;
1859 };
1860
1861 B.prototype.includes = function(value, offset) {
1862 return this.indexOf(value, offset) !== -1;
1863 };
1864
1865 B.prototype.indexOf = function(value, offset) {
1866 var o = offset || 0;
1867 var search = typeof value === 'number' ? [value] : Array.from(new TextEncoder().encode(String(value)));
1868 for (var i = o; i <= this._buf.length - search.length; i++) {
1869 var found = true;
1870 for (var j = 0; j < search.length; j++) {
1871 if (this._buf[i + j] !== search[j]) { found = false; break; }
1872 }
1873 if (found) return i;
1874 }
1875 return -1;
1876 };
1877
1878 B.prototype.readUInt8 = function(offset) { return this._buf[offset || 0]; };
1879 B.prototype.readUInt16LE = function(offset) { var o = offset||0; return this._buf[o] | (this._buf[o+1]<<8); };
1880 B.prototype.readUInt16BE = function(offset) { var o = offset||0; return (this._buf[o]<<8) | this._buf[o+1]; };
1881 B.prototype.readUInt32LE = function(offset) {
1882 var o = offset||0;
1883 return (this._buf[o]) | (this._buf[o+1]<<8) | (this._buf[o+2]<<16) | (this._buf[o+3]<<24);
1884 };
1885 B.prototype.readInt8 = function(offset) { var v = this._buf[offset||0]; return v > 127 ? v - 256 : v; };
1886 B.prototype.readInt16LE = function(offset) { var v = this.readUInt16LE(offset); return v > 32767 ? v - 65536 : v; };
1887 B.prototype.readInt32LE = function(offset) { var v = this.readUInt32LE(offset); return v > 2147483647 ? v - 4294967296 : v; };
1888 B.prototype.readFloatLE = function(offset) {
1889 var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = 0;
1890 new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+4));
1891 return new Float32Array(buf)[0];
1892 };
1893 B.prototype.readDoubleLE = function(offset) {
1894 var buf = new ArrayBuffer(8);
1895 new Uint8Array(buf).set(this._buf.slice(offset||0, (offset||0)+8));
1896 return new Float64Array(buf)[0];
1897 };
1898
1899 B.prototype.writeUInt8 = function(v, offset) { this._buf[offset||0] = v & 0xFF; return (offset||0)+1; };
1900 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; };
1901 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; };
1902 B.prototype.writeInt8 = function(v, offset) { return this.writeUInt8(v < 0 ? v + 256 : v, offset); };
1903 B.prototype.writeInt16LE = function(v, offset) { return this.writeUInt16LE(v < 0 ? v + 65536 : v, offset); };
1904 B.prototype.writeInt32LE = function(v, offset) { return this.writeUInt32LE(v < 0 ? v + 4294967296 : v, offset); };
1905 B.prototype.writeFloatLE = function(v, offset) {
1906 var buf = new ArrayBuffer(4); new Float32Array(buf)[0] = v;
1907 this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+4;
1908 };
1909 B.prototype.writeDoubleLE = function(v, offset) {
1910 var buf = new ArrayBuffer(8); new Float64Array(buf)[0] = v;
1911 this._buf.set(new Uint8Array(buf), offset||0); return (offset||0)+8;
1912 };
1913
1914 B.prototype[Symbol.iterator] = function() {
1915 var idx = 0; var buf = this._buf;
1916 return { next: function() { return idx < buf.length ? { value: buf[idx++], done: false } : { done: true }; } };
1917 };
1918
1919 return B;
1920 })();
1921 }
1922
1923 // require — basic module loader for browser context
1924 if (typeof require === 'undefined') {
1925 var _module_cache = {};
1926 var _module_builtin = {
1927 'fs': { readFileSync: function() { throw new Error('fs not available in browser context'); }, existsSync: function() { return false; } },
1928 'path': {
1929 join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1930 resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1931 dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1932 basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1933 extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1934 sep: '/', delimiter: ':',
1935 posix: {
1936 join: function() { return Array.prototype.slice.call(arguments).join('/').replace(/\/+/g, '/'); },
1937 resolve: function() { var parts = Array.prototype.slice.call(arguments); return '/' + parts.join('/').replace(/\/+/g, '/'); },
1938 dirname: function(p) { return p.split('/').slice(0, -1).join('/') || '.'; },
1939 basename: function(p, ext) { var b = p.split('/').pop(); return ext && b.endsWith(ext) ? b.slice(0, -ext.length) : b; },
1940 extname: function(p) { var i = p.lastIndexOf('.'); return i >= 0 ? p.slice(i) : ''; },
1941 sep: '/', delimiter: ':',
1942 },
1943 win32: { sep: '\\', delimiter: ';' },
1944 },
1945 'url': {
1946 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 {}; } },
1947 format: function(u) { return typeof u === 'string' ? u : (u.protocol||'http:') + '//' + (u.host||u.hostname||'localhost') + (u.pathname||'/'); },
1948 resolve: function(from, to) { try { return new URL(to, from).href; } catch(e) { return to; } },
1949 URL: typeof URL !== 'undefined' ? URL : function() {},
1950 URLSearchParams: typeof URLSearchParams !== 'undefined' ? URLSearchParams : function() {},
1951 },
1952 'querystring': {
1953 parse: function(str, sep, eq) {
1954 sep = sep || '&'; eq = eq || '=';
1955 var obj = {};
1956 if (!str) return obj;
1957 str.split(sep).forEach(function(pair) {
1958 var idx = pair.indexOf(eq);
1959 var key = idx >= 0 ? pair.substring(0, idx) : pair;
1960 var val = idx >= 0 ? pair.substring(idx + 1) : '';
1961 obj[decodeURIComponent(key)] = decodeURIComponent(val);
1962 });
1963 return obj;
1964 },
1965 stringify: function(obj, sep, eq) {
1966 sep = sep || '&'; eq = eq || '=';
1967 return Object.keys(obj || {}).map(function(k) {
1968 return encodeURIComponent(k) + eq + encodeURIComponent(obj[k]);
1969 }).join(sep);
1970 },
1971 escape: encodeURIComponent,
1972 unescape: decodeURIComponent,
1973 },
1974 'events': {
1975 EventEmitter: (function() {
1976 function EE() { this._events = {}; }
1977 EE.prototype.on = function(e, fn) { (this._events[e] = this._events[e] || []).push(fn); return this; };
1978 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; };
1979 EE.prototype.off = function(e, fn) {
1980 if (!this._events[e]) return this;
1981 if (!fn) { delete this._events[e]; return this; }
1982 this._events[e] = this._events[e].filter(function(f) { return f !== fn && f._orig !== fn; });
1983 return this;
1984 };
1985 EE.prototype.emit = function(e) {
1986 var args = Array.prototype.slice.call(arguments, 1);
1987 (this._events[e] || []).forEach(function(fn) { fn.apply(null, args); });
1988 return this;
1989 };
1990 EE.prototype.removeListener = EE.prototype.off;
1991 EE.prototype.removeAllListeners = function(e) { if (e) delete this._events[e]; else this._events = {}; return this; };
1992 EE.prototype.listeners = function(e) { return this._events[e] || []; };
1993 EE.prototype.listenerCount = function(e) { return (this._events[e] || []).length; };
1994 return EE;
1995 })(),
1996 },
1997 'util': {
1998 inspect: function(obj) { return JSON.stringify(obj, null, 2); },
1999 inherits: function(ctor, superCtor) { ctor.prototype = Object.create(superCtor.prototype); ctor.prototype.constructor = ctor; },
2000 isFunction: function(v) { return typeof v === 'function'; },
2001 isNull: function(v) { return v === null; },
2002 isUndefined: function(v) { return v === undefined; },
2003 isObject: function(v) { return v !== null && typeof v === 'object'; },
2004 isString: function(v) { return typeof v === 'string'; },
2005 promisify: function(fn) {
2006 return function() {
2007 var args = Array.prototype.slice.call(arguments);
2008 return new Promise(function(resolve, reject) {
2009 args.push(function(err, result) { if (err) reject(err); else resolve(result); });
2010 fn.apply(null, args);
2011 });
2012 };
2013 },
2014 format: function(fmt) {
2015 var args = Array.prototype.slice.call(arguments, 1);
2016 return fmt.replace(/%[sdjifo]/g, function(m) { return args.length ? String(args.shift()) : m; });
2017 },
2018 types: {
2019 isDate: function(v) { return v instanceof Date; },
2020 isRegExp: function(v) { return v instanceof RegExp; },
2021 isArray: function(v) { return Array.isArray(v); },
2022 isPromise: function(v) { return v && typeof v.then === 'function'; },
2023 },
2024 },
2025 'stream': { Readable: function(){}, Writable: function(){}, Duplex: function(){}, Transform: function(){} },
2026 'buffer': { Buffer: typeof Buffer !== 'undefined' ? Buffer : function(){} },
2027 'crypto': {
2028 randomBytes: function(size, cb) {
2029 var arr = new Uint8Array(size);
2030 if (typeof crypto !== 'undefined' && crypto.getRandomValues) crypto.getRandomValues(arr);
2031 if (cb) cb(null, Buffer.from(arr));
2032 return Buffer.from(arr);
2033 },
2034 createHash: function(algo) {
2035 var chunks = [];
2036 return {
2037 update: function(data) { chunks.push(typeof data === 'string' ? data : String(data)); return this; },
2038 digest: function(enc) {
2039 var str = chunks.join('');
2040 if (typeof crypto !== 'undefined' && crypto.subtle) {
2041 return crypto.subtle.digest('SHA-256', new TextEncoder().encode(str)).then(function(buf) {
2042 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);
2043 });
2044 }
2045 return enc === 'hex' ? '00000000' : Buffer.alloc(0);
2046 },
2047 };
2048 },
2049 },
2050 'os': {
2051 platform: function() { return 'linux'; },
2052 arch: function() { return 'x64'; },
2053 homedir: function() { return '/'; },
2054 tmpdir: function() { return '/tmp'; },
2055 type: function() { return 'Linux'; },
2056 release: function() { return '6.8.0'; },
2057 hostname: function() { return 'bao'; },
2058 cpus: function() { return [{ model: 'bao', speed: 3000 }]; },
2059 totalmem: function() { return 8*1024*1024*1024; },
2060 freemem: function() { return 4*1024*1024*1024; },
2061 uptime: function() { return 3600; },
2062 EOL: '\n',
2063 },
2064 'assert': {
2065 ok: function(v, msg) { if (!v) throw new Error(msg || 'assertion failed'); },
2066 equal: function(a, b, msg) { if (a !== b) throw new Error(msg || a + ' !== ' + b); },
2067 deepEqual: function(a, b, msg) { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(msg || 'not deep equal'); },
2068 throws: function(fn, msg) { try { fn(); throw new Error(msg || 'expected throw'); } catch(e) { if (e.message === (msg || 'expected throw')) throw e; } },
2069 },
2070 'timers': {
2071 setTimeout: typeof setTimeout !== 'undefined' ? setTimeout : function(fn) { fn(); return 0; },
2072 setInterval: typeof setInterval !== 'undefined' ? setInterval : function(fn) { return 0; },
2073 clearTimeout: typeof clearTimeout !== 'undefined' ? clearTimeout : function() {},
2074 clearInterval: typeof clearInterval !== 'undefined' ? clearInterval : function() {},
2075 setImmediate: typeof setImmediate !== 'undefined' ? setImmediate : function(fn) { return setTimeout(fn, 0); },
2076 clearImmediate: typeof clearImmediate !== 'undefined' ? clearImmediate : function() {},
2077 },
2078 };
2079
2080 require = function(name) {
2081 if (_module_cache[name]) return _module_cache[name];
2082 if (_module_builtin[name]) { _module_cache[name] = _module_builtin[name]; return _module_builtin[name]; }
2083 throw new Error("Cannot find module '" + name + "' in browser context");
2084 };
2085
2086 require.resolve = function(name) { return name; };
2087 require.cache = _module_cache;
2088 }
2089
2090 // setImmediate / clearImmediate
2091 if (typeof setImmediate === 'undefined') {
2092 setImmediate = function(fn) {
2093 var args = Array.prototype.slice.call(arguments, 1);
2094 return setTimeout(function() { fn.apply(null, args); }, 0);
2095 };
2096 clearImmediate = function(id) { clearTimeout(id); };
2097 }
2098
2099 // __dirname / __filename
2100 if (typeof __dirname === 'undefined') {
2101 __dirname = '/';
2102 __filename = '/index.js';
2103 }
2104
2105 // TextEncoder / TextDecoder (most browsers have these, but ensure)
2106 if (typeof TextEncoder === 'undefined') {
2107 TextEncoder = function() { this.encode = function(str) { return new Uint8Array(Array.from(str).map(function(c){return c.charCodeAt(0);})); }; };
2108 }
2109 if (typeof TextDecoder === 'undefined') {
2110 TextDecoder = function() { this.decode = function(buf) { return String.fromCharCode.apply(null, buf); }; };
2111 }
2112
2113 // URL / URLSearchParams (most browsers have these, but ensure)
2114 if (typeof URL === 'undefined') {
2115 URL = function(url, base) { throw new Error('URL not available'); };
2116 }
2117 if (typeof URLSearchParams === 'undefined') {
2118 URLSearchParams = function(init) {
2119 this._params = [];
2120 this.append = function(k,v) { this._params.push([k,v]); };
2121 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; };
2122 this.toString = function() { return this._params.map(function(p){return p[0]+'='+p[1];}).join('&'); };
2123 };
2124 }
2125
2126 // btoa / atob (most browsers have these, but ensure)
2127 if (typeof btoa === 'undefined') {
2128 var _b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
2129 btoa = function(str) {
2130 var out = '';
2131 for (var i = 0; i < str.length; i += 3) {
2132 var a = str.charCodeAt(i), b = str.charCodeAt(i+1), c = str.charCodeAt(i+2);
2133 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]);
2134 }
2135 return out;
2136 };
2137 atob = function(str) {
2138 var out = '';
2139 str = str.replace(/=+$/, '');
2140 for (var i = 0; i < str.length; i += 4) {
2141 var a = _b64chars.indexOf(str[i]), b = _b64chars.indexOf(str[i+1]);
2142 var c = _b64chars.indexOf(str[i+2]), d = _b64chars.indexOf(str[i+3]);
2143 out += String.fromCharCode((a<<2)|(b>>4)) + (c>=0?String.fromCharCode(((b&15)<<4)|(c>>2)):'') + (d>=0?String.fromCharCode(((c&3)<<6)|d):'');
2144 }
2145 return out;
2146 };
2147 }
2148})();"#;
2149
2150#[derive(Debug, Clone, PartialEq, Eq)]
2160pub enum BridgeCommand {
2161 Navigate(String),
2163 Evaluate(String),
2165 Screenshot,
2167 Close,
2169 Resize(u32, u32),
2171 GetTitle,
2173 GetUrl,
2175}
2176
2177#[derive(Debug, Clone, PartialEq, Eq)]
2181pub enum BridgeResponse {
2182 Ok,
2184 Err(String),
2186 Null,
2188 Value(String),
2190 Binary(Vec<u8>),
2192}
2193
2194impl BridgeResponse {
2195 pub fn is_ok(&self) -> bool {
2197 matches!(self, BridgeResponse::Ok)
2198 }
2199
2200 pub fn is_err(&self) -> bool {
2202 matches!(self, BridgeResponse::Err(_))
2203 }
2204
2205 pub fn ok(self) -> Result<Self, String> {
2208 match self {
2209 BridgeResponse::Err(e) => Err(e),
2210 other => Ok(other),
2211 }
2212 }
2213}
2214
2215pub struct BridgeReceiver {
2221 rx: mpsc::Receiver<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2222 alive: Arc<AtomicBool>,
2223}
2224
2225impl std::fmt::Debug for BridgeReceiver {
2226 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2227 f.debug_struct("BridgeReceiver")
2228 .field("alive", &self.alive)
2229 .finish()
2230 }
2231}
2232
2233impl BridgeReceiver {
2234 pub fn recv(&self) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2236 self.rx.recv().map_err(|_| "channel closed".to_string())
2237 }
2238
2239 pub fn recv_timeout(
2241 &self,
2242 timeout: Duration,
2243 ) -> Result<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>), String> {
2244 self.rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2245 }
2246
2247 pub fn is_alive(&self) -> bool {
2249 self.alive.load(Ordering::SeqCst)
2250 }
2251}
2252
2253#[derive(Debug)]
2260pub struct BridgeChannel {
2261 tx: mpsc::Sender<(BridgeCommand, Option<mpsc::Sender<BridgeResponse>>)>,
2262 alive: Arc<AtomicBool>,
2263}
2264
2265impl BridgeChannel {
2266 pub fn new() -> (Self, BridgeReceiver) {
2271 let (tx, rx) = mpsc::channel();
2272 let alive = Arc::new(AtomicBool::new(true));
2273 let channel = BridgeChannel {
2274 tx,
2275 alive: alive.clone(),
2276 };
2277 let receiver = BridgeReceiver { rx, alive };
2278 (channel, receiver)
2279 }
2280
2281 pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2283 let (resp_tx, resp_rx) = mpsc::channel();
2284 self.tx
2285 .send((cmd, Some(resp_tx)))
2286 .map_err(|_| "bridge closed".to_string())?;
2287 resp_rx
2288 .recv()
2289 .map_err(|_| "response channel closed".to_string())
2290 }
2291
2292 pub fn send_timeout(
2294 &self,
2295 cmd: BridgeCommand,
2296 timeout: Duration,
2297 ) -> Result<BridgeResponse, String> {
2298 let (resp_tx, resp_rx) = mpsc::channel();
2299 self.tx
2300 .send((cmd, Some(resp_tx)))
2301 .map_err(|_| "bridge closed".to_string())?;
2302 resp_rx.recv_timeout(timeout).map_err(|e| format!("{}", e))
2303 }
2304
2305 pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2310 self.tx
2311 .send((cmd, None))
2312 .map_err(|_| "bridge closed".to_string())
2313 }
2314
2315 pub fn is_alive(&self) -> bool {
2317 self.alive.load(Ordering::SeqCst)
2318 }
2319
2320 pub fn close(&self) {
2326 self.alive.store(false, Ordering::SeqCst);
2327 }
2328}
2329
2330#[derive(Debug)]
2339pub struct RuntimeBridge {
2340 channel: BridgeChannel,
2341}
2342
2343impl RuntimeBridge {
2344 pub fn new() -> (Self, BridgeReceiver) {
2346 let (channel, receiver) = BridgeChannel::new();
2347 (RuntimeBridge { channel }, receiver)
2348 }
2349
2350 pub fn send(&self, cmd: BridgeCommand) -> Result<BridgeResponse, String> {
2352 self.channel.send(cmd)
2353 }
2354
2355 pub fn send_timeout(
2358 &self,
2359 cmd: BridgeCommand,
2360 timeout: Duration,
2361 ) -> Result<BridgeResponse, String> {
2362 self.channel.send_timeout(cmd, timeout)
2363 }
2364
2365 pub fn fire_and_forget(&self, cmd: BridgeCommand) -> Result<(), String> {
2368 self.channel.fire_and_forget(cmd)
2369 }
2370
2371 pub fn is_alive(&self) -> bool {
2373 self.channel.is_alive()
2374 }
2375
2376 pub fn close(&self) {
2378 self.channel.close();
2379 }
2380}
2381
2382#[cfg(test)]
2383mod tests {
2384 use std::sync::OnceLock;
2385 #[test]
2389 fn test_polyfills_are_valid_js() {
2390 assert!(!super::NODE_POLYFILLS.is_empty());
2391 assert!(super::NODE_POLYFILLS.contains("Buffer"));
2392 assert!(super::NODE_POLYFILLS.contains("require"));
2393 assert!(super::NODE_POLYFILLS.contains("process"));
2394 }
2395
2396 #[test]
2400 fn bridge_command_navigate_equality() {
2401 let cmd1 = super::BridgeCommand::Navigate("https://example.com".into());
2402 let cmd2 = super::BridgeCommand::Navigate("https://example.com".into());
2403 let cmd3 = super::BridgeCommand::Navigate("https://other.com".into());
2404 assert_eq!(cmd1, cmd2);
2405 assert_ne!(cmd1, cmd3);
2406 }
2407
2408 #[test]
2409 fn bridge_command_evaluate_equality() {
2410 let cmd1 = super::BridgeCommand::Evaluate("1+1".into());
2411 let cmd2 = super::BridgeCommand::Evaluate("1+1".into());
2412 assert_eq!(cmd1, cmd2);
2413 assert_ne!(cmd1, super::BridgeCommand::Evaluate("2+2".into()));
2414 }
2415
2416 #[test]
2417 fn bridge_command_resize_equality() {
2418 assert_eq!(
2419 super::BridgeCommand::Resize(800, 600),
2420 super::BridgeCommand::Resize(800, 600)
2421 );
2422 assert_ne!(
2423 super::BridgeCommand::Resize(800, 600),
2424 super::BridgeCommand::Resize(1024, 768)
2425 );
2426 }
2427
2428 #[test]
2429 fn bridge_command_variants_distinct() {
2430 let cmds = [
2431 super::BridgeCommand::Navigate("x".into()),
2432 super::BridgeCommand::Evaluate("y".into()),
2433 super::BridgeCommand::Screenshot,
2434 super::BridgeCommand::Close,
2435 super::BridgeCommand::Resize(1, 1),
2436 super::BridgeCommand::GetTitle,
2437 super::BridgeCommand::GetUrl,
2438 ];
2439 for i in 0..cmds.len() {
2440 for j in 0..cmds.len() {
2441 if i != j {
2442 assert_ne!(cmds[i], cmds[j]);
2443 }
2444 }
2445 }
2446 }
2447
2448 #[test]
2449 fn bridge_response_ok_is_ok() {
2450 let resp = super::BridgeResponse::Ok;
2451 assert!(resp.is_ok());
2452 assert!(!resp.is_err());
2453 }
2454
2455 #[test]
2456 fn bridge_response_err_is_err() {
2457 let resp = super::BridgeResponse::Err("failed".into());
2458 assert!(!resp.is_ok());
2459 assert!(resp.is_err());
2460 }
2461
2462 #[test]
2463 fn bridge_response_null_not_err() {
2464 let resp = super::BridgeResponse::Null;
2465 assert!(!resp.is_ok()); assert!(!resp.is_err()); }
2468
2469 #[test]
2470 fn bridge_response_value_not_err() {
2471 let resp = super::BridgeResponse::Value("result".into());
2472 assert!(!resp.is_ok()); assert!(!resp.is_err());
2474 }
2475
2476 #[test]
2477 fn bridge_response_binary_not_err() {
2478 let resp = super::BridgeResponse::Binary(vec![1, 2, 3]);
2479 assert!(!resp.is_ok()); assert!(!resp.is_err());
2481 }
2482
2483 #[test]
2484 fn bridge_response_ok_method_wraps_non_err() {
2485 assert!(super::BridgeResponse::Null.ok().is_ok());
2487 assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2488 assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2489 }
2490
2491 #[test]
2492 fn bridge_response_ok_method_on_err() {
2493 let resp = super::BridgeResponse::Err("error msg".into());
2494 let result = resp.ok();
2495 assert!(result.is_err());
2496 assert_eq!(result.unwrap_err(), "error msg");
2497 }
2498
2499 #[test]
2500 fn bridge_response_ok_method_on_ok_variants() {
2501 assert!(super::BridgeResponse::Ok.ok().is_ok());
2502 assert!(super::BridgeResponse::Null.ok().is_ok());
2503 assert!(super::BridgeResponse::Value("v".into()).ok().is_ok());
2504 assert!(super::BridgeResponse::Binary(vec![]).ok().is_ok());
2505 }
2506
2507 #[test]
2508 fn bridge_channel_new_alive() {
2509 let (channel, _receiver) = super::BridgeChannel::new();
2510 assert!(channel.is_alive());
2511 }
2512
2513 #[test]
2514 fn bridge_channel_close_sets_not_alive() {
2515 let (channel, _receiver) = super::BridgeChannel::new();
2516 channel.close();
2517 assert!(!channel.is_alive());
2518 }
2519
2520 #[test]
2521 fn bridge_receiver_alive_shares_flag() {
2522 let (channel, receiver) = super::BridgeChannel::new();
2523 assert!(receiver.is_alive());
2524 channel.close();
2525 assert!(!receiver.is_alive());
2526 }
2527
2528 #[test]
2529 fn bridge_channel_fire_and_forget() {
2530 let (channel, receiver) = super::BridgeChannel::new();
2531 assert!(channel
2532 .fire_and_forget(super::BridgeCommand::GetTitle)
2533 .is_ok());
2534 let (cmd, responder) = receiver.recv().unwrap();
2535 assert_eq!(cmd, super::BridgeCommand::GetTitle);
2536 assert!(responder.is_none());
2537 }
2538
2539 #[test]
2540 fn bridge_channel_send_with_response() {
2541 let (channel, receiver) = super::BridgeChannel::new();
2542 let worker = std::thread::spawn(move || {
2544 let (_cmd, responder) = receiver.recv().unwrap();
2545 if let Some(resp_tx) = responder {
2546 resp_tx
2547 .send(super::BridgeResponse::Value("title".into()))
2548 .unwrap();
2549 }
2550 });
2551 let result = channel.send(super::BridgeCommand::GetTitle).unwrap();
2552 assert_eq!(result, super::BridgeResponse::Value("title".into()));
2553 worker.join().unwrap();
2554 }
2555
2556 #[test]
2557 fn runtime_bridge_new_alive() {
2558 let (bridge, _receiver) = super::RuntimeBridge::new();
2559 assert!(bridge.is_alive());
2560 }
2561
2562 #[test]
2563 fn runtime_bridge_close() {
2564 let (bridge, _receiver) = super::RuntimeBridge::new();
2565 bridge.close();
2566 assert!(!bridge.is_alive());
2567 }
2568
2569 #[test]
2570 fn runtime_bridge_fire_and_forget() {
2571 let (bridge, receiver) = super::RuntimeBridge::new();
2572 assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2573 let (cmd, responder) = receiver.recv().unwrap();
2574 assert_eq!(cmd, super::BridgeCommand::Close);
2575 assert!(responder.is_none());
2576 }
2577
2578 #[test]
2586 fn bridge_command_debug_format_navigate() {
2587 let cmd = super::BridgeCommand::Navigate("https://example.com".into());
2588 let debug_str = format!("{:?}", cmd);
2589 assert!(debug_str.contains("Navigate"));
2590 assert!(debug_str.contains("https://example.com"));
2591 }
2592
2593 #[test]
2594 fn bridge_command_debug_format_evaluate() {
2595 let cmd = super::BridgeCommand::Evaluate("return 42".into());
2596 let debug_str = format!("{:?}", cmd);
2597 assert!(debug_str.contains("Evaluate"));
2598 assert!(debug_str.contains("return 42"));
2599 }
2600
2601 #[test]
2602 fn bridge_command_debug_format_screenshot() {
2603 let cmd = super::BridgeCommand::Screenshot;
2604 let debug_str = format!("{:?}", cmd);
2605 assert!(debug_str.contains("Screenshot"));
2606 }
2607
2608 #[test]
2609 fn bridge_command_debug_format_close() {
2610 let cmd = super::BridgeCommand::Close;
2611 let debug_str = format!("{:?}", cmd);
2612 assert!(debug_str.contains("Close"));
2613 }
2614
2615 #[test]
2616 fn bridge_command_debug_format_resize() {
2617 let cmd = super::BridgeCommand::Resize(1920, 1080);
2618 let debug_str = format!("{:?}", cmd);
2619 assert!(debug_str.contains("Resize"));
2620 assert!(debug_str.contains("1920"));
2621 assert!(debug_str.contains("1080"));
2622 }
2623
2624 #[test]
2625 fn bridge_command_debug_format_get_title() {
2626 let cmd = super::BridgeCommand::GetTitle;
2627 let debug_str = format!("{:?}", cmd);
2628 assert!(debug_str.contains("GetTitle"));
2629 }
2630
2631 #[test]
2632 fn bridge_command_debug_format_get_url() {
2633 let cmd = super::BridgeCommand::GetUrl;
2634 let debug_str = format!("{:?}", cmd);
2635 assert!(debug_str.contains("GetUrl"));
2636 }
2637
2638 #[test]
2641 fn bridge_command_clone_navigate() {
2642 let cmd = super::BridgeCommand::Navigate("https://test.com".into());
2643 let cloned = cmd.clone();
2644 assert_eq!(cmd, cloned);
2645 }
2646
2647 #[test]
2648 fn bridge_command_clone_evaluate() {
2649 let cmd = super::BridgeCommand::Evaluate("x + y".into());
2650 let cloned = cmd.clone();
2651 assert_eq!(cmd, cloned);
2652 }
2653
2654 #[test]
2655 fn bridge_command_clone_resize() {
2656 let cmd = super::BridgeCommand::Resize(1024, 768);
2657 let cloned = cmd.clone();
2658 assert_eq!(cmd, cloned);
2659 }
2660
2661 #[test]
2664 fn bridge_response_debug_format_ok() {
2665 let resp = super::BridgeResponse::Ok;
2666 let debug_str = format!("{:?}", resp);
2667 assert!(debug_str.contains("Ok"));
2668 }
2669
2670 #[test]
2671 fn bridge_response_debug_format_err() {
2672 let resp = super::BridgeResponse::Err("something went wrong".into());
2673 let debug_str = format!("{:?}", resp);
2674 assert!(debug_str.contains("Err"));
2675 assert!(debug_str.contains("something went wrong"));
2676 }
2677
2678 #[test]
2679 fn bridge_response_debug_format_null() {
2680 let resp = super::BridgeResponse::Null;
2681 let debug_str = format!("{:?}", resp);
2682 assert!(debug_str.contains("Null"));
2683 }
2684
2685 #[test]
2686 fn bridge_response_debug_format_value() {
2687 let resp = super::BridgeResponse::Value("result string".into());
2688 let debug_str = format!("{:?}", resp);
2689 assert!(debug_str.contains("Value"));
2690 assert!(debug_str.contains("result string"));
2691 }
2692
2693 #[test]
2694 fn bridge_response_debug_format_binary() {
2695 let resp = super::BridgeResponse::Binary(vec![0xDE, 0xAD, 0xBE, 0xEF]);
2696 let debug_str = format!("{:?}", resp);
2697 assert!(debug_str.contains("Binary"));
2698 }
2699
2700 #[test]
2701 fn bridge_response_clone_ok() {
2702 let resp = super::BridgeResponse::Ok;
2703 let cloned = resp.clone();
2704 assert_eq!(resp, cloned);
2705 }
2706
2707 #[test]
2708 fn bridge_response_clone_err() {
2709 let resp = super::BridgeResponse::Err("error".into());
2710 let cloned = resp.clone();
2711 assert_eq!(resp, cloned);
2712 }
2713
2714 #[test]
2715 fn bridge_response_clone_value() {
2716 let resp = super::BridgeResponse::Value("value".into());
2717 let cloned = resp.clone();
2718 assert_eq!(resp, cloned);
2719 }
2720
2721 #[test]
2722 fn bridge_response_clone_binary() {
2723 let resp = super::BridgeResponse::Binary(vec![1, 2, 3, 4]);
2724 let cloned = resp.clone();
2725 assert_eq!(resp, cloned);
2726 }
2727
2728 #[test]
2729 fn bridge_response_equality_ok() {
2730 assert_eq!(super::BridgeResponse::Ok, super::BridgeResponse::Ok);
2731 }
2732
2733 #[test]
2734 fn bridge_response_equality_err() {
2735 assert_eq!(
2736 super::BridgeResponse::Err("same error".into()),
2737 super::BridgeResponse::Err("same error".into())
2738 );
2739 assert_ne!(
2740 super::BridgeResponse::Err("error a".into()),
2741 super::BridgeResponse::Err("error b".into())
2742 );
2743 }
2744
2745 #[test]
2746 fn bridge_response_equality_value() {
2747 assert_eq!(
2748 super::BridgeResponse::Value("same".into()),
2749 super::BridgeResponse::Value("same".into())
2750 );
2751 assert_ne!(
2752 super::BridgeResponse::Value("a".into()),
2753 super::BridgeResponse::Value("b".into())
2754 );
2755 }
2756
2757 #[test]
2758 fn bridge_response_equality_binary() {
2759 assert_eq!(
2760 super::BridgeResponse::Binary(vec![1, 2, 3]),
2761 super::BridgeResponse::Binary(vec![1, 2, 3])
2762 );
2763 assert_ne!(
2764 super::BridgeResponse::Binary(vec![1, 2, 3]),
2765 super::BridgeResponse::Binary(vec![1, 2, 4])
2766 );
2767 }
2768
2769 #[test]
2770 fn bridge_response_variants_distinct() {
2771 let responses = [
2772 super::BridgeResponse::Ok,
2773 super::BridgeResponse::Err("e".into()),
2774 super::BridgeResponse::Null,
2775 super::BridgeResponse::Value("v".into()),
2776 super::BridgeResponse::Binary(vec![1]),
2777 ];
2778 for i in 0..responses.len() {
2779 for j in 0..responses.len() {
2780 if i != j {
2781 assert_ne!(responses[i], responses[j]);
2782 }
2783 }
2784 }
2785 }
2786
2787 #[test]
2790 fn bridge_channel_send_timeout_zero_timeout_returns_err() {
2791 let (channel, receiver) = super::BridgeChannel::new();
2794 let _drainer = std::thread::spawn(move || {
2796 let _ = receiver.recv();
2798 });
2799 let result = channel.send_timeout(
2800 super::BridgeCommand::GetTitle,
2801 std::time::Duration::from_secs(0),
2802 );
2803 assert!(result.is_err());
2804 }
2805
2806 #[test]
2807 fn bridge_channel_send_timeout_short_timeout() {
2808 let (channel, _receiver) = super::BridgeChannel::new();
2809 let result = channel.send_timeout(
2811 super::BridgeCommand::GetTitle,
2812 std::time::Duration::from_millis(1),
2813 );
2814 assert!(result.is_err());
2815 }
2816
2817 #[test]
2818 fn bridge_channel_fire_and_forget_multiple() {
2819 let (channel, receiver) = super::BridgeChannel::new();
2820 assert!(channel
2821 .fire_and_forget(super::BridgeCommand::GetTitle)
2822 .is_ok());
2823 assert!(channel
2824 .fire_and_forget(super::BridgeCommand::GetUrl)
2825 .is_ok());
2826 assert!(channel
2827 .fire_and_forget(super::BridgeCommand::Screenshot)
2828 .is_ok());
2829
2830 let (cmd1, _) = receiver.recv().unwrap();
2831 let (cmd2, _) = receiver.recv().unwrap();
2832 let (cmd3, _) = receiver.recv().unwrap();
2833
2834 assert_eq!(cmd1, super::BridgeCommand::GetTitle);
2835 assert_eq!(cmd2, super::BridgeCommand::GetUrl);
2836 assert_eq!(cmd3, super::BridgeCommand::Screenshot);
2837 }
2838
2839 #[test]
2840 fn bridge_channel_close_then_send_fails() {
2841 let (channel, receiver) = super::BridgeChannel::new();
2842 channel.close();
2843 assert!(!channel.is_alive());
2847 drop(receiver);
2849 let result = channel.send(super::BridgeCommand::GetTitle);
2851 assert!(result.is_err());
2852 }
2853
2854 #[test]
2855 fn bridge_channel_close_then_fire_and_forget_fails() {
2856 let (channel, receiver) = super::BridgeChannel::new();
2857 channel.close();
2858 drop(receiver);
2860 let result = channel.fire_and_forget(super::BridgeCommand::Close);
2861 assert!(result.is_err());
2862 }
2863
2864 #[test]
2865 fn bridge_channel_multiple_send_response_pairs() {
2866 let (channel, receiver) = super::BridgeChannel::new();
2867
2868 let worker = std::thread::spawn(move || {
2869 for _ in 0..3 {
2870 let (cmd, responder) = receiver.recv().unwrap();
2871 if let Some(resp_tx) = responder {
2872 let resp = match cmd {
2873 super::BridgeCommand::GetTitle => {
2874 super::BridgeResponse::Value("Title".into())
2875 }
2876 super::BridgeCommand::GetUrl => {
2877 super::BridgeResponse::Value("https://url.com".into())
2878 }
2879 _ => super::BridgeResponse::Ok,
2880 };
2881 resp_tx.send(resp).unwrap();
2882 }
2883 }
2884 });
2885
2886 let r1 = channel.send(super::BridgeCommand::GetTitle).unwrap();
2887 let r2 = channel.send(super::BridgeCommand::GetUrl).unwrap();
2888 let r3 = channel.send(super::BridgeCommand::Screenshot).unwrap();
2889
2890 assert_eq!(r1, super::BridgeResponse::Value("Title".into()));
2891 assert_eq!(r2, super::BridgeResponse::Value("https://url.com".into()));
2892 assert_eq!(r3, super::BridgeResponse::Ok);
2893
2894 worker.join().unwrap();
2895 }
2896
2897 #[test]
2900 fn bridge_receiver_recv_timeout_short() {
2901 let (_channel, receiver) = super::BridgeChannel::new();
2902 let result = receiver.recv_timeout(std::time::Duration::from_millis(1));
2904 assert!(result.is_err());
2905 }
2906
2907 #[test]
2908 fn bridge_receiver_recv_after_channel_dropped() {
2909 let (channel, receiver) = super::BridgeChannel::new();
2910 drop(channel);
2911 let result = receiver.recv();
2913 assert!(result.is_err());
2914 assert_eq!(result.unwrap_err(), "channel closed");
2915 }
2916
2917 #[test]
2918 fn bridge_receiver_debug_format() {
2919 let (_channel, receiver) = super::BridgeChannel::new();
2920 let debug_str = format!("{:?}", receiver);
2921 assert!(debug_str.contains("BridgeReceiver"));
2922 assert!(debug_str.contains("alive"));
2923 }
2924
2925 #[test]
2928 fn runtime_bridge_send_timeout() {
2929 let (bridge, receiver) = super::RuntimeBridge::new();
2930
2931 let worker = std::thread::spawn(move || {
2932 let (cmd, responder) = receiver.recv().unwrap();
2933 if let Some(resp_tx) = responder {
2934 let resp = match cmd {
2935 super::BridgeCommand::Evaluate(ref code) => {
2936 super::BridgeResponse::Value(format!("evaluated: {}", code))
2937 }
2938 _ => super::BridgeResponse::Ok,
2939 };
2940 resp_tx.send(resp).unwrap();
2941 }
2942 });
2943
2944 let result = bridge
2945 .send_timeout(
2946 super::BridgeCommand::Evaluate("1+1".into()),
2947 std::time::Duration::from_secs(5),
2948 )
2949 .unwrap();
2950 assert_eq!(
2951 result,
2952 super::BridgeResponse::Value("evaluated: 1+1".into())
2953 );
2954
2955 worker.join().unwrap();
2956 }
2957
2958 #[test]
2959 fn runtime_bridge_close_propagates() {
2960 let (bridge, receiver) = super::RuntimeBridge::new();
2961 assert!(bridge.is_alive());
2962 assert!(receiver.is_alive());
2963 bridge.close();
2964 assert!(!bridge.is_alive());
2965 assert!(!receiver.is_alive());
2966 }
2967
2968 #[test]
2969 fn runtime_bridge_fire_and_forget_after_close_still_works() {
2970 let (bridge, receiver) = super::RuntimeBridge::new();
2971 bridge.close();
2972 assert!(bridge.fire_and_forget(super::BridgeCommand::Close).is_ok());
2975 let (cmd, responder) = receiver.recv().unwrap();
2976 assert_eq!(cmd, super::BridgeCommand::Close);
2977 assert!(responder.is_none());
2978 }
2979
2980 #[test]
2981 fn runtime_bridge_send_after_receiver_dropped() {
2982 let (bridge, receiver) = super::RuntimeBridge::new();
2983 drop(receiver);
2984 let result = bridge.send(super::BridgeCommand::GetTitle);
2985 assert!(result.is_err());
2986 }
2987
2988 #[test]
2989 fn runtime_bridge_debug_format() {
2990 let (bridge, _receiver) = super::RuntimeBridge::new();
2991 let debug_str = format!("{:?}", bridge);
2992 assert!(debug_str.contains("RuntimeBridge"));
2993 }
2994
2995 #[test]
2998 fn node_polyfills_process_version() {
2999 let poly = super::NODE_POLYFILLS;
3000 assert!(poly.contains("version: 'v20.11.0'"));
3001 }
3002
3003 #[test]
3004 fn node_polyfills_process_versions_structure() {
3005 let poly = super::NODE_POLYFILLS;
3006 assert!(poly.contains("node: '20.11.0'"));
3008 assert!(poly.contains("v8: '12.4.254.14'"));
3009 assert!(poly.contains("uv: '1.27.0'"));
3010 assert!(poly.contains("zlib: '1.2.13'"));
3011 assert!(poly.contains("brotli: '1.0.9'"));
3012 assert!(poly.contains("ares: '1.19.1'"));
3013 assert!(poly.contains("modules: '115'"));
3014 assert!(poly.contains("openssl: '3.0.12'"));
3015 assert!(poly.contains("icu: '74.2'"));
3016 assert!(poly.contains("bun: '1.0.25'"));
3017 assert!(poly.contains("bao: '0.1.0'"));
3018 }
3019
3020 #[test]
3021 fn node_polyfills_process_env() {
3022 let poly = super::NODE_POLYFILLS;
3023 assert!(poly.contains("env:"));
3024 assert!(poly.contains("e.HOME = '/'"));
3025 assert!(poly.contains("e.PATH = '/usr/local/bin:/usr/bin:/bin'"));
3026 assert!(poly.contains("e.TERM = 'xterm-256color'"));
3027 assert!(poly.contains("e.NODE_VERSION = '20.11.0'"));
3028 assert!(poly.contains("e.BAO_VERSION = '0.1.0'"));
3029 }
3030
3031 #[test]
3032 fn node_polyfills_process_argv() {
3033 let poly = super::NODE_POLYFILLS;
3034 assert!(poly.contains("argv:"));
3035 assert!(poly.contains("argv0: 'bao'"));
3036 }
3037
3038 #[test]
3039 fn node_polyfills_buffer_from() {
3040 let poly = super::NODE_POLYFILLS;
3041 assert!(poly.contains("B.from = function"));
3042 assert!(poly.contains("if (data instanceof B)"));
3043 assert!(poly.contains("if (encoding === 'hex')"));
3044 assert!(poly.contains("if (encoding === 'base64')"));
3045 }
3046
3047 #[test]
3048 fn node_polyfills_buffer_alloc() {
3049 let poly = super::NODE_POLYFILLS;
3050 assert!(poly.contains("B.alloc = function"));
3051 assert!(poly.contains("B.allocUnsafe = function"));
3052 assert!(poly.contains("B.allocUnsafeSlow = function"));
3053 }
3054
3055 #[test]
3056 fn node_polyfills_buffer_static_methods() {
3057 let poly = super::NODE_POLYFILLS;
3058 assert!(poly.contains("B.isBuffer = function"));
3059 assert!(poly.contains("B.concat = function"));
3060 assert!(poly.contains("B.byteLength = function"));
3061 assert!(poly.contains("B.compare = function"));
3062 }
3063
3064 #[test]
3065 fn node_polyfills_buffer_instance_methods() {
3066 let poly = super::NODE_POLYFILLS;
3067 assert!(poly.contains("B.prototype.slice = function"));
3068 assert!(poly.contains("B.prototype.toString = function"));
3069 assert!(poly.contains("B.prototype.toJSON = function"));
3070 assert!(poly.contains("B.prototype.equals = function"));
3071 assert!(poly.contains("B.prototype.compare = function"));
3072 assert!(poly.contains("B.prototype.copy = function"));
3073 assert!(poly.contains("B.prototype.fill = function"));
3074 assert!(poly.contains("B.prototype.write = function"));
3075 assert!(poly.contains("B.prototype.indexOf = function"));
3076 }
3077
3078 #[test]
3079 fn node_polyfills_buffer_read_methods() {
3080 let poly = super::NODE_POLYFILLS;
3081 assert!(poly.contains("B.prototype.readUInt8 = function"));
3082 assert!(poly.contains("B.prototype.readUInt16LE = function"));
3083 assert!(poly.contains("B.prototype.readUInt16BE = function"));
3084 assert!(poly.contains("B.prototype.readUInt32LE = function"));
3085 assert!(poly.contains("B.prototype.readInt8 = function"));
3086 assert!(poly.contains("B.prototype.readInt16LE = function"));
3087 assert!(poly.contains("B.prototype.readInt32LE = function"));
3088 assert!(poly.contains("B.prototype.readFloatLE = function"));
3089 assert!(poly.contains("B.prototype.readDoubleLE = function"));
3090 }
3091
3092 #[test]
3093 fn node_polyfills_buffer_write_methods() {
3094 let poly = super::NODE_POLYFILLS;
3095 assert!(poly.contains("B.prototype.writeUInt8 = function"));
3096 assert!(poly.contains("B.prototype.writeUInt16LE = function"));
3097 assert!(poly.contains("B.prototype.writeUInt32LE = function"));
3098 assert!(poly.contains("B.prototype.writeInt8 = function"));
3099 assert!(poly.contains("B.prototype.writeInt16LE = function"));
3100 assert!(poly.contains("B.prototype.writeInt32LE = function"));
3101 assert!(poly.contains("B.prototype.writeFloatLE = function"));
3102 assert!(poly.contains("B.prototype.writeDoubleLE = function"));
3103 }
3104
3105 #[test]
3106 fn node_polyfills_require_cache() {
3107 let poly = super::NODE_POLYFILLS;
3108 assert!(poly.contains("require.cache = _module_cache"));
3109 assert!(poly.contains("_module_cache = {}"));
3110 }
3111
3112 #[test]
3113 fn node_polyfills_require_builtin_modules() {
3114 let poly = super::NODE_POLYFILLS;
3115 assert!(poly.contains("'fs':"));
3117 assert!(poly.contains("'path':"));
3118 assert!(poly.contains("'url':"));
3119 assert!(poly.contains("'querystring':"));
3120 assert!(poly.contains("'events':"));
3121 assert!(poly.contains("'util':"));
3122 assert!(poly.contains("'stream':"));
3123 assert!(poly.contains("'buffer':"));
3124 assert!(poly.contains("'crypto':"));
3125 assert!(poly.contains("'os':"));
3126 assert!(poly.contains("'assert':"));
3127 assert!(poly.contains("'timers':"));
3128 }
3129
3130 #[test]
3131 fn node_polyfills_path_module() {
3132 let poly = super::NODE_POLYFILLS;
3133 assert!(poly.contains("join: function"));
3134 assert!(poly.contains("resolve: function"));
3135 assert!(poly.contains("dirname: function"));
3136 assert!(poly.contains("basename: function"));
3137 assert!(poly.contains("extname: function"));
3138 assert!(poly.contains("sep: '/'"));
3139 assert!(poly.contains("posix:"));
3140 assert!(poly.contains("win32:"));
3141 }
3142
3143 #[test]
3144 fn node_polyfills_global_alias() {
3145 let poly = super::NODE_POLYFILLS;
3146 assert!(poly.contains("global = globalThis"));
3147 }
3148
3149 #[test]
3150 fn node_polyfills_text_encoder_decoder() {
3151 let poly = super::NODE_POLYFILLS;
3152 assert!(poly.contains("TextEncoder"));
3153 assert!(poly.contains("TextDecoder"));
3154 }
3155
3156 #[test]
3157 fn node_polyfills_btoa_atob() {
3158 let poly = super::NODE_POLYFILLS;
3159 assert!(poly.contains("btoa = function"));
3160 assert!(poly.contains("atob = function"));
3161 assert!(poly.contains("_b64chars"));
3162 }
3163
3164 #[test]
3167 fn bridge_command_empty_navigate_url() {
3168 let cmd = super::BridgeCommand::Navigate("".into());
3169 let cloned = cmd.clone();
3170 assert_eq!(cmd, cloned);
3171 let debug_str = format!("{:?}", cmd);
3172 assert!(debug_str.contains("Navigate"));
3173 }
3174
3175 #[test]
3176 fn bridge_command_empty_evaluate_string() {
3177 let cmd = super::BridgeCommand::Evaluate("".into());
3178 let cloned = cmd.clone();
3179 assert_eq!(cmd, cloned);
3180 let debug_str = format!("{:?}", cmd);
3181 assert!(debug_str.contains("Evaluate"));
3182 }
3183
3184 #[test]
3185 fn bridge_response_empty_value() {
3186 let resp = super::BridgeResponse::Value("".into());
3187 assert!(!resp.is_ok());
3188 assert!(!resp.is_err());
3189 let result = resp.ok();
3190 assert!(result.is_ok());
3191 assert_eq!(result.unwrap(), super::BridgeResponse::Value("".into()));
3192 }
3193
3194 #[test]
3195 fn bridge_response_empty_binary() {
3196 let resp = super::BridgeResponse::Binary(vec![]);
3197 assert!(!resp.is_ok());
3198 assert!(!resp.is_err());
3199 let cloned = resp.clone();
3200 assert_eq!(resp, cloned);
3201 }
3202
3203 #[test]
3204 fn bridge_response_large_binary_payload() {
3205 let large_data: Vec<u8> = (0..=255).cycle().take(1024 * 1024).collect();
3207 let resp = super::BridgeResponse::Binary(large_data.clone());
3208 assert!(!resp.is_ok());
3209 assert!(!resp.is_err());
3210 let cloned = resp.clone();
3211 assert_eq!(resp, cloned);
3212 if let super::BridgeResponse::Binary(data) = cloned {
3214 assert_eq!(data.len(), 1024 * 1024);
3215 assert_eq!(data[0], 0);
3216 assert_eq!(data[255], 255);
3217 assert_eq!(data[256], 0); } else {
3219 panic!("Expected Binary variant");
3220 }
3221 }
3222
3223 #[test]
3224 fn bridge_command_unicode_navigate_url() {
3225 let unicode_url = "https://例子.测试/路径?查询=值#片段";
3226 let cmd = super::BridgeCommand::Navigate(unicode_url.into());
3227 let cloned = cmd.clone();
3228 assert_eq!(cmd, cloned);
3229 let debug_str = format!("{:?}", cmd);
3230 assert!(debug_str.contains(unicode_url));
3231 }
3232
3233 #[test]
3234 fn bridge_command_unicode_evaluate_string() {
3235 let unicode_code = "console.log('你好世界 🎉')";
3236 let cmd = super::BridgeCommand::Evaluate(unicode_code.into());
3237 let cloned = cmd.clone();
3238 assert_eq!(cmd, cloned);
3239 let debug_str = format!("{:?}", cmd);
3240 assert!(debug_str.contains(unicode_code));
3241 }
3242
3243 #[test]
3244 fn bridge_response_unicode_value() {
3245 let unicode_value = "结果: 成功 ✅ 日本語 한국어 العربية";
3246 let resp = super::BridgeResponse::Value(unicode_value.into());
3247 let cloned = resp.clone();
3248 assert_eq!(resp, cloned);
3249 let debug_str = format!("{:?}", resp);
3250 assert!(debug_str.contains(unicode_value));
3251 }
3252
3253 #[test]
3254 fn bridge_response_unicode_error() {
3255 let unicode_error = "错误: 文件未找到 📁❌";
3256 let resp = super::BridgeResponse::Err(unicode_error.into());
3257 assert!(resp.is_err());
3258 let result = resp.ok();
3259 assert!(result.is_err());
3260 assert_eq!(result.unwrap_err(), unicode_error);
3261 }
3262
3263 #[test]
3264 fn bridge_channel_debug_format() {
3265 let (channel, _receiver) = super::BridgeChannel::new();
3266 let debug_str = format!("{:?}", channel);
3267 assert!(debug_str.contains("BridgeChannel"));
3268 assert!(debug_str.contains("alive"));
3269 }
3270
3271 #[test]
3277 fn runtime_bridge_calls_web_apis_not_install_all() {
3278 let source = include_str!("runtime_bridge.rs");
3279 let func_start = source
3280 .find("unsafe fn install_all_native")
3281 .expect("install_all_native function not found");
3282 let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3284
3285 assert!(
3286 func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3287 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3288 );
3289 assert!(
3290 func_body.contains("bun_runtime::timers::install_timer_globals"),
3291 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3292 );
3293 assert!(
3294 !func_body.contains("globals::install_all("),
3295 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3296 );
3297 assert!(
3298 !func_body.contains("globals::install_node_apis("),
3299 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3300 );
3301 }
3302
3303 #[test]
3311 fn worker_scope_init_native_is_stealth_only() {
3312 let source = include_str!("runtime_bridge.rs");
3313 let func_start = source
3314 .find("unsafe fn worker_scope_init_native")
3315 .expect("worker_scope_init_native function not found");
3316 let search_end = source[func_start..]
3318 .find("\n// @trace REQ-SEC-003 [entity:WebPolyfills]")
3319 .or_else(|| source[func_start..].find("\nconst WEB_POLYFILLS"))
3320 .unwrap_or(5000)
3321 .min(5000);
3322 let func_body = &source[func_start..func_start + search_end];
3323
3324 assert!(
3326 func_body.contains("install_stealth_props"),
3327 "BCE-20260627-009 REGRESSION: worker_scope_init_native must install stealth props (DEC-WK-007)"
3328 );
3329
3330 assert!(
3332 !func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3333 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install fetch (servo native)"
3334 );
3335 assert!(
3336 !func_body.contains("bun_runtime::timers::install_timer_globals"),
3337 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install timers (servo native)"
3338 );
3339 assert!(
3340 !func_body.contains("bun_runtime::web_api::install_performance"),
3341 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install performance (servo native)"
3342 );
3343 assert!(
3344 !func_body.contains("bun_runtime::globals::install_crypto_global"),
3345 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install crypto (servo native)"
3346 );
3347 assert!(
3348 !func_body.contains("bun_runtime::globals::install_structured_clone"),
3349 "BCE-20260627-009 REGRESSION: worker_scope_init_native must NOT install structuredClone (servo native)"
3350 );
3351 }
3352
3353 #[test]
3356 fn runtime_bridge_node_realm_uses_new_compartment() {
3357 let source = include_str!("runtime_bridge.rs");
3358
3359 let func_start = source
3360 .find("unsafe fn create_node_realm_native")
3361 .expect("create_node_realm_native function not found");
3362 let func_body_start = source[func_start..]
3363 .find("{")
3364 .expect("function body start not found");
3365 let search_limit = source[func_start + func_body_start..]
3366 .find("pub fn inject_node_apis")
3367 .or_else(|| {
3368 source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3369 })
3370 .unwrap_or(3000)
3371 .min(3000);
3372 let func_body =
3373 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3374
3375 assert!(
3376 func_body.contains("NewCompartmentAndZone"),
3377 "REQ-SEC-002 REGRESSION: create_node_realm_native must use NewCompartmentAndZone"
3378 );
3379 assert!(
3380 func_body.contains("SIMPLE_GLOBAL_CLASS"),
3381 "REQ-SEC-002 REGRESSION: create_node_realm_native must use SIMPLE_GLOBAL_CLASS"
3382 );
3383 assert!(
3384 func_body.contains("AutoRealm::new_from_handle"),
3385 "REQ-SEC-002 REGRESSION: create_node_realm_native must use AutoRealm"
3386 );
3387 assert!(
3388 func_body.contains("bun_runtime::globals::install_node_apis"),
3389 "REQ-SEC-002 REGRESSION: Node APIs must be installed on Node Realm global"
3390 );
3391 }
3392
3393 #[test]
3396 fn runtime_bridge_evaluate_in_node_realm_uses_auto_realm() {
3397 let source = include_str!("runtime_bridge.rs");
3398
3399 let func_start = source
3400 .find("pub unsafe fn evaluate_in_node_realm")
3401 .expect("evaluate_in_node_realm function not found");
3402 let func_body_start = source[func_start..]
3403 .find("{")
3404 .expect("function body start not found");
3405 let search_limit = source[func_start + func_body_start..]
3406 .find("unsafe fn create_node_realm_native")
3407 .unwrap_or(3000)
3408 .min(3000);
3409 let func_body =
3410 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3411
3412 assert!(
3413 func_body.contains("AutoRealm::new"),
3414 "REQ-SEC-002 REGRESSION: evaluate_in_node_realm must use AutoRealm"
3415 );
3416 assert!(
3417 func_body.contains("evaluate_script"),
3418 "REQ-SEC-002: evaluate_in_node_realm must call evaluate_script"
3419 );
3420 }
3421
3422 #[test]
3429 fn runtime_bridge_has_per_page_node_realm_storage() {
3430 let source = include_str!("runtime_bridge.rs");
3431 assert!(
3432 source.contains("NODE_REALM_BY_WEBVIEW"),
3433 "REQ-SEC-002 REGRESSION: must have NODE_REALM_BY_WEBVIEW per-page storage"
3434 );
3435 assert!(
3436 source.contains("store_node_realm"),
3437 "REQ-SEC-002 REGRESSION: must have store_node_realm accessor"
3438 );
3439 assert!(
3440 source.contains("get_node_realm_by_id"),
3441 "REQ-SEC-002 REGRESSION: must have get_node_realm_by_id accessor (WebViewId-keyed)"
3442 );
3443 assert!(
3444 source.contains("get_node_realm_global"),
3445 "REQ-SEC-002 REGRESSION: must have get_node_realm_global accessor"
3446 );
3447 for line in source.lines() {
3453 let trimmed = line.trim_start();
3454 assert!(
3457 !(trimmed.starts_with("static NODE_REALMS:")
3458 && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
3459 "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed NODE_REALMS must not exist"
3460 );
3461 assert!(
3462 !(trimmed.starts_with("static PAGE_GLOBALS:")
3463 && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
3464 "BCE-20260621-001 REGRESSION: cross-thread *mut JSObject-keyed PAGE_GLOBALS must not exist"
3465 );
3466 assert!(
3467 !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
3468 "BCE-20260621-001 REGRESSION: process-wide LAST_PAGE_GLOBAL must not exist"
3469 );
3470 }
3471 }
3472
3473 #[test]
3477 fn runtime_bridge_drain_uses_callbacks_method() {
3478 let source = include_str!("runtime_bridge.rs");
3479
3480 let func_start = source
3481 .find("pub fn inject_node_apis_with_stealth")
3482 .expect("inject_node_apis_with_stealth function not found");
3483 let func_end = source[func_start..]
3484 .find("fn register_native_host_functions")
3485 .expect("end boundary not found");
3486 let func_body = &source[func_start..func_start + func_end];
3487
3488 assert!(
3489 func_body.contains("drain_callbacks"),
3490 "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must use drain_callbacks (not evaluate_js)"
3491 );
3492 assert!(
3493 !func_body.contains("page.evaluate_js(\""),
3494 "REQ-SEC-002 REGRESSION: inject_node_apis_with_stealth must NOT call evaluate_js with string arg (would cause recursion)"
3495 );
3496 assert!(
3497 !func_body.contains("let _"),
3498 "REQ-SEC-003 REGRESSION: inject_node_apis_with_stealth must NOT swallow errors with let _"
3499 );
3500 }
3501
3502 #[test]
3504 fn node_polyfills_contains_security_sensitive_names() {
3505 let poly = super::NODE_POLYFILLS;
3506 assert!(
3507 poly.contains("require"),
3508 "NODE_POLYFILLS must contain 'require'"
3509 );
3510 assert!(
3511 poly.contains("Buffer"),
3512 "NODE_POLYFILLS must contain 'Buffer'"
3513 );
3514 assert!(
3515 poly.contains("process"),
3516 "NODE_POLYFILLS must contain 'process'"
3517 );
3518 }
3519
3520 #[test]
3525 fn web_polyfills_excludes_node_apis() {
3526 let poly = super::WEB_POLYFILLS;
3527 assert!(
3528 !poly.contains("require"),
3529 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'require'"
3530 );
3531 assert!(
3532 !poly.contains("Buffer"),
3533 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Buffer'"
3534 );
3535 assert!(
3536 !poly.contains("process"),
3537 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'process'"
3538 );
3539 assert!(
3540 !poly.contains("Bun"),
3541 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'Bun'"
3542 );
3543 assert!(
3544 !poly.contains("module"),
3545 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain 'module'"
3546 );
3547 assert!(
3548 !poly.contains("__dirname"),
3549 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__dirname'"
3550 );
3551 assert!(
3552 !poly.contains("__filename"),
3553 "REQ-SEC-003 REGRESSION: WEB_POLYFILLS must NOT contain '__filename'"
3554 );
3555 }
3556
3557 #[test]
3560 fn web_polyfills_includes_web_apis() {
3561 let poly = super::WEB_POLYFILLS;
3562 assert!(
3563 poly.contains("TextEncoder"),
3564 "WEB_POLYFILLS must contain TextEncoder"
3565 );
3566 assert!(
3567 poly.contains("TextDecoder"),
3568 "WEB_POLYFILLS must contain TextDecoder"
3569 );
3570 assert!(poly.contains("URL"), "WEB_POLYFILLS must contain URL");
3571 assert!(
3572 poly.contains("URLSearchParams"),
3573 "WEB_POLYFILLS must contain URLSearchParams"
3574 );
3575 assert!(poly.contains("btoa"), "WEB_POLYFILLS must contain btoa");
3576 assert!(poly.contains("atob"), "WEB_POLYFILLS must contain atob");
3577 }
3578
3579 #[test]
3582 fn fallback_uses_web_polyfills_not_node_polyfills() {
3583 let source = include_str!("runtime_bridge.rs");
3584
3585 let func_start = source
3586 .find("pub fn inject_node_apis_with_stealth")
3587 .expect("inject_node_apis_with_stealth function not found");
3588 let func_end = source[func_start..]
3589 .find("fn register_native_host_functions")
3590 .expect("end boundary not found");
3591 let func_body = &source[func_start..func_start + func_end];
3592
3593 assert!(
3594 func_body.contains("WEB_POLYFILLS"),
3595 "REQ-SEC-003 REGRESSION: fallback must use WEB_POLYFILLS (not NODE_POLYFILLS)"
3596 );
3597 let fallback_section = func_body
3599 .find("if !registered")
3600 .map(|i| &func_body[i..])
3601 .unwrap_or("");
3602 assert!(
3603 !fallback_section.contains("NODE_POLYFILLS"),
3604 "REQ-SEC-003 REGRESSION: fallback path must NOT reference NODE_POLYFILLS"
3605 );
3606 }
3607
3608 #[test]
3611 fn install_all_native_web_apis_only() {
3612 let source = include_str!("runtime_bridge.rs");
3613
3614 let func_start = source
3615 .find("unsafe fn install_all_native")
3616 .expect("install_all_native function not found");
3617 let func_body_start = source[func_start..]
3618 .find("{")
3619 .expect("function body start not found");
3620 let search_limit = source[func_start + func_body_start..]
3621 .find("const NODE_POLYFILLS")
3622 .or_else(|| {
3623 source[func_start + func_body_start..].find("/// Inject Node.js APIs as native")
3624 })
3625 .unwrap_or(5000)
3626 .min(5000);
3627 let func_body =
3628 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3629
3630 assert!(
3631 func_body.contains("bun_runtime::fetch_api::install_fetch_global"),
3632 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (fetch)"
3633 );
3634 assert!(
3635 func_body.contains("bun_runtime::timers::install_timer_globals"),
3636 "REQ-SEC-003 REGRESSION: install_all_native must install Web APIs (timers)"
3637 );
3638 assert!(
3639 !func_body.contains("globals::install_all("),
3640 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_all()"
3641 );
3642 assert!(
3643 !func_body.contains("globals::install_node_apis("),
3644 "REQ-SEC-003 REGRESSION: install_all_native must NOT call install_node_apis()"
3645 );
3646 }
3647
3648 #[test]
3651 fn node_apis_installed_in_node_realm_only() {
3652 let source = include_str!("runtime_bridge.rs");
3653
3654 let func_start = source
3655 .find("unsafe fn create_node_realm_native")
3656 .expect("create_node_realm_native function not found");
3657 let func_body_start = source[func_start..]
3658 .find("{")
3659 .expect("function body start not found");
3660 let search_limit = source[func_start + func_body_start..]
3661 .find("unsafe fn wrap_and_install_dom_proxy")
3662 .or_else(|| source[func_start + func_body_start..].find("/// Wrap a DOM property"))
3663 .unwrap_or(3000)
3664 .min(3000);
3665 let func_body =
3666 &source[func_start + func_body_start..func_start + func_body_start + search_limit];
3667
3668 assert!(
3669 func_body.contains("bun_runtime::globals::install_node_apis"),
3670 "REQ-SEC-003 REGRESSION: Node Realm must install Node APIs (install_node_apis)"
3671 );
3672 assert!(
3673 func_body.contains("NewCompartmentAndZone"),
3674 "REQ-SEC-003 REGRESSION: Node Realm must be isolated via NewCompartmentAndZone"
3675 );
3676 }
3677
3678 #[test]
3680 fn web_polyfills_is_valid_js() {
3681 let poly = super::WEB_POLYFILLS;
3682 assert!(
3683 poly.starts_with("(function()"),
3684 "WEB_POLYFILLS must be an IIFE"
3685 );
3686 assert!(poly.ends_with("})();"), "WEB_POLYFILLS must close IIFE");
3687 }
3688
3689 #[test]
3692 fn remove_node_realm_by_id_is_safe_no_op() {
3693 let _guard = super::test_serial_lock().lock().unwrap();
3698 super::clear_all_node_realms();
3699 let _f: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3705 }
3706
3707 #[test]
3710 fn webview_id_keyed_storage_api_exists() {
3711 let _store: fn(servo::WebViewId, *mut mozjs::jsapi::JSObject, *mut mozjs::jsapi::JSObject) =
3713 super::store_node_realm;
3714 let _get_node: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3715 super::get_node_realm_by_id;
3716 let _get_page: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3717 super::get_page_global_by_id;
3718 let _get_node_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3719 super::get_node_realm_global;
3720 let _get_page_global: fn(servo::WebViewId) -> *mut mozjs::jsapi::JSObject =
3721 super::get_page_global;
3722 let _remove: fn(servo::WebViewId) = super::remove_node_realm_by_id;
3723 }
3724
3725 #[test]
3728 fn clear_all_removes_all_entries() {
3729 let _guard = super::test_serial_lock().lock().unwrap();
3730 super::clear_all_node_realms();
3732 super::clear_all_node_realms();
3733 }
3734
3735 #[test]
3737 fn lazy_dom_getters_are_valid_jsnative() {
3738 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_window);
3740 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_document);
3741 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_navigator);
3742 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_worker);
3744 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_shared_worker);
3745 let _: mozjs::jsapi::JSNative = Some(super::lazy_dom_getter_service_worker);
3746 }
3747
3748 #[test]
3754 fn worker_constructor_getters_use_permanent_attribute() {
3755 let source = include_str!("runtime_bridge.rs");
3756 assert!(
3758 source.contains("ctor_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY | mozjs::jsapi::JSPROP_PERMANENT)"),
3759 "REQ-BRW-004 REGRESSION: Worker/SharedWorker/ServiceWorker constructors must use JSPROP_PERMANENT"
3760 );
3761 assert!(
3763 source.contains("obj_attrs = (mozjs::jsapi::JSPROP_ENUMERATE | mozjs::jsapi::JSPROP_READONLY)"),
3764 "REQ-BRW-004 REGRESSION: window/document/navigator getters should use obj_attrs without JSPROP_PERMANENT"
3765 );
3766 assert!(
3768 source.contains("(c\"Worker\", Some(lazy_dom_getter_worker))"),
3769 "REQ-BRW-004 REGRESSION: Worker must be in ctor_getters"
3770 );
3771 assert!(
3772 source.contains("(c\"SharedWorker\", Some(lazy_dom_getter_shared_worker))"),
3773 "REQ-BRW-004 REGRESSION: SharedWorker must be in ctor_getters"
3774 );
3775 assert!(
3776 source.contains("(c\"ServiceWorker\", Some(lazy_dom_getter_service_worker))"),
3777 "REQ-BRW-004 REGRESSION: ServiceWorker must be in ctor_getters"
3778 );
3779 }
3780
3781 #[test]
3795 fn constructor_getter_reads_from_page_realm_not_node_realm() {
3796 let source = include_str!("runtime_bridge.rs");
3797 let func_start = source
3798 .find("unsafe fn lazy_constructor_getter_impl")
3799 .expect("lazy_constructor_getter_impl not found");
3800 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3801
3802 assert!(
3804 func_body.contains("PER_THREAD_PAGE_GLOBAL.with"),
3805 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must read Page Realm global from PER_THREAD_PAGE_GLOBAL"
3806 );
3807 assert!(
3809 func_body.contains("page_global_root.handle()"),
3810 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must JS_GetProperty from page_global_root (Page Realm)"
3811 );
3812 }
3813
3814 #[test]
3824 fn constructor_getter_uses_js_wrap_object_for_cross_compartment_proxy() {
3825 let source = include_str!("runtime_bridge.rs");
3826 let func_start = source
3827 .find("unsafe fn lazy_constructor_getter_impl")
3828 .expect("lazy_constructor_getter_impl not found");
3829 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3830
3831 assert!(
3832 func_body.contains("JS_WrapObject"),
3833 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call JS_WrapObject for cross-Compartment proxy"
3834 );
3835 }
3836
3837 #[test]
3847 fn constructor_getter_validates_is_constructor_before_wrapping() {
3848 let source = include_str!("runtime_bridge.rs");
3849 let func_start = source
3850 .find("unsafe fn lazy_constructor_getter_impl")
3851 .expect("lazy_constructor_getter_impl not found");
3852 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3853
3854 assert!(
3855 func_body.contains("mozjs::jsapi::IsConstructor"),
3856 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call IsConstructor to validate the constructor"
3857 );
3858 }
3859
3860 #[test]
3870 fn constructor_getter_throws_reference_error_on_failure() {
3871 let source = include_str!("runtime_bridge.rs");
3872 let func_start = source
3873 .find("unsafe fn lazy_constructor_getter_impl")
3874 .expect("lazy_constructor_getter_impl not found");
3875 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3876
3877 assert!(
3879 func_body.contains("report_reference_error"),
3880 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must call report_reference_error for error reporting"
3881 );
3882 assert!(
3884 func_body.contains("property_name"),
3885 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl error messages must reference property_name"
3886 );
3887 }
3888
3889 #[test]
3899 fn constructor_getter_throws_on_null_page_global() {
3900 let source = include_str!("runtime_bridge.rs");
3901 let func_start = source
3902 .find("unsafe fn lazy_constructor_getter_impl")
3903 .expect("lazy_constructor_getter_impl not found");
3904 let func_body = &source[func_start..func_start + 3000.min(source.len() - func_start)];
3905
3906 assert!(
3908 func_body.contains("page_global.is_null()"),
3909 "DF-WK-11 REGRESSION: lazy_constructor_getter_impl must check page_global.is_null()"
3910 );
3911 let page_global_null_pos = func_body
3915 .find("page_global.is_null()")
3916 .expect("DF-WK-11: page_global.is_null() check not found");
3917 let after_null_check = &func_body[page_global_null_pos..page_global_null_pos + 300];
3918 assert!(
3919 after_null_check.contains("report_reference_error"),
3920 "DF-WK-11 REGRESSION: null page_global must trigger report_reference_error, not silent return"
3921 );
3922 }
3923
3924 #[test]
3934 fn install_lazy_dom_getters_uses_js_define_property1_for_constructors() {
3935 let source = include_str!("runtime_bridge.rs");
3939 assert!(
3940 source.find("unsafe fn install_lazy_dom_getters").is_some(),
3941 "install_lazy_dom_getters not found"
3942 );
3943
3944 assert!(
3946 source.contains("JS_DefineProperty1"),
3947 "DF-WK-11 REGRESSION: install_lazy_dom_getters must use JS_DefineProperty1 for property registration"
3948 );
3949 assert!(
3951 source.contains("for &(name, getter) in ctor_getters"),
3952 "DF-WK-11 REGRESSION: ctor_getters must be registered with their own (PERMANENT) attributes"
3953 );
3954 }
3955
3956 #[test]
3967 fn lazy_dom_getters_installed_on_node_realm_not_page_realm() {
3968 let source = include_str!("runtime_bridge.rs");
3969 let func_start = source
3970 .find("unsafe fn create_node_realm_native")
3971 .expect("create_node_realm_native not found");
3972 let func_body = &source[func_start..func_start + 5000.min(source.len() - func_start)];
3974
3975 assert!(
3976 func_body.contains("install_lazy_dom_getters(realm_cx, global.handle())"),
3977 "DF-WK-11 REGRESSION: install_lazy_dom_getters must be called from create_node_realm_native on the Node Realm global"
3978 );
3979 assert!(
3982 func_body.contains("JS_NewGlobalObject") && func_body.contains("NewCompartmentAndZone"),
3983 "DF-WK-11 REGRESSION: create_node_realm_native must create Node Realm with NewCompartmentAndZone before installing lazy getters"
3984 );
3985 }
3986
3987 #[test]
3996 fn report_reference_error_uses_reference_err_type() {
3997 let source = include_str!("runtime_bridge.rs");
3998 let func_start = source
3999 .find("unsafe fn report_reference_error")
4000 .expect("report_reference_error not found");
4001 let func_body = &source[func_start..func_start + 2000.min(source.len() - func_start)];
4002
4003 assert!(
4004 func_body.contains("JSEXN_REFERENCEERR"),
4005 "DF-WK-11 REGRESSION: report_reference_error must use JSEXN_REFERENCEERR error type"
4006 );
4007 assert!(
4008 func_body.contains("JS_ReportErrorNumberUTF8"),
4009 "DF-WK-11 REGRESSION: report_reference_error must use JS_ReportErrorNumberUTF8 for proper error reporting"
4010 );
4011 }
4012
4013 #[test]
4019 fn storage_is_webview_id_keyed_dashmap() {
4020 let source = include_str!("runtime_bridge.rs");
4021 assert!(
4022 source.contains(
4023 "static NODE_REALM_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4024 ),
4025 "BCE-20260621-001 REGRESSION: NODE_REALM_BY_WEBVIEW must be WebViewId-keyed"
4026 );
4027 assert!(
4028 source.contains(
4029 "static PAGE_GLOBAL_BY_WEBVIEW: OnceLock<DashMap<servo::WebViewId, usize>>"
4030 ),
4031 "BCE-20260621-001 REGRESSION: PAGE_GLOBAL_BY_WEBVIEW must be WebViewId-keyed"
4032 );
4033 assert!(
4034 source.contains("thread_local! {\n static PER_THREAD_PAGE_GLOBAL"),
4035 "BCE-20260621-001 REGRESSION: PER_THREAD_PAGE_GLOBAL thread_local must exist for lazy getters"
4036 );
4037 }
4038
4039 #[test]
4043 fn no_cross_thread_raw_jsobject_storage_residual() {
4044 let source = include_str!("runtime_bridge.rs");
4045 for line in source.lines() {
4046 let trimmed = line.trim_start();
4047 assert!(
4048 !(trimmed.starts_with("static NODE_REALMS:")
4049 && !trimmed.starts_with("static NODE_REALM_BY_WEBVIEW")),
4050 "BCE-20260621-001 RESIDUAL: NODE_REALMS usize-keyed DashMap still present"
4051 );
4052 assert!(
4053 !(trimmed.starts_with("static PAGE_GLOBALS:")
4054 && !trimmed.starts_with("static PAGE_GLOBAL_BY_WEBVIEW")),
4055 "BCE-20260621-001 RESIDUAL: PAGE_GLOBALS usize-keyed DashMap still present"
4056 );
4057 assert!(
4058 !trimmed.starts_with("static LAST_PAGE_GLOBAL:"),
4059 "BCE-20260621-001 RESIDUAL: LAST_PAGE_GLOBAL AtomicUsize still present"
4060 );
4061 assert!(
4062 !trimmed.starts_with("fn get_last_page_global"),
4063 "BCE-20260621-001 RESIDUAL: get_last_page_global accessor still present"
4064 );
4065 assert!(
4066 !trimmed.starts_with("fn set_last_page_global"),
4067 "BCE-20260621-001 RESIDUAL: set_last_page_global accessor still present"
4068 );
4069 assert!(
4070 !trimmed.starts_with("pub fn get_last_page_global"),
4071 "BCE-20260621-001 RESIDUAL: pub get_last_page_global accessor still present"
4072 );
4073 }
4074 }
4075
4076 #[test]
4078 fn oncelock_evaluate_result_set_and_get() {
4079 use std::sync::Arc;
4080 let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4081 assert!(lock.get().is_none(), "OnceLock should be unset initially");
4082
4083 let result = super::EvaluateResult::ok("42".into());
4084 assert!(lock.set(result).is_ok(), "First set should succeed");
4085
4086 let got = lock.get().unwrap();
4087 assert_eq!(got.value, Some("42".into()));
4088 assert!(got.error.is_none());
4089 }
4090
4091 #[test]
4093 fn oncelock_evaluate_result_second_set_fails() {
4094 use std::sync::Arc;
4095 let lock: Arc<OnceLock<super::EvaluateResult>> = Arc::new(OnceLock::new());
4096
4097 let first = super::EvaluateResult::ok("first".into());
4098 assert!(lock.set(first).is_ok());
4099
4100 let second = super::EvaluateResult::err("second".into());
4101 let set_result = lock.set(second);
4102 assert!(set_result.is_err(), "Second set should return Err");
4103 assert_eq!(lock.get().unwrap().value, Some("first".into()));
4105 }
4106}