1use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10 ConsoleArgument, ConsoleArgumentObject, ConsoleArgumentPropertyValue, ConsoleLogLevel,
11 ConsoleMessage, ConsoleMessageFields, ScriptToDevtoolsControlMsg, StackFrame, get_time_stamp,
12};
13use embedder_traits::EmbedderMsg;
14use js::conversions::jsstr_to_string;
15use js::jsapi::{self, ESClass, JS_IsTypedArrayObject, PropertyDescriptor};
16use js::jsval::{Int32Value, UndefinedValue};
17use js::rust::wrappers::{
18 GetBuiltinClass, GetPropertyKeys, IsArray, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
19 JS_IdToValue, JS_Stringify, JS_ValueToSource,
20};
21use js::rust::{
22 CapturedJSStack, HandleObject, HandleValue, IdVector, ToString, describe_scripted_caller,
23};
24use script_bindings::conversions::get_dom_class;
25
26use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::str::DOMString;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::workerglobalscope::WorkerGlobalScope;
31use crate::script_runtime::JSContext;
32
33const MAX_LOG_DEPTH: usize = 10;
35const MAX_LOG_CHILDREN: usize = 15;
37
38#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
40pub(crate) struct Console;
41
42impl Console {
43 #[expect(unsafe_code)]
44 fn build_message(
45 level: ConsoleLogLevel,
46 arguments: Vec<ConsoleArgument>,
47 stacktrace: Option<Vec<StackFrame>>,
48 ) -> ConsoleMessage {
49 let cx = GlobalScope::get_cx();
50 let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
51
52 ConsoleMessage {
53 fields: ConsoleMessageFields {
54 level,
55 filename: caller.filename,
56 line_number: caller.line,
57 column_number: caller.col,
58 time_stamp: get_time_stamp(),
59 },
60 arguments,
61 stacktrace,
62 }
63 }
64
65 fn send_string_message(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
67 let prefix = global.current_group_label().unwrap_or_default();
68 let formatted_message = format!("{prefix}{message}");
69
70 Self::send_to_embedder(global, level.clone(), formatted_message);
71
72 let console_message = Self::build_message(level, vec![message.into()], None);
73
74 Self::send_to_devtools(global, console_message);
75 }
76
77 fn method(
78 global: &GlobalScope,
79 level: ConsoleLogLevel,
80 messages: Vec<HandleValue>,
81 include_stacktrace: IncludeStackTrace,
82 ) {
83 let cx = GlobalScope::get_cx();
84
85 let arguments = messages
86 .iter()
87 .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
88 .collect();
89 let stacktrace = (include_stacktrace == IncludeStackTrace::Yes)
90 .then_some(get_js_stack(*GlobalScope::get_cx()));
91 let console_message = Self::build_message(level.clone(), arguments, stacktrace);
92
93 Console::send_to_devtools(global, console_message);
94
95 let prefix = global.current_group_label().unwrap_or_default();
96 let msgs = stringify_handle_values(&messages);
97 let formatted_message = format!("{prefix}{msgs}");
98
99 Self::send_to_embedder(global, level, formatted_message);
100 }
101
102 fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
103 if let Some(chan) = global.devtools_chan() {
104 let worker_id = global
105 .downcast::<WorkerGlobalScope>()
106 .map(|worker| worker.worker_id());
107 let devtools_message =
108 ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
109 chan.send(devtools_message).unwrap();
110 }
111 }
112
113 fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
114 global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
115 global.webview_id(),
116 level,
117 message,
118 ));
119 }
120
121 pub(crate) fn internal_warn(global: &GlobalScope, message: DOMString) {
123 Console::send_string_message(global, ConsoleLogLevel::Warn, String::from(message.clone()));
124 }
125}
126
127#[expect(unsafe_code)]
128unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
129 rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
130 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
131 Some(js_str) => {
132 js_string.set(js_str.as_ptr());
133 DOMString::from_string(unsafe { jsstr_to_string(cx, js_str) })
134 },
135 None => "<error converting value to string>".into(),
136 }
137}
138
139#[expect(unsafe_code)]
140fn console_argument_from_handle_value(
141 cx: JSContext,
142 handle_value: HandleValue,
143 seen: &mut Vec<u64>,
144) -> ConsoleArgument {
145 if handle_value.is_string() {
146 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
147 let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
148 return ConsoleArgument::String(dom_string);
149 }
150
151 if handle_value.is_int32() {
152 let integer = handle_value.to_int32();
153 return ConsoleArgument::Integer(integer);
154 }
155
156 if handle_value.is_number() {
157 let number = handle_value.to_number();
158 return ConsoleArgument::Number(number);
159 }
160
161 if handle_value.is_boolean() {
162 let boolean = handle_value.to_boolean();
163 return ConsoleArgument::Boolean(boolean);
164 }
165
166 if handle_value.is_object() {
167 if seen.contains(&handle_value.asBits_) {
169 return ConsoleArgument::String("[circular]".into());
171 }
172
173 seen.push(handle_value.asBits_);
174 let maybe_argument_object = console_object_from_handle_value(cx, handle_value, seen);
175 let js_value = seen.pop();
176 debug_assert_eq!(js_value, Some(handle_value.asBits_));
177
178 if let Some(console_argument_object) = maybe_argument_object {
179 return ConsoleArgument::Object(console_argument_object);
180 }
181 }
182
183 let stringified_value = stringify_handle_value(handle_value);
185
186 ConsoleArgument::String(stringified_value.into())
187}
188
189#[expect(unsafe_code)]
190fn console_object_from_handle_value(
191 cx: JSContext,
192 handle_value: HandleValue,
193 seen: &mut Vec<u64>,
194) -> Option<ConsoleArgumentObject> {
195 rooted!(in(*cx) let object = handle_value.to_object());
196
197 let mut is_array = false;
199 if !unsafe { IsArray(*cx, object.handle(), &mut is_array) } || is_array {
200 return None;
201 }
202 if unsafe { JS_IsTypedArrayObject(object.get()) } {
203 return None;
204 }
205
206 let mut own_properties = Vec::new();
207 let mut ids = unsafe { IdVector::new(*cx) };
208 if !unsafe {
209 GetPropertyKeys(
210 *cx,
211 object.handle(),
212 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
213 ids.handle_mut(),
214 )
215 } {
216 return None;
217 }
218
219 for id in ids.iter() {
220 rooted!(in(*cx) let id = *id);
221 rooted!(in(*cx) let mut descriptor = PropertyDescriptor::default());
222
223 let mut is_none = false;
224 if !unsafe {
225 JS_GetOwnPropertyDescriptorById(
226 *cx,
227 object.handle(),
228 id.handle(),
229 descriptor.handle_mut(),
230 &mut is_none,
231 )
232 } {
233 return None;
234 }
235
236 rooted!(in(*cx) let mut property = UndefinedValue());
237 if !unsafe { JS_GetPropertyById(*cx, object.handle(), id.handle(), property.handle_mut()) }
238 {
239 return None;
240 }
241
242 let key = if id.is_string() {
243 rooted!(in(*cx) let mut key_value = UndefinedValue());
244 let raw_id: jsapi::HandleId = id.handle().into();
245 if !unsafe { JS_IdToValue(*cx, *raw_id.ptr, key_value.handle_mut()) } {
246 continue;
247 }
248 rooted!(in(*cx) let js_string = key_value.to_string());
249 let Some(js_string) = NonNull::new(js_string.get()) else {
250 continue;
251 };
252 unsafe { jsstr_to_string(*cx, js_string) }
253 } else {
254 continue;
255 };
256
257 own_properties.push(ConsoleArgumentPropertyValue {
258 key,
259 configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
260 enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
261 writable: descriptor.hasWritable_() && descriptor.writable_(),
262 value: console_argument_from_handle_value(cx, property.handle(), seen),
263 });
264 }
265
266 Some(ConsoleArgumentObject {
267 class: "Object".to_owned(),
268 own_properties,
269 })
270}
271
272#[expect(unsafe_code)]
273fn stringify_handle_value(message: HandleValue) -> DOMString {
274 let cx = GlobalScope::get_cx();
275 unsafe {
276 if message.is_string() {
277 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
278 return DOMString::from_string(jsstr_to_string(*cx, jsstr));
279 }
280 unsafe fn stringify_object_from_handle_value(
281 cx: *mut jsapi::JSContext,
282 value: HandleValue,
283 parents: Vec<u64>,
284 ) -> DOMString {
285 rooted!(in(cx) let mut obj = value.to_object());
286 let mut object_class = ESClass::Other;
287 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
288 return DOMString::from("/* invalid */");
289 }
290 let mut ids = unsafe { IdVector::new(cx) };
291 if !unsafe {
292 GetPropertyKeys(
293 cx,
294 obj.handle(),
295 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
296 ids.handle_mut(),
297 )
298 } {
299 return DOMString::from("/* invalid */");
300 }
301 let truncate = ids.len() > MAX_LOG_CHILDREN;
302 if object_class != ESClass::Array && object_class != ESClass::Object {
303 if truncate {
304 return DOMString::from("…");
305 } else {
306 return unsafe { handle_value_to_string(cx, value) };
307 }
308 }
309
310 let mut explicit_keys = object_class == ESClass::Object;
311 let mut props = Vec::with_capacity(ids.len());
312 for id in ids.iter().take(MAX_LOG_CHILDREN) {
313 rooted!(in(cx) let id = *id);
314 rooted!(in(cx) let mut desc = PropertyDescriptor::default());
315
316 let mut is_none = false;
317 if !unsafe {
318 JS_GetOwnPropertyDescriptorById(
319 cx,
320 obj.handle(),
321 id.handle(),
322 desc.handle_mut(),
323 &mut is_none,
324 )
325 } {
326 return DOMString::from("/* invalid */");
327 }
328
329 rooted!(in(cx) let mut property = UndefinedValue());
330 if !unsafe {
331 JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
332 } {
333 return DOMString::from("/* invalid */");
334 }
335
336 if !explicit_keys {
337 if id.is_int() {
338 if let Ok(id_int) = usize::try_from(id.to_int()) {
339 explicit_keys = props.len() != id_int;
340 } else {
341 explicit_keys = false;
342 }
343 } else {
344 explicit_keys = false;
345 }
346 }
347 let value_string = stringify_inner(
348 unsafe { JSContext::from_ptr(cx) },
349 property.handle(),
350 parents.clone(),
351 );
352 if explicit_keys {
353 let key = if id.is_string() || id.is_symbol() || id.is_int() {
354 rooted!(in(cx) let mut key_value = UndefinedValue());
355 let raw_id: jsapi::HandleId = id.handle().into();
356 if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
357 return DOMString::from("/* invalid */");
358 }
359 unsafe { handle_value_to_string(cx, key_value.handle()) }
360 } else {
361 return DOMString::from("/* invalid */");
362 };
363 props.push(format!("{}: {}", key, value_string,));
364 } else {
365 props.push(value_string.to_string());
366 }
367 }
368 if truncate {
369 props.push("…".to_string());
370 }
371 if object_class == ESClass::Array {
372 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
373 } else {
374 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
375 }
376 }
377 fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
378 if parents.len() >= MAX_LOG_DEPTH {
379 return DOMString::from("...");
380 }
381 let value_bits = value.asBits_;
382 if parents.contains(&value_bits) {
383 return DOMString::from("[circular]");
384 }
385 if value.is_undefined() {
386 return DOMString::from("undefined");
388 } else if !value.is_object() {
389 return unsafe { handle_value_to_string(*cx, value) };
390 }
391 parents.push(value_bits);
392
393 if value.is_object() {
394 if let Some(repr) = maybe_stringify_dom_object(cx, value) {
395 return repr;
396 }
397 }
398 unsafe { stringify_object_from_handle_value(*cx, value, parents) }
399 }
400 stringify_inner(cx, message, Vec::new())
401 }
402}
403
404#[expect(unsafe_code)]
405fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
406 rooted!(in(*cx) let obj = value.to_object());
411 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
412 if !is_dom_class {
413 return None;
414 }
415 rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
416 let Some(class_name) = NonNull::new(class_name.get()) else {
417 return Some("<error converting DOM object to string>".into());
418 };
419 let class_name = unsafe {
420 jsstr_to_string(*cx, class_name)
421 .replace("[object ", "")
422 .replace("]", "")
423 };
424 let mut repr = format!("{} ", class_name);
425 rooted!(in(*cx) let mut value = value.get());
426
427 #[expect(unsafe_code)]
428 unsafe extern "C" fn stringified(
429 string: *const u16,
430 len: u32,
431 data: *mut std::ffi::c_void,
432 ) -> bool {
433 let s = data as *mut String;
434 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
435 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
436 true
437 }
438
439 rooted!(in(*cx) let space = Int32Value(2));
440 let stringify_result = unsafe {
441 JS_Stringify(
442 *cx,
443 value.handle_mut(),
444 HandleObject::null(),
445 space.handle(),
446 Some(stringified),
447 &mut repr as *mut String as *mut _,
448 )
449 };
450 if !stringify_result {
451 return Some("<error converting DOM object to string>".into());
452 }
453 Some(repr.into())
454}
455
456fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
457 DOMString::from(itertools::join(
458 messages.iter().copied().map(stringify_handle_value),
459 " ",
460 ))
461}
462
463#[derive(Debug, Eq, PartialEq)]
464enum IncludeStackTrace {
465 Yes,
466 No,
467}
468
469impl consoleMethods<crate::DomTypeHolder> for Console {
470 fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
472 Console::method(
473 global,
474 ConsoleLogLevel::Log,
475 messages,
476 IncludeStackTrace::No,
477 );
478 }
479
480 fn Clear(global: &GlobalScope) {
482 if let Some(chan) = global.devtools_chan() {
483 let worker_id = global
484 .downcast::<WorkerGlobalScope>()
485 .map(|worker| worker.worker_id());
486 let devtools_message =
487 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
488 if let Err(error) = chan.send(devtools_message) {
489 log::warn!("Error sending clear message to devtools: {error:?}");
490 }
491 }
492 }
493
494 fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
496 Console::method(
497 global,
498 ConsoleLogLevel::Debug,
499 messages,
500 IncludeStackTrace::No,
501 );
502 }
503
504 fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
506 Console::method(
507 global,
508 ConsoleLogLevel::Info,
509 messages,
510 IncludeStackTrace::No,
511 );
512 }
513
514 fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
516 Console::method(
517 global,
518 ConsoleLogLevel::Warn,
519 messages,
520 IncludeStackTrace::No,
521 );
522 }
523
524 fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
526 Console::method(
527 global,
528 ConsoleLogLevel::Error,
529 messages,
530 IncludeStackTrace::No,
531 );
532 }
533
534 fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
536 Console::method(
537 global,
538 ConsoleLogLevel::Trace,
539 messages,
540 IncludeStackTrace::Yes,
541 );
542 }
543
544 fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
546 if !condition {
547 let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
548
549 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
550 }
551 }
552
553 fn Time(global: &GlobalScope, label: DOMString) {
555 if let Ok(()) = global.time(label.clone()) {
556 let message = format!("{label}: timer started");
557 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
558 }
559 }
560
561 fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
563 if let Ok(delta) = global.time_log(&label) {
564 let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
565
566 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
567 }
568 }
569
570 fn TimeEnd(global: &GlobalScope, label: DOMString) {
572 if let Ok(delta) = global.time_end(&label) {
573 let message = format!("{label}: {delta}ms");
574
575 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
576 }
577 }
578
579 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
581 global.push_console_group(stringify_handle_values(&messages));
582 }
583
584 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
586 global.push_console_group(stringify_handle_values(&messages));
587 }
588
589 fn GroupEnd(global: &GlobalScope) {
591 global.pop_console_group();
592 }
593
594 fn Count(global: &GlobalScope, label: DOMString) {
596 let count = global.increment_console_count(&label);
597 let message = format!("{label}: {count}");
598
599 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
600 }
601
602 fn CountReset(global: &GlobalScope, label: DOMString) {
604 if global.reset_console_count(&label).is_err() {
605 Self::internal_warn(
606 global,
607 DOMString::from(format!("Counter “{label}” doesn’t exist.")),
608 )
609 }
610 }
611}
612
613#[expect(unsafe_code)]
614fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
615 const MAX_FRAME_COUNT: u32 = 128;
616
617 let mut frames = vec![];
618 rooted!(in(cx) let mut handle = ptr::null_mut());
619 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
620 let Some(captured_js_stack) = captured_js_stack else {
621 return frames;
622 };
623
624 captured_js_stack.for_each_stack_frame(|frame| {
625 rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
626
627 unsafe {
629 jsapi::GetSavedFrameFunctionDisplayName(
630 cx,
631 ptr::null_mut(),
632 frame.into(),
633 result.handle_mut().into(),
634 jsapi::SavedFrameSelfHosted::Include,
635 );
636 }
637 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
638 unsafe { jsstr_to_string(cx, nonnull_result) }
639 } else {
640 "<anonymous>".into()
641 };
642
643 result.set(ptr::null_mut());
645 unsafe {
646 jsapi::GetSavedFrameSource(
647 cx,
648 ptr::null_mut(),
649 frame.into(),
650 result.handle_mut().into(),
651 jsapi::SavedFrameSelfHosted::Include,
652 );
653 }
654 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
655 unsafe { jsstr_to_string(cx, nonnull_result) }
656 } else {
657 "<anonymous>".into()
658 };
659
660 let mut line_number = 0;
662 unsafe {
663 jsapi::GetSavedFrameLine(
664 cx,
665 ptr::null_mut(),
666 frame.into(),
667 &mut line_number,
668 jsapi::SavedFrameSelfHosted::Include,
669 );
670 }
671
672 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
673 unsafe {
674 jsapi::GetSavedFrameColumn(
675 cx,
676 ptr::null_mut(),
677 frame.into(),
678 &mut column_number,
679 jsapi::SavedFrameSelfHosted::Include,
680 );
681 }
682 let frame = StackFrame {
683 filename,
684 function_name,
685 line_number,
686 column_number: column_number.value_,
687 };
688
689 frames.push(frame);
690 });
691
692 frames
693}