1use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10 ConsoleLogLevel, ConsoleMessage, ConsoleMessageFields, DebuggerValue, FunctionPreview,
11 ObjectPreview, PropertyDescriptor as DevtoolsPropertyDescriptor, ScriptToDevtoolsControlMsg,
12 StackFrame, get_time_stamp,
13};
14use embedder_traits::EmbedderMsg;
15use js::context::JSContext;
16use js::conversions::jsstr_to_string;
17use js::jsapi::{self, ESClass, JS_GetFunctionArity, PropertyDescriptor, SavedFrameSelfHosted};
18use js::jsval::{Int32Value, UndefinedValue};
19use js::realm::CurrentRealm;
20use js::rust::wrappers2::{
21 GetArrayLength, GetBuiltinClass, GetPropertyKeys, GetSavedFrameColumn,
22 GetSavedFrameFunctionDisplayName, GetSavedFrameLine, GetSavedFrameSource,
23 JS_ClearPendingException, JS_GetElement, JS_GetFunctionDisplayId, JS_GetFunctionId,
24 JS_GetOwnPropertyDescriptorById, JS_GetPropertyById, JS_IdToValue, JS_Stringify,
25 JS_ValueToFunction, JS_ValueToSource, MapEntries, MapSize,
26};
27use js::rust::{
28 CapturedJSStack, HandleObject, HandleValue, IdVector, ToNumber, ToString,
29 describe_scripted_caller, for_of,
30};
31use script_bindings::conversions::get_dom_class;
32
33use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
34use crate::dom::bindings::error::report_pending_exception;
35use crate::dom::bindings::inheritance::Castable;
36use crate::dom::bindings::str::DOMString;
37use crate::dom::globalscope::GlobalScope;
38use crate::dom::workerglobalscope::WorkerGlobalScope;
39
40const MAX_LOG_DEPTH: usize = 10;
42const MAX_LOG_CHILDREN: usize = 15;
44
45#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
47pub(crate) struct Console;
48
49impl Console {
50 fn build_message(
51 cx: &mut JSContext,
52 level: ConsoleLogLevel,
53 arguments: Vec<DebuggerValue>,
54 stacktrace: Option<Vec<StackFrame>>,
55 ) -> ConsoleMessage {
56 let caller = describe_scripted_caller(cx).unwrap_or_default();
57
58 ConsoleMessage {
59 fields: ConsoleMessageFields {
60 level,
61 filename: caller.filename,
62 line_number: caller.line,
63 column_number: caller.col,
64 time_stamp: get_time_stamp(),
65 },
66 arguments,
67 stacktrace,
68 }
69 }
70
71 fn send_string_message(
73 cx: &mut JSContext,
74 global: &GlobalScope,
75 level: ConsoleLogLevel,
76 message: String,
77 ) {
78 let prefix = global.current_group_label().unwrap_or_default();
79 let formatted_message = format!("{prefix}{message}");
80
81 Self::send_to_embedder(global, level.clone(), formatted_message);
82
83 let console_message =
84 Self::build_message(cx, level, vec![DebuggerValue::StringValue(message)], None);
85
86 Self::send_to_devtools(global, console_message);
87 }
88
89 fn method(
90 cx: &mut JSContext,
91 global: &GlobalScope,
92 level: ConsoleLogLevel,
93 messages: Vec<HandleValue>,
94 include_stacktrace: IncludeStackTrace,
95 ) {
96 let (arguments, embedder_msg) = if !messages.is_empty() && messages[0].is_string() {
100 let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
101 let remaining = &messages[consumed..];
102
103 let mut arguments: Vec<DebuggerValue> =
104 vec![DebuggerValue::StringValue(formatted.clone())];
105 for msg in remaining {
106 arguments.push(console_argument_from_handle_value(
107 cx,
108 *msg,
109 &mut Vec::new(),
110 ));
111 }
112
113 let embedder_msg = if remaining.is_empty() {
114 formatted
115 } else {
116 format!("{formatted} {}", stringify_handle_values(cx, remaining))
117 };
118
119 (arguments, embedder_msg.into())
120 } else {
121 let arguments = messages
122 .iter()
123 .map(|msg| console_argument_from_handle_value(cx, *msg, &mut Vec::new()))
124 .collect();
125 (arguments, stringify_handle_values(cx, &messages))
126 };
127
128 let stacktrace = (include_stacktrace == IncludeStackTrace::Yes).then_some(get_js_stack(cx));
129 let console_message = Self::build_message(cx, level.clone(), arguments, stacktrace);
130
131 Console::send_to_devtools(global, console_message);
132
133 let prefix = global.current_group_label().unwrap_or_default();
134 let formatted_message = format!("{prefix}{embedder_msg}");
135
136 Self::send_to_embedder(global, level, formatted_message);
137 }
138
139 fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
140 if let Some(chan) = global.devtools_chan() {
141 let worker_id = global
142 .downcast::<WorkerGlobalScope>()
143 .map(|worker| worker.worker_id());
144 let devtools_message =
145 ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
146 chan.send(devtools_message).unwrap();
147 }
148 }
149
150 fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
151 global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
152 global.webview_id(),
153 level,
154 message,
155 ));
156 }
157
158 pub(crate) fn internal_warn(cx: &mut JSContext, global: &GlobalScope, message: String) {
160 Console::send_string_message(cx, global, ConsoleLogLevel::Warn, message);
161 }
162}
163
164#[expect(unsafe_code)]
165fn handle_value_to_string(cx: &mut JSContext, value: HandleValue) -> DOMString {
166 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
167 Some(js_str) => unsafe { jsstr_to_string(cx, js_str) }.into(),
168 None => DOMString::from_static("<error converting value to string>"),
169 }
170}
171
172fn console_argument_from_handle_value(
173 cx: &mut JSContext,
174 handle_value: HandleValue,
175 seen: &mut Vec<u64>,
176) -> DebuggerValue {
177 #[expect(unsafe_code)]
178 fn inner(
179 cx: &mut JSContext,
180 handle_value: HandleValue,
181 seen: &mut Vec<u64>,
182 ) -> Result<DebuggerValue, ()> {
183 if handle_value.is_string() {
184 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
185 let dom_string = unsafe { jsstr_to_string(cx, js_string) };
186 return Ok(DebuggerValue::StringValue(dom_string));
187 }
188
189 if handle_value.is_number() {
190 let number = handle_value.to_number();
191 return Ok(DebuggerValue::NumberValue(number));
192 }
193
194 if handle_value.is_boolean() {
195 let boolean = handle_value.to_boolean();
196 return Ok(DebuggerValue::BooleanValue(boolean));
197 }
198
199 if handle_value.is_object() {
200 if seen.contains(&handle_value.asBits_) {
202 return Ok(DebuggerValue::StringValue("[circular]".into()));
204 }
205
206 seen.push(handle_value.asBits_);
207 let console_object = console_object_from_handle_value(cx, handle_value, seen);
208 let js_value = seen.pop();
209 debug_assert_eq!(js_value, Some(handle_value.asBits_));
210
211 if let Some((class, preview)) = console_object {
212 return Ok(DebuggerValue::ObjectValue {
213 actor: None,
214 class,
215 own_property_length: preview.own_properties_length,
216 preview: Some(Box::new(preview)),
217 });
218 }
219
220 return Err(());
221 }
222
223 let stringified_value = stringify_handle_value(cx, handle_value);
225
226 Ok(DebuggerValue::StringValue(stringified_value.into()))
227 }
228
229 match inner(cx, handle_value, seen) {
230 Ok(arg) => arg,
231 Err(()) => {
232 report_pending_exception(&mut CurrentRealm::assert(cx));
233 DebuggerValue::StringValue("<error>".into())
234 },
235 }
236}
237
238fn accessor_value_from_property_descriptor(descriptor: &PropertyDescriptor) -> DebuggerValue {
239 let value = match (
242 descriptor.hasGetter_() && !descriptor.getter_.is_null(),
243 descriptor.hasSetter_() && !descriptor.setter_.is_null(),
244 ) {
245 (true, true) => "Getter/Setter",
246 (true, false) => "Getter",
247 (false, true) => "Setter",
248 (false, false) => "undefined",
249 };
250 DebuggerValue::StringValue(value.into())
251}
252
253#[expect(unsafe_code)]
254fn console_map_object_from_handle_value(
255 cx: &mut JSContext,
256 handle_object: HandleObject,
257 seen: &mut Vec<u64>,
258) -> Option<(String, ObjectPreview)> {
259 rooted!(&in(cx) let mut iterator = UndefinedValue());
260 if !unsafe { MapEntries(cx, handle_object, iterator.handle_mut()) } {
261 return None;
262 }
263
264 let mut entries = Vec::new();
265 for_of(cx, iterator.handle(), |cx, entry| {
266 if !entry.is_object() {
267 return Err(().into());
268 }
269
270 rooted!(&in(cx) let entry_object = entry.to_object());
271 rooted!(&in(cx) let mut key = UndefinedValue());
272 rooted!(&in(cx) let mut value = UndefinedValue());
273
274 if !unsafe { JS_GetElement(cx, entry_object.handle(), 0, key.handle_mut()) } ||
276 !unsafe { JS_GetElement(cx, entry_object.handle(), 1, value.handle_mut()) }
277 {
278 return Err(().into());
279 }
280
281 entries.push((
282 console_argument_from_handle_value(cx, key.handle(), seen),
283 console_argument_from_handle_value(cx, value.handle(), seen),
284 ));
285
286 Ok(std::ops::ControlFlow::Continue(()))
287 })
288 .ok()?;
289
290 Some((
291 "Map".into(),
292 ObjectPreview {
293 kind: "MapLike".into(),
294 size: Some(unsafe { MapSize(cx, handle_object) }),
295 entries: Some(entries),
296 own_properties_length: Some(0),
297 own_properties: None,
298 function: None,
299 array_length: None,
300 items: None,
301 },
302 ))
303}
304
305#[expect(unsafe_code)]
306fn console_object_from_handle_value(
307 cx: &mut JSContext,
308 handle_value: HandleValue,
309 seen: &mut Vec<u64>,
310) -> Option<(String, ObjectPreview)> {
311 rooted!(&in(cx) let object = handle_value.to_object());
312 let mut object_class = ESClass::Other;
313 if !unsafe { GetBuiltinClass(cx, object.handle(), &mut object_class as *mut _) } {
314 return None;
315 }
316 if object_class != ESClass::Object &&
317 object_class != ESClass::Array &&
318 object_class != ESClass::Map &&
319 object_class != ESClass::Function
320 {
321 return None;
322 }
323
324 if object_class == ESClass::Map {
325 return console_map_object_from_handle_value(cx, object.handle(), seen);
326 }
327
328 let mut own_properties = Vec::new();
329 let mut items: Vec<(i32, DebuggerValue)> = Vec::new();
330 let mut ids = IdVector::new(cx);
331 if !unsafe {
334 GetPropertyKeys(
335 cx,
336 object.handle(),
337 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
338 ids.handle_mut(),
339 )
340 } {
341 return None;
342 }
343
344 for id in ids.iter() {
345 rooted!(&in(cx) let id = *id);
346 rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
347
348 let mut is_none = false;
349 if !unsafe {
350 JS_GetOwnPropertyDescriptorById(
351 cx,
352 object.handle(),
353 id.handle(),
354 descriptor.handle_mut(),
355 &mut is_none,
356 )
357 } {
358 return None;
359 }
360 if is_none {
361 continue;
362 }
363
364 let is_accessor = (descriptor.hasGetter_() && !descriptor.getter_.is_null()) ||
367 (descriptor.hasSetter_() && !descriptor.setter_.is_null());
368 let value = if is_accessor {
369 accessor_value_from_property_descriptor(&descriptor)
370 } else {
371 rooted!(&in(cx) let property = descriptor.value_);
372 console_argument_from_handle_value(cx, property.handle(), seen)
373 };
374
375 if object_class == ESClass::Array && id.is_int() {
376 let index = id.to_int();
377 items.push((index, value));
378 continue;
379 }
380
381 let key = if id.is_string() {
382 rooted!(&in(cx) let mut key_value = UndefinedValue());
383 if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
384 continue;
385 }
386 rooted!(&in(cx) let js_string = key_value.to_string());
387 let Some(js_string) = NonNull::new(js_string.get()) else {
388 continue;
389 };
390 unsafe { jsstr_to_string(cx, js_string) }
391 } else if id.is_symbol() || id.is_int() {
392 rooted!(&in(cx) let mut key_value = UndefinedValue());
393 if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
394 continue;
395 }
396 handle_value_to_string(cx, key_value.handle()).to_string()
397 } else {
398 continue;
399 };
400
401 own_properties.push(DevtoolsPropertyDescriptor {
402 name: key,
403 value,
404 configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
405 enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
406 writable: !is_accessor && descriptor.hasWritable_() && descriptor.writable_(),
407 is_accessor,
408 });
409 }
410
411 let (class, kind, function, array_length, items) = match object_class {
412 ESClass::Array => {
413 let mut len = 0u32;
414 if !unsafe { GetArrayLength(cx, object.handle(), &mut len) } {
415 return None;
416 }
417 items.sort_by_key(|(index, _)| *index);
418 let ordered: Vec<DebuggerValue> = items.into_iter().map(|(_, value)| value).collect();
419 (
420 "Array".into(),
421 "ArrayLike".into(),
422 None,
423 Some(len),
424 Some(ordered),
425 )
426 },
427 ESClass::Function => {
428 rooted!(&in(cx) let fun = unsafe { JS_ValueToFunction(cx, handle_value) });
429 rooted!(&in(cx) let mut name = std::ptr::null_mut::<jsapi::JSString>());
430 rooted!(&in(cx) let mut display_name = std::ptr::null_mut::<jsapi::JSString>());
431 let arity;
432 unsafe {
433 JS_GetFunctionId(cx, fun.handle(), name.handle_mut());
434 JS_GetFunctionDisplayId(cx, fun.handle(), display_name.handle_mut());
435 arity = JS_GetFunctionArity(fun.get());
436 }
437 let name = ptr::NonNull::new(*name).map(|name| unsafe { jsstr_to_string(cx, name) });
438 let display_name = ptr::NonNull::new(*display_name)
439 .map(|display_name| unsafe { jsstr_to_string(cx, display_name) });
440
441 let parameter_names = (0..arity).map(|i| format!("<arg{i}>")).collect();
444
445 let function = FunctionPreview {
446 name,
447 display_name,
448 parameter_names,
449 is_async: None,
450 is_generator: None,
451 };
452 (
453 "Function".into(),
454 "Object".into(),
455 Some(function),
456 None,
457 None,
458 )
459 },
460 _ => ("Object".into(), "Object".into(), None, None, None),
462 };
463
464 Some((
465 class,
466 ObjectPreview {
467 kind,
468 size: None,
469 entries: None,
470 own_properties_length: Some(own_properties.len() as u32),
471 own_properties: Some(own_properties),
472 function,
473 array_length,
474 items,
475 },
476 ))
477}
478
479#[expect(unsafe_code)]
480pub(crate) fn stringify_handle_value(cx: &mut JSContext, message: HandleValue) -> DOMString {
481 if message.is_string() {
482 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
483 return unsafe { jsstr_to_string(cx, jsstr) }.into();
484 }
485 fn stringify_object_from_handle_value(
486 cx: &mut JSContext,
487 value: HandleValue,
488 parents: Vec<u64>,
489 ) -> DOMString {
490 rooted!(&in(cx) let mut obj = value.to_object());
491 let mut object_class = ESClass::Other;
492 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
493 return DOMString::from_static("/* invalid */");
494 }
495 let mut ids = IdVector::new(cx);
496 if !unsafe {
497 GetPropertyKeys(
498 cx,
499 obj.handle(),
500 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
501 ids.handle_mut(),
502 )
503 } {
504 return DOMString::from_static("/* invalid */");
505 }
506 let truncate = ids.len() > MAX_LOG_CHILDREN;
507 if object_class != ESClass::Array && object_class != ESClass::Object {
508 if truncate {
509 return DOMString::from_static("…");
510 } else {
511 return handle_value_to_string(cx, value);
512 }
513 }
514
515 let mut explicit_keys = object_class == ESClass::Object;
516 let mut props = Vec::with_capacity(ids.len());
517 for id in ids.iter().take(MAX_LOG_CHILDREN) {
518 rooted!(&in(cx) let id = *id);
519 rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
520
521 let mut is_none = false;
522 if !unsafe {
523 JS_GetOwnPropertyDescriptorById(
524 cx,
525 obj.handle(),
526 id.handle(),
527 desc.handle_mut(),
528 &mut is_none,
529 )
530 } {
531 return DOMString::from_static("/* invalid */");
532 }
533
534 rooted!(&in(cx) let mut property = UndefinedValue());
535 if !unsafe { JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) }
536 {
537 return DOMString::from_static("/* invalid */");
538 }
539
540 if !explicit_keys {
541 if id.is_int() {
542 if let Ok(id_int) = usize::try_from(id.to_int()) {
543 explicit_keys = props.len() != id_int;
544 } else {
545 explicit_keys = false;
546 }
547 } else {
548 explicit_keys = false;
549 }
550 }
551 let value_string = stringify_inner(cx, property.handle(), parents.clone());
552 if explicit_keys {
553 let key = if id.is_string() || id.is_symbol() || id.is_int() {
554 rooted!(&in(cx) let mut key_value = UndefinedValue());
555 if !unsafe { JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) } {
556 return DOMString::from_static("/* invalid */");
557 }
558 handle_value_to_string(cx, key_value.handle())
559 } else {
560 return DOMString::from_static("/* invalid */");
561 };
562 props.push(format!("{}: {}", key, value_string,));
563 } else {
564 props.push(String::from(value_string));
565 }
566 }
567 if truncate {
568 props.push("…".to_string());
569 }
570 if object_class == ESClass::Array {
571 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
572 } else {
573 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
574 }
575 }
576 fn stringify_inner(cx: &mut JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
577 if parents.len() >= MAX_LOG_DEPTH {
578 return DOMString::from_static("...");
579 }
580 let value_bits = value.asBits_;
581 if parents.contains(&value_bits) {
582 return DOMString::from_static("[circular]");
583 }
584 if value.is_undefined() {
585 return DOMString::from_static("undefined");
587 } else if !value.is_object() {
588 return handle_value_to_string(cx, value);
589 }
590 parents.push(value_bits);
591
592 if value.is_object() &&
593 let Some(repr) = maybe_stringify_dom_object(cx, value)
594 {
595 return repr;
596 }
597 stringify_object_from_handle_value(cx, value, parents)
598 }
599 stringify_inner(cx, message, Vec::new())
600}
601
602#[expect(unsafe_code)]
603fn maybe_stringify_dom_object(cx: &mut JSContext, value: HandleValue) -> Option<DOMString> {
604 rooted!(&in(cx) let obj = value.to_object());
609 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
610 if !is_dom_class {
611 return None;
612 }
613 rooted!(&in(cx) let class_name = unsafe { ToString(cx, value) });
614 let Some(class_name) = NonNull::new(class_name.get()) else {
615 return Some(DOMString::from_static(
616 "<error converting DOM object to string>",
617 ));
618 };
619 let class_name = unsafe { jsstr_to_string(cx, class_name) }
620 .replace("[object ", "")
621 .replace("]", "");
622 let mut repr = format!("{} ", class_name);
623 rooted!(&in(cx) let mut value = value.get());
624
625 #[expect(unsafe_code)]
626 unsafe extern "C" fn stringified(
627 string: *const u16,
628 len: u32,
629 data: *mut std::ffi::c_void,
630 ) -> bool {
631 let s = data as *mut String;
632 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
633 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
634 true
635 }
636
637 rooted!(&in(cx) let space = Int32Value(2));
638 let stringify_result = unsafe {
639 JS_Stringify(
640 cx,
641 value.handle_mut(),
642 HandleObject::null(),
643 space.handle(),
644 Some(stringified),
645 &mut repr as *mut String as *mut _,
646 )
647 };
648 if !stringify_result {
649 return Some(DOMString::from_static(
650 "<error converting DOM object to string>",
651 ));
652 }
653 Some(repr.into())
654}
655
656#[expect(unsafe_code)]
664fn apply_sprintf_substitutions(cx: &mut JSContext, messages: &[HandleValue]) -> (String, usize) {
665 debug_assert!(!messages.is_empty() && messages[0].is_string());
666
667 let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
668 let format_string = unsafe { jsstr_to_string(cx, js_string) };
669
670 let mut result = String::new();
671 let mut arg_index = 1usize;
672 let mut chars = format_string.chars().peekable();
673
674 while let Some(c) = chars.next() {
675 if c != '%' {
676 result.push(c);
677 continue;
678 }
679
680 match chars.peek().copied() {
681 Some('s') => {
682 chars.next();
683 if arg_index < messages.len() {
684 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
685 arg_index += 1;
686 } else {
687 result.push_str("%s");
688 }
689 },
690 Some('d') | Some('i') => {
691 let spec = chars.next().unwrap();
692 if arg_index < messages.len() {
693 let num = unsafe { ToNumber(cx, messages[arg_index]) };
694 if num.is_err() {
695 unsafe { JS_ClearPendingException(cx) };
696 }
697 arg_index += 1;
698 format_integer_substitution(&mut result, num);
699 } else {
700 result.push('%');
701 result.push(spec);
702 }
703 },
704 Some('f') => {
705 chars.next();
706 if arg_index < messages.len() {
707 let num = unsafe { ToNumber(cx, messages[arg_index]) };
708 if num.is_err() {
709 unsafe { JS_ClearPendingException(cx) };
710 }
711 arg_index += 1;
712 format_float_substitution(&mut result, num);
713 } else {
714 result.push_str("%f");
715 }
716 },
717 Some('o') | Some('O') => {
718 let spec = chars.next().unwrap();
719 if arg_index < messages.len() {
720 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
721 arg_index += 1;
722 } else {
723 result.push('%');
724 result.push(spec);
725 }
726 },
727 Some('c') => {
728 chars.next();
729 if arg_index < messages.len() {
730 arg_index += 1; }
732 },
733 Some('%') => {
734 chars.next();
735 result.push('%');
736 },
737 _ => {
738 result.push('%');
739 },
740 }
741 }
742
743 (result, arg_index)
744}
745
746fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
747 match num {
748 Ok(n) if n.is_nan() => result.push_str("NaN"),
749 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
750 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
751 Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
752 Err(_) => result.push_str("NaN"),
753 }
754}
755
756fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
757 match num {
758 Ok(n) if n.is_nan() => result.push_str("NaN"),
759 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
760 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
761 Ok(n) => result.push_str(&n.to_string()),
762 Err(_) => result.push_str("NaN"),
763 }
764}
765
766fn stringify_handle_values(cx: &mut JSContext, messages: &[HandleValue]) -> DOMString {
767 DOMString::from(itertools::join(
768 messages
769 .iter()
770 .copied()
771 .map(|msg| stringify_handle_value(cx, msg)),
772 " ",
773 ))
774}
775
776fn stringify_debugger_value(value: &DebuggerValue) -> String {
779 match value {
780 DebuggerValue::VoidValue => "undefined".into(),
781 DebuggerValue::NullValue(_) => "null".into(),
782 DebuggerValue::BooleanValue(value) => value.to_string(),
783 DebuggerValue::NumberValue(value) => value.to_string(),
784 DebuggerValue::StringValue(value) => value.clone(),
785 DebuggerValue::ObjectValue { class, preview, .. } => {
786 let Some(preview) = preview else {
787 return class.clone();
788 };
789
790 if preview.kind == "ArrayLike" {
791 let mut items = preview
792 .items
793 .as_ref()
794 .map(|items| {
795 items
796 .iter()
797 .take(MAX_LOG_CHILDREN)
798 .map(stringify_debugger_value)
799 .collect::<Vec<_>>()
800 })
801 .unwrap_or_default();
802 if preview
803 .array_length
804 .is_some_and(|length| length as usize > items.len())
805 {
806 items.push("...".into());
807 }
808 return format!("[{}]", itertools::join(items, ", "));
809 }
810
811 let mut properties = preview
812 .own_properties
813 .as_ref()
814 .map(|properties| {
815 properties
816 .iter()
817 .take(MAX_LOG_CHILDREN)
818 .map(|property| {
819 format!(
820 "{}: {}",
821 property.name,
822 stringify_debugger_value(&property.value)
823 )
824 })
825 .collect::<Vec<_>>()
826 })
827 .unwrap_or_default();
828 if preview
829 .own_properties_length
830 .is_some_and(|length| length as usize > properties.len())
831 {
832 properties.push("...".into());
833 }
834 format!("{class} {{{}}}", itertools::join(properties, ", "))
835 },
836 }
837}
838
839#[derive(Debug, Eq, PartialEq)]
840enum IncludeStackTrace {
841 Yes,
842 No,
843}
844
845impl consoleMethods<crate::DomTypeHolder> for Console {
846 fn Log(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
848 Console::method(
849 cx,
850 global,
851 ConsoleLogLevel::Log,
852 messages,
853 IncludeStackTrace::No,
854 );
855 }
856
857 fn Clear(global: &GlobalScope) {
859 if let Some(chan) = global.devtools_chan() {
860 let worker_id = global
861 .downcast::<WorkerGlobalScope>()
862 .map(|worker| worker.worker_id());
863 let devtools_message =
864 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
865 if let Err(error) = chan.send(devtools_message) {
866 log::warn!("Error sending clear message to devtools: {error:?}");
867 }
868 }
869 }
870
871 fn Debug(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
873 Console::method(
874 cx,
875 global,
876 ConsoleLogLevel::Debug,
877 messages,
878 IncludeStackTrace::No,
879 );
880 }
881
882 fn Info(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
884 Console::method(
885 cx,
886 global,
887 ConsoleLogLevel::Info,
888 messages,
889 IncludeStackTrace::No,
890 );
891 }
892
893 fn Warn(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
895 Console::method(
896 cx,
897 global,
898 ConsoleLogLevel::Warn,
899 messages,
900 IncludeStackTrace::No,
901 );
902 }
903
904 fn Error(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
906 Console::method(
907 cx,
908 global,
909 ConsoleLogLevel::Error,
910 messages,
911 IncludeStackTrace::No,
912 );
913 }
914
915 fn Trace(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
917 Console::method(
918 cx,
919 global,
920 ConsoleLogLevel::Trace,
921 messages,
922 IncludeStackTrace::Yes,
923 );
924 }
925
926 fn Dir(
928 cx: &mut js::context::JSContext,
929 global: &GlobalScope,
930 item: HandleValue,
931 _options: Option<*mut jsapi::JSObject>,
932 ) {
933 let argument = console_argument_from_handle_value(cx, item, &mut Vec::new());
935 let prefix = global.current_group_label().unwrap_or_default();
936 Console::send_to_devtools(
938 global,
939 Self::build_message(cx, ConsoleLogLevel::Dir, vec![argument.clone()], None),
940 );
941 Self::send_to_embedder(
942 global,
943 ConsoleLogLevel::Dir,
944 format!("{prefix}{}", stringify_debugger_value(&argument)),
945 );
946 }
947
948 fn Assert(
950 cx: &mut JSContext,
951 global: &GlobalScope,
952 condition: bool,
953 messages: Vec<HandleValue>,
954 ) {
955 if !condition {
956 let message = format!(
957 "Assertion failed: {}",
958 stringify_handle_values(cx, &messages)
959 );
960
961 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
962 }
963 }
964
965 fn Time(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
967 if let Ok(()) = global.time(label.clone()) {
968 let message = format!("{label}: timer started");
969 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
970 }
971 }
972
973 fn TimeLog(cx: &mut JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
975 if let Ok(delta) = global.time_log(&label) {
976 let message = format!("{label}: {delta}ms {}", stringify_handle_values(cx, &data));
977
978 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
979 }
980 }
981
982 fn TimeEnd(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
984 if let Ok(delta) = global.time_end(&label) {
985 let message = format!("{label}: {delta}ms");
986
987 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
988 }
989 }
990
991 fn Group(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
993 global.push_console_group(stringify_handle_values(cx, &messages));
994 }
995
996 fn GroupCollapsed(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
998 global.push_console_group(stringify_handle_values(cx, &messages));
999 }
1000
1001 fn GroupEnd(global: &GlobalScope) {
1003 global.pop_console_group();
1004 }
1005
1006 fn Count(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1008 let count = global.increment_console_count(&label);
1009 let message = format!("{label}: {count}");
1010
1011 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1012 }
1013
1014 fn CountReset(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1016 if global.reset_console_count(&label).is_err() {
1017 Self::internal_warn(cx, global, format!("Counter “{label}” doesn’t exist."))
1018 }
1019 }
1020}
1021
1022#[expect(unsafe_code)]
1023fn get_js_stack(cx: &mut JSContext) -> Vec<StackFrame> {
1024 const MAX_FRAME_COUNT: u32 = 128;
1025
1026 let mut frames = vec![];
1027 rooted!(&in(cx) let mut handle = ptr::null_mut());
1028 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
1029 let Some(mut captured_js_stack) = captured_js_stack else {
1030 return frames;
1031 };
1032
1033 captured_js_stack.for_each_stack_frame(|cx, frame| {
1034 rooted!(&in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
1035
1036 unsafe {
1038 GetSavedFrameFunctionDisplayName(
1039 cx,
1040 ptr::null_mut(),
1041 frame,
1042 result.handle_mut(),
1043 SavedFrameSelfHosted::Include,
1044 );
1045 }
1046 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1047 unsafe { jsstr_to_string(cx, nonnull_result) }
1048 } else {
1049 "<anonymous>".into()
1050 };
1051
1052 result.set(ptr::null_mut());
1054 unsafe {
1055 GetSavedFrameSource(
1056 cx,
1057 ptr::null_mut(),
1058 frame,
1059 result.handle_mut(),
1060 SavedFrameSelfHosted::Include,
1061 );
1062 }
1063 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1064 unsafe { jsstr_to_string(cx, nonnull_result) }
1065 } else {
1066 "<anonymous>".into()
1067 };
1068
1069 let mut line_number = 0;
1071 unsafe {
1072 GetSavedFrameLine(
1073 cx,
1074 ptr::null_mut(),
1075 frame,
1076 &mut line_number,
1077 SavedFrameSelfHosted::Include,
1078 );
1079 }
1080
1081 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
1082 unsafe {
1083 GetSavedFrameColumn(
1084 cx,
1085 ptr::null_mut(),
1086 frame,
1087 &mut column_number,
1088 SavedFrameSelfHosted::Include,
1089 );
1090 }
1091 let frame = StackFrame {
1092 filename,
1093 function_name,
1094 line_number,
1095 column_number: column_number.value_,
1096 };
1097
1098 frames.push(frame);
1099 });
1100
1101 frames
1102}