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