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_safe, 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_safe(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 => "<error converting value to string>".into(),
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(unsafe { cx.raw_cx() }, iterator.handle(), |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 = unsafe { IdVector::new(cx.raw_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("/* invalid */");
494 }
495 let mut ids = unsafe { IdVector::new(cx.raw_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("/* 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("…");
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("/* 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("/* 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("/* invalid */");
557 }
558 handle_value_to_string(cx, key_value.handle())
559 } else {
560 return DOMString::from("/* 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("...");
579 }
580 let value_bits = value.asBits_;
581 if parents.contains(&value_bits) {
582 return DOMString::from("[circular]");
583 }
584 if value.is_undefined() {
585 return DOMString::from("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("<error converting DOM object to string>".into());
616 };
617 let class_name = unsafe { jsstr_to_string(cx, class_name) }
618 .replace("[object ", "")
619 .replace("]", "");
620 let mut repr = format!("{} ", class_name);
621 rooted!(&in(cx) let mut value = value.get());
622
623 #[expect(unsafe_code)]
624 unsafe extern "C" fn stringified(
625 string: *const u16,
626 len: u32,
627 data: *mut std::ffi::c_void,
628 ) -> bool {
629 let s = data as *mut String;
630 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
631 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
632 true
633 }
634
635 rooted!(&in(cx) let space = Int32Value(2));
636 let stringify_result = unsafe {
637 JS_Stringify(
638 cx,
639 value.handle_mut(),
640 HandleObject::null(),
641 space.handle(),
642 Some(stringified),
643 &mut repr as *mut String as *mut _,
644 )
645 };
646 if !stringify_result {
647 return Some("<error converting DOM object to string>".into());
648 }
649 Some(repr.into())
650}
651
652#[expect(unsafe_code)]
660fn apply_sprintf_substitutions(cx: &mut JSContext, messages: &[HandleValue]) -> (String, usize) {
661 debug_assert!(!messages.is_empty() && messages[0].is_string());
662
663 let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
664 let format_string = unsafe { jsstr_to_string(cx, js_string) };
665
666 let mut result = String::new();
667 let mut arg_index = 1usize;
668 let mut chars = format_string.chars().peekable();
669
670 while let Some(c) = chars.next() {
671 if c != '%' {
672 result.push(c);
673 continue;
674 }
675
676 match chars.peek().copied() {
677 Some('s') => {
678 chars.next();
679 if arg_index < messages.len() {
680 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
681 arg_index += 1;
682 } else {
683 result.push_str("%s");
684 }
685 },
686 Some('d') | Some('i') => {
687 let spec = chars.next().unwrap();
688 if arg_index < messages.len() {
689 let num = unsafe { ToNumber(cx.raw_cx(), messages[arg_index]) };
690 if num.is_err() {
691 unsafe { JS_ClearPendingException(cx) };
692 }
693 arg_index += 1;
694 format_integer_substitution(&mut result, num);
695 } else {
696 result.push('%');
697 result.push(spec);
698 }
699 },
700 Some('f') => {
701 chars.next();
702 if arg_index < messages.len() {
703 let num = unsafe { ToNumber(cx.raw_cx(), messages[arg_index]) };
704 if num.is_err() {
705 unsafe { JS_ClearPendingException(cx) };
706 }
707 arg_index += 1;
708 format_float_substitution(&mut result, num);
709 } else {
710 result.push_str("%f");
711 }
712 },
713 Some('o') | Some('O') => {
714 let spec = chars.next().unwrap();
715 if arg_index < messages.len() {
716 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
717 arg_index += 1;
718 } else {
719 result.push('%');
720 result.push(spec);
721 }
722 },
723 Some('c') => {
724 chars.next();
725 if arg_index < messages.len() {
726 arg_index += 1; }
728 },
729 Some('%') => {
730 chars.next();
731 result.push('%');
732 },
733 _ => {
734 result.push('%');
735 },
736 }
737 }
738
739 (result, arg_index)
740}
741
742fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
743 match num {
744 Ok(n) if n.is_nan() => result.push_str("NaN"),
745 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
746 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
747 Ok(n) => result.push_str(&(n.trunc() as i64).to_string()),
748 Err(_) => result.push_str("NaN"),
749 }
750}
751
752fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
753 match num {
754 Ok(n) if n.is_nan() => result.push_str("NaN"),
755 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
756 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
757 Ok(n) => result.push_str(&n.to_string()),
758 Err(_) => result.push_str("NaN"),
759 }
760}
761
762fn stringify_handle_values(cx: &mut JSContext, messages: &[HandleValue]) -> DOMString {
763 DOMString::from(itertools::join(
764 messages
765 .iter()
766 .copied()
767 .map(|msg| stringify_handle_value(cx, msg)),
768 " ",
769 ))
770}
771
772fn stringify_debugger_value(value: &DebuggerValue) -> String {
775 match value {
776 DebuggerValue::VoidValue => "undefined".into(),
777 DebuggerValue::NullValue(_) => "null".into(),
778 DebuggerValue::BooleanValue(value) => value.to_string(),
779 DebuggerValue::NumberValue(value) => value.to_string(),
780 DebuggerValue::StringValue(value) => value.clone(),
781 DebuggerValue::ObjectValue { class, preview, .. } => {
782 let Some(preview) = preview else {
783 return class.clone();
784 };
785
786 if preview.kind == "ArrayLike" {
787 let mut items = preview
788 .items
789 .as_ref()
790 .map(|items| {
791 items
792 .iter()
793 .take(MAX_LOG_CHILDREN)
794 .map(stringify_debugger_value)
795 .collect::<Vec<_>>()
796 })
797 .unwrap_or_default();
798 if preview
799 .array_length
800 .is_some_and(|length| length as usize > items.len())
801 {
802 items.push("...".into());
803 }
804 return format!("[{}]", itertools::join(items, ", "));
805 }
806
807 let mut properties = preview
808 .own_properties
809 .as_ref()
810 .map(|properties| {
811 properties
812 .iter()
813 .take(MAX_LOG_CHILDREN)
814 .map(|property| {
815 format!(
816 "{}: {}",
817 property.name,
818 stringify_debugger_value(&property.value)
819 )
820 })
821 .collect::<Vec<_>>()
822 })
823 .unwrap_or_default();
824 if preview
825 .own_properties_length
826 .is_some_and(|length| length as usize > properties.len())
827 {
828 properties.push("...".into());
829 }
830 format!("{class} {{{}}}", itertools::join(properties, ", "))
831 },
832 }
833}
834
835#[derive(Debug, Eq, PartialEq)]
836enum IncludeStackTrace {
837 Yes,
838 No,
839}
840
841impl consoleMethods<crate::DomTypeHolder> for Console {
842 fn Log(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
844 Console::method(
845 cx,
846 global,
847 ConsoleLogLevel::Log,
848 messages,
849 IncludeStackTrace::No,
850 );
851 }
852
853 fn Clear(global: &GlobalScope) {
855 if let Some(chan) = global.devtools_chan() {
856 let worker_id = global
857 .downcast::<WorkerGlobalScope>()
858 .map(|worker| worker.worker_id());
859 let devtools_message =
860 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
861 if let Err(error) = chan.send(devtools_message) {
862 log::warn!("Error sending clear message to devtools: {error:?}");
863 }
864 }
865 }
866
867 fn Debug(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
869 Console::method(
870 cx,
871 global,
872 ConsoleLogLevel::Debug,
873 messages,
874 IncludeStackTrace::No,
875 );
876 }
877
878 fn Info(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
880 Console::method(
881 cx,
882 global,
883 ConsoleLogLevel::Info,
884 messages,
885 IncludeStackTrace::No,
886 );
887 }
888
889 fn Warn(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
891 Console::method(
892 cx,
893 global,
894 ConsoleLogLevel::Warn,
895 messages,
896 IncludeStackTrace::No,
897 );
898 }
899
900 fn Error(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
902 Console::method(
903 cx,
904 global,
905 ConsoleLogLevel::Error,
906 messages,
907 IncludeStackTrace::No,
908 );
909 }
910
911 fn Trace(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
913 Console::method(
914 cx,
915 global,
916 ConsoleLogLevel::Trace,
917 messages,
918 IncludeStackTrace::Yes,
919 );
920 }
921
922 fn Dir(
924 cx: &mut js::context::JSContext,
925 global: &GlobalScope,
926 item: HandleValue,
927 _options: Option<*mut jsapi::JSObject>,
928 ) {
929 let argument = console_argument_from_handle_value(cx, item, &mut Vec::new());
931 let prefix = global.current_group_label().unwrap_or_default();
932 Console::send_to_devtools(
934 global,
935 Self::build_message(cx, ConsoleLogLevel::Dir, vec![argument.clone()], None),
936 );
937 Self::send_to_embedder(
938 global,
939 ConsoleLogLevel::Dir,
940 format!("{prefix}{}", stringify_debugger_value(&argument)),
941 );
942 }
943
944 fn Assert(
946 cx: &mut JSContext,
947 global: &GlobalScope,
948 condition: bool,
949 messages: Vec<HandleValue>,
950 ) {
951 if !condition {
952 let message = format!(
953 "Assertion failed: {}",
954 stringify_handle_values(cx, &messages)
955 );
956
957 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
958 }
959 }
960
961 fn Time(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
963 if let Ok(()) = global.time(label.clone()) {
964 let message = format!("{label}: timer started");
965 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
966 }
967 }
968
969 fn TimeLog(cx: &mut JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
971 if let Ok(delta) = global.time_log(&label) {
972 let message = format!("{label}: {delta}ms {}", stringify_handle_values(cx, &data));
973
974 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
975 }
976 }
977
978 fn TimeEnd(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
980 if let Ok(delta) = global.time_end(&label) {
981 let message = format!("{label}: {delta}ms");
982
983 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
984 }
985 }
986
987 fn Group(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
989 global.push_console_group(stringify_handle_values(cx, &messages));
990 }
991
992 fn GroupCollapsed(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
994 global.push_console_group(stringify_handle_values(cx, &messages));
995 }
996
997 fn GroupEnd(global: &GlobalScope) {
999 global.pop_console_group();
1000 }
1001
1002 fn Count(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1004 let count = global.increment_console_count(&label);
1005 let message = format!("{label}: {count}");
1006
1007 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1008 }
1009
1010 fn CountReset(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1012 if global.reset_console_count(&label).is_err() {
1013 Self::internal_warn(cx, global, format!("Counter “{label}” doesn’t exist."))
1014 }
1015 }
1016}
1017
1018#[expect(unsafe_code)]
1019fn get_js_stack(cx: &mut JSContext) -> Vec<StackFrame> {
1020 const MAX_FRAME_COUNT: u32 = 128;
1021
1022 let mut frames = vec![];
1023 rooted!(&in(cx) let mut handle = ptr::null_mut());
1024 let captured_js_stack =
1025 unsafe { CapturedJSStack::new(cx.raw_cx(), handle, Some(MAX_FRAME_COUNT)) };
1026 let Some(captured_js_stack) = captured_js_stack else {
1027 return frames;
1028 };
1029
1030 captured_js_stack.for_each_stack_frame(|frame| {
1031 rooted!(&in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
1032
1033 unsafe {
1035 GetSavedFrameFunctionDisplayName(
1036 cx,
1037 ptr::null_mut(),
1038 frame,
1039 result.handle_mut(),
1040 SavedFrameSelfHosted::Include,
1041 );
1042 }
1043 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1044 unsafe { jsstr_to_string(cx, nonnull_result) }
1045 } else {
1046 "<anonymous>".into()
1047 };
1048
1049 result.set(ptr::null_mut());
1051 unsafe {
1052 GetSavedFrameSource(
1053 cx,
1054 ptr::null_mut(),
1055 frame,
1056 result.handle_mut(),
1057 SavedFrameSelfHosted::Include,
1058 );
1059 }
1060 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1061 unsafe { jsstr_to_string(cx, nonnull_result) }
1062 } else {
1063 "<anonymous>".into()
1064 };
1065
1066 let mut line_number = 0;
1068 unsafe {
1069 GetSavedFrameLine(
1070 cx,
1071 ptr::null_mut(),
1072 frame,
1073 &mut line_number,
1074 SavedFrameSelfHosted::Include,
1075 );
1076 }
1077
1078 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
1079 unsafe {
1080 GetSavedFrameColumn(
1081 cx,
1082 ptr::null_mut(),
1083 frame,
1084 &mut column_number,
1085 SavedFrameSelfHosted::Include,
1086 );
1087 }
1088 let frame = StackFrame {
1089 filename,
1090 function_name,
1091 line_number,
1092 column_number: column_number.value_,
1093 };
1094
1095 frames.push(frame);
1096 });
1097
1098 frames
1099}