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