1use std::convert::TryFrom;
6use std::fmt::Write as _;
7use std::ptr::{self, NonNull};
8use std::slice;
9
10#[cfg(feature = "devtools")]
11use devtools_traits::{
12 ConsoleMessage, ConsoleMessageFields, DebuggerValue, FunctionPreview, ObjectPreview,
13 PropertyDescriptor as DevtoolsPropertyDescriptor, ScriptToDevtoolsControlMsg, StackFrame,
14 get_time_stamp,
15};
16use embedder_traits::{ConsoleLogLevel, EmbedderMsg};
17use js::context::JSContext;
18use js::conversions::jsstr_to_string;
19use js::jsapi::{self, ESClass, PropertyDescriptor};
20#[cfg(feature = "devtools")]
21use js::jsapi::{JS_GetFunctionArity, SavedFrameSelfHosted};
22use js::jsval::{Int32Value, ObjectValue, UndefinedValue};
23#[cfg(feature = "devtools")]
24use js::realm::CurrentRealm;
25#[cfg(feature = "devtools")]
26use js::rust::wrappers2::{
27 GetArrayLength, GetSavedFrameColumn, GetSavedFrameFunctionDisplayName, GetSavedFrameLine,
28 GetSavedFrameSource, JS_GetElement, JS_GetFunctionDisplayId, JS_GetFunctionId,
29 JS_ValueToFunction, MapEntries, MapSize,
30};
31use js::rust::wrappers2::{
32 GetBuiltinClass, JS_ClearPendingException, 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;
38use script_bindings::js::{
39 GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById, JS_GetPrototype,
40 JS_IdToValue,
41};
42
43use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
44#[cfg(feature = "devtools")]
45use crate::dom::bindings::error::report_pending_exception;
46#[cfg(feature = "devtools")]
47use crate::dom::bindings::inheritance::Castable;
48use crate::dom::bindings::str::DOMString;
49use crate::dom::globalscope::GlobalScope;
50#[cfg(feature = "devtools")]
51use crate::dom::workerglobalscope::WorkerGlobalScope;
52
53const MAX_LOG_DEPTH: usize = 10;
55const MAX_LOG_CHILDREN: usize = 15;
57
58#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
60pub(crate) struct Console;
61
62impl Console {
63 fn send_string_message(
65 cx: &mut JSContext,
66 global: &GlobalScope,
67 level: ConsoleLogLevel,
68 message: String,
69 ) {
70 let prefix = global.current_group_label().unwrap_or_default();
71 let formatted_message = format!("{prefix}{message}");
72
73 Self::send_to_embedder(global, level.clone(), formatted_message);
74
75 #[cfg(feature = "devtools")]
76 {
77 let console_message =
78 Self::build_message(cx, level, vec![DebuggerValue::StringValue(message)], None);
79 Self::send_to_devtools(global, console_message);
80 }
81
82 let _ = cx;
83 }
84
85 fn method(
86 cx: &mut JSContext,
87 global: &GlobalScope,
88 level: ConsoleLogLevel,
89 messages: Vec<HandleValue>,
90 include_stacktrace: IncludeStackTrace,
91 ) {
92 #[cfg(feature = "devtools")]
93 let devtools_arguments: Vec<DebuggerValue>;
94
95 let embedder_msg = if !messages.is_empty() && messages[0].is_string() {
99 let (formatted, consumed) = apply_sprintf_substitutions(cx, &messages);
100 let remaining = &messages[consumed..];
101
102 #[cfg(feature = "devtools")]
103 {
104 let mut arguments: Vec<DebuggerValue> =
105 vec![DebuggerValue::StringValue(formatted.clone())];
106 for msg in remaining {
107 arguments.push(devtools::console_argument_from_handle_value(
108 cx,
109 *msg,
110 &mut Vec::new(),
111 ));
112 }
113 devtools_arguments = arguments;
114 }
115
116 let embedder_msg = if remaining.is_empty() {
117 formatted
118 } else {
119 format!("{formatted} {}", stringify_handle_values(cx, remaining))
120 };
121
122 embedder_msg.into()
123 } else {
124 #[cfg(feature = "devtools")]
125 {
126 let arguments: Vec<DebuggerValue> = messages
127 .iter()
128 .map(|msg| {
129 devtools::console_argument_from_handle_value(cx, *msg, &mut Vec::new())
130 })
131 .collect();
132 devtools_arguments = arguments;
133 }
134
135 stringify_handle_values(cx, &messages)
136 };
137
138 #[cfg(feature = "devtools")]
139 {
140 let stacktrace =
141 (include_stacktrace == IncludeStackTrace::Yes).then_some(get_js_stack(cx));
142 Console::send_to_devtools(
143 global,
144 Self::build_message(cx, level.clone(), devtools_arguments, stacktrace),
145 );
146 }
147
148 let prefix = global.current_group_label().unwrap_or_default();
149 let formatted_message = format!("{prefix}{embedder_msg}");
150
151 Self::send_to_embedder(global, level, formatted_message);
152 }
153
154 fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
155 global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
156 global.webview_id(),
157 level,
158 message,
159 ));
160 }
161
162 pub(crate) fn internal_warn(cx: &mut JSContext, global: &GlobalScope, message: String) {
164 Console::send_string_message(cx, global, ConsoleLogLevel::Warn, message);
165 }
166
167 pub(crate) fn internal_error(cx: &mut JSContext, global: &GlobalScope, message: String) {
168 Console::send_string_message(cx, global, ConsoleLogLevel::Error, message);
169 }
170}
171
172#[expect(unsafe_code)]
173fn handle_value_to_string(cx: &mut JSContext, value: HandleValue) -> DOMString {
174 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
175 Some(js_str) => unsafe { jsstr_to_string(cx, js_str) }.into(),
176 None => DOMString::from_static("<error converting value to string>"),
177 }
178}
179
180#[cfg(feature = "devtools")]
181mod devtools {
182 use super::*;
183
184 impl Console {
185 pub(super) fn build_message(
186 cx: &mut JSContext,
187 level: ConsoleLogLevel,
188 arguments: Vec<DebuggerValue>,
189 stacktrace: Option<Vec<StackFrame>>,
190 ) -> ConsoleMessage {
191 let caller = describe_scripted_caller(cx).unwrap_or_default();
192
193 ConsoleMessage {
194 fields: ConsoleMessageFields {
195 level,
196 filename: caller.filename,
197 line_number: caller.line,
198 column_number: caller.col,
199 time_stamp: get_time_stamp(),
200 },
201 arguments,
202 stacktrace,
203 }
204 }
205
206 pub(super) fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
207 if let Some(chan) = global.devtools_chan() {
208 let worker_id = global
209 .downcast::<WorkerGlobalScope>()
210 .map(|worker| worker.worker_id());
211 let devtools_message = ScriptToDevtoolsControlMsg::ConsoleAPI(
212 global.pipeline_id(),
213 message,
214 worker_id,
215 );
216 chan.send(devtools_message).unwrap();
217 }
218 }
219 }
220
221 pub(super) fn console_argument_from_handle_value(
222 cx: &mut JSContext,
223 handle_value: HandleValue,
224 seen: &mut Vec<u64>,
225 ) -> DebuggerValue {
226 #[expect(unsafe_code)]
227 fn inner(
228 cx: &mut JSContext,
229 handle_value: HandleValue,
230 seen: &mut Vec<u64>,
231 ) -> Result<DebuggerValue, ()> {
232 if handle_value.is_string() {
233 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
234 let dom_string = unsafe { jsstr_to_string(cx, js_string) };
235 return Ok(DebuggerValue::StringValue(dom_string));
236 }
237
238 if handle_value.is_number() {
239 let number = handle_value.to_number();
240 return Ok(DebuggerValue::NumberValue(number));
241 }
242
243 if handle_value.is_boolean() {
244 let boolean = handle_value.to_boolean();
245 return Ok(DebuggerValue::BooleanValue(boolean));
246 }
247
248 if handle_value.is_object() {
249 if seen.contains(&handle_value.asBits_) {
251 return Ok(DebuggerValue::StringValue("[circular]".into()));
253 }
254
255 seen.push(handle_value.asBits_);
256 rooted!(&in(cx) let object = handle_value.to_object());
257 let console_object = console_object_from_handle_value(cx, handle_value, seen);
258 let prototype = object_prototype_debugger_value(cx, object.handle(), seen)?;
259 let js_value = seen.pop();
260 debug_assert_eq!(js_value, Some(handle_value.asBits_));
261
262 if let Some((class, preview)) = console_object {
263 return Ok(DebuggerValue::ObjectValue {
264 actor: None,
265 class,
266 own_property_length: preview.own_properties_length,
267 preview: Some(Box::new(preview)),
268 prototype,
269 });
270 }
271
272 return Err(());
273 }
274
275 let stringified_value = stringify_handle_value(cx, handle_value);
277
278 Ok(DebuggerValue::StringValue(stringified_value.into()))
279 }
280
281 match inner(cx, handle_value, seen) {
282 Ok(arg) => arg,
283 Err(()) => {
284 report_pending_exception(&mut CurrentRealm::assert(cx));
285 DebuggerValue::StringValue("<error>".into())
286 },
287 }
288 }
289
290 #[cfg(feature = "devtools")]
291 fn object_prototype_debugger_value(
292 cx: &mut JSContext,
293 object: HandleObject,
294 seen: &mut Vec<u64>,
295 ) -> Result<Option<Box<DebuggerValue>>, ()> {
296 rooted!(&in(cx) let mut prototype = ptr::null_mut::<jsapi::JSObject>());
297 if !JS_GetPrototype(cx, object, prototype.handle_mut()) {
298 return Err(());
299 }
300 if prototype.is_null() {
301 return Ok(Some(Box::new(DebuggerValue::NullValue(false))));
302 }
303
304 rooted!(&in(cx) let prototype = ObjectValue(prototype.get()));
305 Ok(Some(Box::new(console_argument_from_handle_value(
306 cx,
307 prototype.handle(),
308 seen,
309 ))))
310 }
311
312 fn accessor_value_from_property_descriptor(descriptor: &PropertyDescriptor) -> DebuggerValue {
313 let value = match (
316 descriptor.hasGetter_() && !descriptor.getter_.is_null(),
317 descriptor.hasSetter_() && !descriptor.setter_.is_null(),
318 ) {
319 (true, true) => "Getter/Setter",
320 (true, false) => "Getter",
321 (false, true) => "Setter",
322 (false, false) => "undefined",
323 };
324 DebuggerValue::StringValue(value.into())
325 }
326
327 #[expect(unsafe_code)]
328 fn console_map_object_from_handle_value(
329 cx: &mut JSContext,
330 handle_object: HandleObject,
331 seen: &mut Vec<u64>,
332 ) -> Option<(String, ObjectPreview)> {
333 rooted!(&in(cx) let mut iterator = UndefinedValue());
334 if !unsafe { MapEntries(cx, handle_object, iterator.handle_mut()) } {
335 return None;
336 }
337
338 let mut entries = Vec::new();
339 for_of(cx, iterator.handle(), |cx, entry| {
340 if !entry.is_object() {
341 return Err(().into());
342 }
343
344 rooted!(&in(cx) let entry_object = entry.to_object());
345 rooted!(&in(cx) let mut key = UndefinedValue());
346 rooted!(&in(cx) let mut value = UndefinedValue());
347
348 if !unsafe { JS_GetElement(cx, entry_object.handle(), 0, key.handle_mut()) } ||
350 !unsafe { JS_GetElement(cx, entry_object.handle(), 1, value.handle_mut()) }
351 {
352 return Err(().into());
353 }
354
355 entries.push((
356 console_argument_from_handle_value(cx, key.handle(), seen),
357 console_argument_from_handle_value(cx, value.handle(), seen),
358 ));
359
360 Ok(std::ops::ControlFlow::Continue(()))
361 })
362 .ok()?;
363
364 Some((
365 "Map".into(),
366 ObjectPreview {
367 kind: "MapLike".into(),
368 size: Some(unsafe { MapSize(cx, handle_object) }),
369 entries: Some(entries),
370 own_properties_length: Some(0),
371 own_properties: None,
372 function: None,
373 array_length: None,
374 items: None,
375 },
376 ))
377 }
378
379 #[expect(unsafe_code)]
380 fn console_object_from_handle_value(
381 cx: &mut JSContext,
382 handle_value: HandleValue,
383 seen: &mut Vec<u64>,
384 ) -> Option<(String, ObjectPreview)> {
385 rooted!(&in(cx) let object = handle_value.to_object());
386 let mut object_class = ESClass::Other;
387 if !unsafe { GetBuiltinClass(cx, object.handle(), &mut object_class as *mut _) } {
388 return None;
389 }
390 if object_class != ESClass::Object &&
391 object_class != ESClass::Array &&
392 object_class != ESClass::Map &&
393 object_class != ESClass::Function
394 {
395 return None;
396 }
397
398 if object_class == ESClass::Map {
399 return console_map_object_from_handle_value(cx, object.handle(), seen);
400 }
401
402 let mut own_properties = Vec::new();
403 let mut items: Vec<(i32, DebuggerValue)> = Vec::new();
404 let mut ids = IdVector::new(cx);
405 if !GetPropertyKeys(
408 cx,
409 object.handle(),
410 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS | jsapi::JSITER_HIDDEN,
411 ids.handle_mut(),
412 ) {
413 return None;
414 }
415
416 for id in ids.iter() {
417 rooted!(&in(cx) let id = *id);
418 rooted!(&in(cx) let mut descriptor = PropertyDescriptor::default());
419
420 let mut is_none = false;
421 if !JS_GetOwnPropertyDescriptorById(
422 cx,
423 object.handle(),
424 id.handle(),
425 descriptor.handle_mut(),
426 &mut is_none,
427 ) {
428 return None;
429 }
430 if is_none {
431 continue;
432 }
433
434 let is_accessor = (descriptor.hasGetter_() && !descriptor.getter_.is_null()) ||
437 (descriptor.hasSetter_() && !descriptor.setter_.is_null());
438 let value = if is_accessor {
439 accessor_value_from_property_descriptor(&descriptor)
440 } else {
441 rooted!(&in(cx) let property = descriptor.value_);
442 console_argument_from_handle_value(cx, property.handle(), seen)
443 };
444
445 if object_class == ESClass::Array && id.is_int() {
446 let index = id.to_int();
447 items.push((index, value));
448 continue;
449 }
450
451 let key = if id.is_string() {
452 rooted!(&in(cx) let mut key_value = UndefinedValue());
453 if !JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) {
454 continue;
455 }
456 rooted!(&in(cx) let js_string = key_value.to_string());
457 let Some(js_string) = NonNull::new(js_string.get()) else {
458 continue;
459 };
460 unsafe { jsstr_to_string(cx, js_string) }
461 } else if id.is_symbol() || id.is_int() {
462 rooted!(&in(cx) let mut key_value = UndefinedValue());
463 if !JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) {
464 continue;
465 }
466 handle_value_to_string(cx, key_value.handle()).to_string()
467 } else {
468 continue;
469 };
470
471 own_properties.push(DevtoolsPropertyDescriptor {
472 name: key,
473 value,
474 configurable: descriptor.hasConfigurable_() && descriptor.configurable_(),
475 enumerable: descriptor.hasEnumerable_() && descriptor.enumerable_(),
476 writable: !is_accessor && descriptor.hasWritable_() && descriptor.writable_(),
477 is_accessor,
478 });
479 }
480
481 let (class, kind, function, array_length, items) = match object_class {
482 ESClass::Array => {
483 let mut len = 0u32;
484 if !unsafe { GetArrayLength(cx, object.handle(), &mut len) } {
485 return None;
486 }
487 items.sort_by_key(|(index, _)| *index);
488 let ordered: Vec<DebuggerValue> =
489 items.into_iter().map(|(_, value)| value).collect();
490 (
491 "Array".into(),
492 "ArrayLike".into(),
493 None,
494 Some(len),
495 Some(ordered),
496 )
497 },
498 ESClass::Function => {
499 rooted!(&in(cx) let fun = unsafe { JS_ValueToFunction(cx, handle_value) });
500 rooted!(&in(cx) let mut name = std::ptr::null_mut::<jsapi::JSString>());
501 rooted!(&in(cx) let mut display_name = std::ptr::null_mut::<jsapi::JSString>());
502 let arity;
503 unsafe {
504 JS_GetFunctionId(cx, fun.handle(), name.handle_mut());
505 JS_GetFunctionDisplayId(cx, fun.handle(), display_name.handle_mut());
506 arity = JS_GetFunctionArity(fun.get());
507 }
508 let name =
509 ptr::NonNull::new(*name).map(|name| unsafe { jsstr_to_string(cx, name) });
510 let display_name = ptr::NonNull::new(*display_name)
511 .map(|display_name| unsafe { jsstr_to_string(cx, display_name) });
512
513 let parameter_names = (0..arity).map(|i| format!("<arg{i}>")).collect();
516
517 let function = Box::new(FunctionPreview {
518 name,
519 display_name,
520 parameter_names,
521 is_async: None,
522 is_generator: None,
523 });
524 (
525 "Function".into(),
526 "Object".into(),
527 Some(function),
528 None,
529 None,
530 )
531 },
532 _ => ("Object".into(), "Object".into(), None, None, None),
534 };
535
536 Some((
537 class,
538 ObjectPreview {
539 kind,
540 size: None,
541 entries: None,
542 own_properties_length: Some(own_properties.len() as u32),
543 own_properties: Some(own_properties),
544 function,
545 array_length,
546 items,
547 },
548 ))
549 }
550
551 pub(super) fn string_array_debugger_value(strings: &[DOMString]) -> DebuggerValue {
552 let items: Vec<DebuggerValue> = strings
553 .iter()
554 .map(|s| DebuggerValue::StringValue(s.to_string()))
555 .collect();
556 DebuggerValue::ObjectValue {
557 actor: None,
558 class: "Array".into(),
559 own_property_length: Some(0),
560 preview: Some(Box::new(ObjectPreview {
561 kind: "ArrayLike".into(),
562 size: None,
563 entries: None,
564 own_properties_length: Some(0),
565 own_properties: None,
566 function: None,
567 array_length: Some(items.len() as u32),
568 items: Some(items),
569 })),
570 prototype: None,
571 }
572 }
573}
574
575#[expect(unsafe_code)]
576pub(crate) fn stringify_handle_value(cx: &mut JSContext, message: HandleValue) -> DOMString {
577 if message.is_string() {
578 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
579 return unsafe { jsstr_to_string(cx, jsstr) }.into();
580 }
581 fn stringify_object_from_handle_value(
582 cx: &mut JSContext,
583 value: HandleValue,
584 parents: Vec<u64>,
585 ) -> DOMString {
586 rooted!(&in(cx) let mut obj = value.to_object());
587 let mut object_class = ESClass::Other;
588 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
589 return DOMString::from_static("/* invalid */");
590 }
591 let mut ids = IdVector::new(cx);
592 if !GetPropertyKeys(
593 cx,
594 obj.handle(),
595 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
596 ids.handle_mut(),
597 ) {
598 return DOMString::from_static("/* invalid */");
599 }
600 let truncate = ids.len() > MAX_LOG_CHILDREN;
601 if object_class != ESClass::Array && object_class != ESClass::Object {
602 if truncate {
603 return DOMString::from_static("…");
604 } else {
605 return handle_value_to_string(cx, value);
606 }
607 }
608
609 let mut explicit_keys = object_class == ESClass::Object;
610 let mut props = Vec::with_capacity(ids.len());
611 for id in ids.iter().take(MAX_LOG_CHILDREN) {
612 rooted!(&in(cx) let id = *id);
613 rooted!(&in(cx) let mut desc = PropertyDescriptor::default());
614
615 let mut is_none = false;
616 if !JS_GetOwnPropertyDescriptorById(
617 cx,
618 obj.handle(),
619 id.handle(),
620 desc.handle_mut(),
621 &mut is_none,
622 ) {
623 return DOMString::from_static("/* invalid */");
624 }
625
626 rooted!(&in(cx) let mut property = UndefinedValue());
627 if !JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut()) {
628 return DOMString::from_static("/* invalid */");
629 }
630
631 if !explicit_keys {
632 if id.is_int() {
633 if let Ok(id_int) = usize::try_from(id.to_int()) {
634 explicit_keys = props.len() != id_int;
635 } else {
636 explicit_keys = false;
637 }
638 } else {
639 explicit_keys = false;
640 }
641 }
642 let value_string = stringify_inner(cx, property.handle(), parents.clone());
643 if explicit_keys {
644 let key = if id.is_string() || id.is_symbol() || id.is_int() {
645 rooted!(&in(cx) let mut key_value = UndefinedValue());
646 if !JS_IdToValue(cx, id.handle().get(), key_value.handle_mut()) {
647 return DOMString::from_static("/* invalid */");
648 }
649 handle_value_to_string(cx, key_value.handle())
650 } else {
651 return DOMString::from_static("/* invalid */");
652 };
653 props.push(format!("{}: {}", key, value_string,));
654 } else {
655 props.push(String::from(value_string));
656 }
657 }
658 if truncate {
659 props.push("…".to_string());
660 }
661 if object_class == ESClass::Array {
662 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
663 } else {
664 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
665 }
666 }
667 fn stringify_inner(cx: &mut JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
668 if parents.len() >= MAX_LOG_DEPTH {
669 return DOMString::from_static("...");
670 }
671 let value_bits = value.asBits_;
672 if parents.contains(&value_bits) {
673 return DOMString::from_static("[circular]");
674 }
675 if value.is_undefined() {
676 return DOMString::from_static("undefined");
678 } else if !value.is_object() {
679 return handle_value_to_string(cx, value);
680 }
681 parents.push(value_bits);
682
683 if value.is_object() &&
684 let Some(repr) = maybe_stringify_dom_object(cx, value)
685 {
686 return repr;
687 }
688 stringify_object_from_handle_value(cx, value, parents)
689 }
690 stringify_inner(cx, message, Vec::new())
691}
692
693#[expect(unsafe_code)]
694fn maybe_stringify_dom_object(cx: &mut JSContext, value: HandleValue) -> Option<DOMString> {
695 rooted!(&in(cx) let obj = value.to_object());
700 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
701 if !is_dom_class {
702 return None;
703 }
704 rooted!(&in(cx) let class_name = unsafe { ToString(cx, value) });
705 let Some(class_name) = NonNull::new(class_name.get()) else {
706 return Some(DOMString::from_static(
707 "<error converting DOM object to string>",
708 ));
709 };
710 let class_name = unsafe { jsstr_to_string(cx, class_name) }
711 .replace("[object ", "")
712 .replace("]", "");
713 let mut repr = format!("{} ", class_name);
714 rooted!(&in(cx) let mut value = value.get());
715
716 #[expect(unsafe_code)]
717 unsafe extern "C" fn stringified(
718 string: *const u16,
719 len: u32,
720 data: *mut std::ffi::c_void,
721 ) -> bool {
722 let s = data as *mut String;
723 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
724 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
725 true
726 }
727
728 rooted!(&in(cx) let space = Int32Value(2));
729 let stringify_result = unsafe {
730 JS_Stringify(
731 cx,
732 value.handle_mut(),
733 HandleObject::null(),
734 space.handle(),
735 Some(stringified),
736 &mut repr as *mut String as *mut _,
737 )
738 };
739 if !stringify_result {
740 return Some(DOMString::from_static(
741 "<error converting DOM object to string>",
742 ));
743 }
744 Some(repr.into())
745}
746
747#[expect(unsafe_code)]
755fn apply_sprintf_substitutions(cx: &mut JSContext, messages: &[HandleValue]) -> (String, usize) {
756 debug_assert!(!messages.is_empty() && messages[0].is_string());
757
758 let js_string = ptr::NonNull::new(messages[0].to_string()).unwrap();
759 let format_string = unsafe { jsstr_to_string(cx, js_string) };
760
761 let mut result = String::new();
762 let mut arg_index = 1usize;
763 let mut chars = format_string.chars().peekable();
764
765 while let Some(c) = chars.next() {
766 if c != '%' {
767 result.push(c);
768 continue;
769 }
770
771 match chars.peek().copied() {
772 Some('s') => {
773 chars.next();
774 if arg_index < messages.len() {
775 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
776 arg_index += 1;
777 } else {
778 result.push_str("%s");
779 }
780 },
781 Some('d') | Some('i') => {
782 let spec = chars.next().unwrap();
783 if arg_index < messages.len() {
784 let num = unsafe { ToNumber(cx, messages[arg_index]) };
785 if num.is_err() {
786 unsafe { JS_ClearPendingException(cx) };
787 }
788 arg_index += 1;
789 format_integer_substitution(&mut result, num);
790 } else {
791 result.push('%');
792 result.push(spec);
793 }
794 },
795 Some('f') => {
796 chars.next();
797 if arg_index < messages.len() {
798 let num = unsafe { ToNumber(cx, messages[arg_index]) };
799 if num.is_err() {
800 unsafe { JS_ClearPendingException(cx) };
801 }
802 arg_index += 1;
803 format_float_substitution(&mut result, num);
804 } else {
805 result.push_str("%f");
806 }
807 },
808 Some('o') | Some('O') => {
809 let spec = chars.next().unwrap();
810 if arg_index < messages.len() {
811 result.push_str(&stringify_handle_value(cx, messages[arg_index]).str());
812 arg_index += 1;
813 } else {
814 result.push('%');
815 result.push(spec);
816 }
817 },
818 Some('c') => {
819 chars.next();
820 if arg_index < messages.len() {
821 arg_index += 1; }
823 },
824 Some('%') => {
825 chars.next();
826 result.push('%');
827 },
828 _ => {
829 result.push('%');
830 },
831 }
832 }
833
834 (result, arg_index)
835}
836
837fn format_integer_substitution(result: &mut String, num: Result<f64, ()>) {
838 match num {
839 Ok(n) if n.is_nan() => result.push_str("NaN"),
840 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
841 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
842 Ok(n) => write!(result, "{}", n.trunc() as i64).unwrap(),
843 Err(_) => result.push_str("NaN"),
844 }
845}
846
847fn format_float_substitution(result: &mut String, num: Result<f64, ()>) {
848 match num {
849 Ok(n) if n.is_nan() => result.push_str("NaN"),
850 Ok(n) if n == f64::INFINITY => result.push_str("Infinity"),
851 Ok(n) if n == f64::NEG_INFINITY => result.push_str("-Infinity"),
852 Ok(n) => write!(result, "{n}").unwrap(),
853 Err(_) => result.push_str("NaN"),
854 }
855}
856
857fn stringify_handle_values(cx: &mut JSContext, messages: &[HandleValue]) -> DOMString {
858 DOMString::from(itertools::join(
859 messages
860 .iter()
861 .copied()
862 .map(|msg| stringify_handle_value(cx, msg)),
863 " ",
864 ))
865}
866
867#[cfg(feature = "devtools")]
870fn stringify_debugger_value(value: &DebuggerValue) -> String {
871 match value {
872 DebuggerValue::VoidValue => "undefined".into(),
873 DebuggerValue::NullValue(_) => "null".into(),
874 DebuggerValue::BooleanValue(value) => value.to_string(),
875 DebuggerValue::NumberValue(value) => value.to_string(),
876 DebuggerValue::StringValue(value) => value.clone(),
877 DebuggerValue::ObjectValue { class, preview, .. } => {
878 let Some(preview) = preview else {
879 return class.clone();
880 };
881
882 if preview.kind == "ArrayLike" {
883 let mut items = preview
884 .items
885 .as_ref()
886 .map(|items| {
887 items
888 .iter()
889 .take(MAX_LOG_CHILDREN)
890 .map(stringify_debugger_value)
891 .collect::<Vec<_>>()
892 })
893 .unwrap_or_default();
894 if preview
895 .array_length
896 .is_some_and(|length| length as usize > items.len())
897 {
898 items.push("...".into());
899 }
900 return format!("[{}]", itertools::join(items, ", "));
901 }
902
903 let mut properties = preview
904 .own_properties
905 .as_ref()
906 .map(|properties| {
907 properties
908 .iter()
909 .take(MAX_LOG_CHILDREN)
910 .map(|property| {
911 format!(
912 "{}: {}",
913 property.name,
914 stringify_debugger_value(&property.value)
915 )
916 })
917 .collect::<Vec<_>>()
918 })
919 .unwrap_or_default();
920 if preview
921 .own_properties_length
922 .is_some_and(|length| length as usize > properties.len())
923 {
924 properties.push("...".into());
925 }
926 format!("{class} {{{}}}", itertools::join(properties, ", "))
927 },
928 }
929}
930
931#[derive(Default)]
932struct ConsoleTable {
933 columns: Vec<String>,
935 rows: Vec<TableRow>,
936}
937
938struct TableRow {
939 index: String,
940 cells: Vec<(String, String)>,
942 value: Option<String>,
944}
945
946impl ConsoleTable {
947 fn render(&self) -> String {
948 let has_values = self.rows.iter().any(|row| row.value.is_some());
949
950 let mut headers = vec!["(index)".to_string()];
951 headers.extend(self.columns.iter().cloned());
952 if has_values {
953 headers.push("Values".into());
954 }
955
956 let grid: Vec<Vec<String>> = self
957 .rows
958 .iter()
959 .map(|row| {
960 let mut line = vec![row.index.clone()];
961 for column in &self.columns {
962 let cell = row
963 .cells
964 .iter()
965 .find(|(key, _)| key == column)
966 .map(|(_, v)| v.clone())
967 .unwrap_or_default();
968 line.push(cell);
969 }
970 if has_values {
971 line.push(row.value.clone().unwrap_or_default());
972 }
973 line
974 })
975 .collect();
976
977 let widths: Vec<usize> = (0..headers.len())
978 .map(|i| {
979 std::iter::once(&headers[i])
980 .chain(grid.iter().map(|row| &row[i]))
981 .map(|s| s.chars().count())
982 .max()
983 .unwrap_or(0)
984 })
985 .collect();
986
987 let border = |left: &str, mid: &str, right: &str| {
988 let segments: Vec<String> = widths.iter().map(|w| "─".repeat(w + 2)).collect();
989 format!("{left}{}{right}", segments.join(mid))
990 };
991 let line = |cells: &[String]| {
992 let padded: Vec<String> = cells
993 .iter()
994 .zip(&widths)
995 .map(|(cell, &w)| format!(" {cell:<w$} "))
996 .collect();
997 format!("│{}│", padded.join("│"))
998 };
999
1000 let mut out = vec![border("┌", "┬", "┐"), line(&headers), border("├", "┼", "┤")];
1001 out.extend(grid.iter().map(|row| line(row)));
1002 out.push(border("└", "┴", "┘"));
1003 out.join("\n")
1004 }
1005}
1006
1007fn for_each_own_property(
1010 cx: &mut JSContext,
1011 obj: HandleObject,
1012 mut f: impl FnMut(&mut JSContext, String, HandleValue),
1013) -> bool {
1014 let mut ids = IdVector::new(cx);
1015 if !GetPropertyKeys(cx, obj, jsapi::JSITER_OWNONLY, ids.handle_mut()) {
1017 return false;
1018 }
1019 for id in ids.iter() {
1020 rooted!(&in(cx) let id = *id);
1021 rooted!(&in(cx) let mut key = UndefinedValue());
1022 if !JS_IdToValue(cx, id.handle().get(), key.handle_mut()) {
1023 return false;
1024 }
1025 let key = stringify_handle_value(cx, key.handle()).to_string();
1026
1027 rooted!(&in(cx) let mut value = UndefinedValue());
1028 if !JS_GetPropertyById(cx, obj, id.handle(), value.handle_mut()) {
1030 return false;
1031 }
1032 f(cx, key, value.handle());
1033 }
1034 true
1035}
1036
1037fn table_cell_string(cx: &mut JSContext, value: HandleValue) -> String {
1038 if value.is_string() {
1039 handle_value_to_string(cx, value).to_string() } else {
1041 stringify_handle_value(cx, value).to_string()
1042 }
1043}
1044
1045#[expect(unsafe_code)]
1046fn build_console_table(
1047 cx: &mut JSContext,
1048 data: HandleValue,
1049 properties: Option<&[DOMString]>,
1050) -> Option<ConsoleTable> {
1051 if !data.is_object() {
1052 return None;
1053 }
1054 let filter: Option<Vec<String>> =
1055 properties.map(|props| props.iter().map(|p| p.to_string()).collect());
1056
1057 let mut table = ConsoleTable::default();
1058 if let Some(filter) = &filter {
1059 table.columns = filter.clone();
1061 }
1062
1063 rooted!(&in(cx) let data_obj = data.to_object());
1064 let mut failed = false;
1065 let ok = for_each_own_property(cx, data_obj.handle(), |cx, index, row| {
1066 let mut cells = Vec::new();
1067 let mut value = None;
1068
1069 if row.is_object() {
1070 rooted!(&in(cx) let row_obj = row.to_object());
1071 let columns = &mut table.columns;
1072 let ok = for_each_own_property(cx, row_obj.handle(), |cx, key, cell| {
1073 match &filter {
1074 Some(filter) if !filter.contains(&key) => return,
1075 None if !columns.contains(&key) => columns.push(key.clone()),
1076 _ => {},
1077 }
1078 cells.push((key, table_cell_string(cx, cell)));
1079 });
1080 failed |= !ok;
1081 } else {
1082 value = Some(table_cell_string(cx, row));
1083 }
1084
1085 table.rows.push(TableRow {
1086 index,
1087 cells,
1088 value,
1089 });
1090 });
1091
1092 if !ok || failed {
1093 unsafe { JS_ClearPendingException(cx) };
1094 return None;
1095 }
1096 Some(table)
1097}
1098
1099#[derive(Debug, Eq, PartialEq)]
1100enum IncludeStackTrace {
1101 Yes,
1102 No,
1103}
1104
1105impl consoleMethods<crate::DomTypeHolder> for Console {
1106 fn Log(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1108 Console::method(
1109 cx,
1110 global,
1111 ConsoleLogLevel::Log,
1112 messages,
1113 IncludeStackTrace::No,
1114 );
1115 }
1116
1117 fn Table(
1119 cx: &mut JSContext,
1120 global: &GlobalScope,
1121 tabular_data: HandleValue,
1122 properties: Option<Vec<DOMString>>,
1123 ) {
1124 let Some(table) = build_console_table(cx, tabular_data, properties.as_deref()) else {
1125 Console::method(
1127 cx,
1128 global,
1129 ConsoleLogLevel::Log,
1130 vec![tabular_data],
1131 IncludeStackTrace::No,
1132 );
1133 return;
1134 };
1135
1136 #[cfg(feature = "devtools")]
1137 {
1138 let data =
1139 devtools::console_argument_from_handle_value(cx, tabular_data, &mut Vec::new());
1140
1141 let level = if matches!(data, DebuggerValue::ObjectValue { .. }) {
1144 ConsoleLogLevel::Table
1145 } else {
1146 ConsoleLogLevel::Log
1147 };
1148
1149 let mut arguments = vec![data];
1150 if let Some(properties) = &properties {
1151 arguments.push(devtools::string_array_debugger_value(properties));
1152 }
1153
1154 Console::send_to_devtools(global, Self::build_message(cx, level, arguments, None));
1155 }
1156
1157 let prefix = global.current_group_label().unwrap_or_default();
1158 Self::send_to_embedder(
1159 global,
1160 ConsoleLogLevel::Table,
1161 format!("{prefix}\n{}", table.render()),
1162 );
1163 }
1164
1165 fn Clear(global: &GlobalScope) {
1167 #[cfg(feature = "devtools")]
1168 if let Some(chan) = global.devtools_chan() {
1169 let worker_id = global
1170 .downcast::<WorkerGlobalScope>()
1171 .map(|worker| worker.worker_id());
1172 let devtools_message =
1173 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
1174 if let Err(error) = chan.send(devtools_message) {
1175 log::warn!("Error sending clear message to devtools: {error:?}");
1176 }
1177 }
1178
1179 let _ = global;
1180 }
1181
1182 fn Debug(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1184 Console::method(
1185 cx,
1186 global,
1187 ConsoleLogLevel::Debug,
1188 messages,
1189 IncludeStackTrace::No,
1190 );
1191 }
1192
1193 fn Info(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1195 Console::method(
1196 cx,
1197 global,
1198 ConsoleLogLevel::Info,
1199 messages,
1200 IncludeStackTrace::No,
1201 );
1202 }
1203
1204 fn Warn(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1206 Console::method(
1207 cx,
1208 global,
1209 ConsoleLogLevel::Warn,
1210 messages,
1211 IncludeStackTrace::No,
1212 );
1213 }
1214
1215 fn Error(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1217 Console::method(
1218 cx,
1219 global,
1220 ConsoleLogLevel::Error,
1221 messages,
1222 IncludeStackTrace::No,
1223 );
1224 }
1225
1226 fn Trace(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1228 Console::method(
1229 cx,
1230 global,
1231 ConsoleLogLevel::Trace,
1232 messages,
1233 IncludeStackTrace::Yes,
1234 );
1235 }
1236
1237 fn Dir(
1239 cx: &mut js::context::JSContext,
1240 global: &GlobalScope,
1241 item: HandleValue,
1242 _options: Option<*mut jsapi::JSObject>,
1243 ) {
1244 #[cfg(feature = "devtools")]
1245 {
1246 let argument = devtools::console_argument_from_handle_value(cx, item, &mut Vec::new());
1248 let prefix = global.current_group_label().unwrap_or_default();
1249 Console::send_to_devtools(
1251 global,
1252 Self::build_message(cx, ConsoleLogLevel::Dir, vec![argument.clone()], None),
1253 );
1254 Self::send_to_embedder(
1255 global,
1256 ConsoleLogLevel::Dir,
1257 format!("{prefix}{}", stringify_debugger_value(&argument)),
1258 );
1259 }
1260
1261 #[cfg(not(feature = "devtools"))]
1262 {
1263 let prefix = global.current_group_label().unwrap_or_default();
1264 Self::send_to_embedder(
1265 global,
1266 ConsoleLogLevel::Dir,
1267 format!("{prefix}{}", stringify_handle_value(cx, item)),
1268 );
1269 }
1270 }
1271
1272 fn Assert(
1274 cx: &mut JSContext,
1275 global: &GlobalScope,
1276 condition: bool,
1277 messages: Vec<HandleValue>,
1278 ) {
1279 if !condition {
1280 let message = format!(
1281 "Assertion failed: {}",
1282 stringify_handle_values(cx, &messages)
1283 );
1284
1285 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1286 }
1287 }
1288
1289 fn Time(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1291 if let Ok(()) = global.time(label.clone()) {
1292 let message = format!("{label}: timer started");
1293 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1294 }
1295 }
1296
1297 fn TimeLog(cx: &mut JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
1299 if let Ok(delta) = global.time_log(&label) {
1300 let message = format!("{label}: {delta}ms {}", stringify_handle_values(cx, &data));
1301
1302 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1303 }
1304 }
1305
1306 fn TimeEnd(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1308 if let Ok(delta) = global.time_end(&label) {
1309 let message = format!("{label}: {delta}ms");
1310
1311 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1312 }
1313 }
1314
1315 fn Group(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1317 global.push_console_group(stringify_handle_values(cx, &messages));
1318 }
1319
1320 fn GroupCollapsed(cx: &mut JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
1322 global.push_console_group(stringify_handle_values(cx, &messages));
1323 }
1324
1325 fn GroupEnd(global: &GlobalScope) {
1327 global.pop_console_group();
1328 }
1329
1330 fn Count(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1332 let count = global.increment_console_count(&label);
1333 let message = format!("{label}: {count}");
1334
1335 Console::send_string_message(cx, global, ConsoleLogLevel::Log, message);
1336 }
1337
1338 fn CountReset(cx: &mut JSContext, global: &GlobalScope, label: DOMString) {
1340 if global.reset_console_count(&label).is_err() {
1341 Self::internal_warn(cx, global, format!("Counter “{label}” doesn’t exist."))
1342 }
1343 }
1344}
1345
1346#[cfg(feature = "devtools")]
1347#[expect(unsafe_code)]
1348fn get_js_stack(cx: &mut JSContext) -> Vec<StackFrame> {
1349 const MAX_FRAME_COUNT: u32 = 128;
1350
1351 let mut frames = vec![];
1352 rooted!(&in(cx) let mut handle = ptr::null_mut());
1353 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
1354 let Some(mut captured_js_stack) = captured_js_stack else {
1355 return frames;
1356 };
1357
1358 captured_js_stack.for_each_stack_frame(|cx, frame| {
1359 rooted!(&in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
1360
1361 unsafe {
1363 GetSavedFrameFunctionDisplayName(
1364 cx,
1365 ptr::null_mut(),
1366 frame,
1367 result.handle_mut(),
1368 SavedFrameSelfHosted::Include,
1369 );
1370 }
1371 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1372 unsafe { jsstr_to_string(cx, nonnull_result) }
1373 } else {
1374 "<anonymous>".into()
1375 };
1376
1377 result.set(ptr::null_mut());
1379 unsafe {
1380 GetSavedFrameSource(
1381 cx,
1382 ptr::null_mut(),
1383 frame,
1384 result.handle_mut(),
1385 SavedFrameSelfHosted::Include,
1386 );
1387 }
1388 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
1389 unsafe { jsstr_to_string(cx, nonnull_result) }
1390 } else {
1391 "<anonymous>".into()
1392 };
1393
1394 let mut line_number = 0;
1396 unsafe {
1397 GetSavedFrameLine(
1398 cx,
1399 ptr::null_mut(),
1400 frame,
1401 &mut line_number,
1402 SavedFrameSelfHosted::Include,
1403 );
1404 }
1405
1406 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
1407 unsafe {
1408 GetSavedFrameColumn(
1409 cx,
1410 ptr::null_mut(),
1411 frame,
1412 &mut column_number,
1413 SavedFrameSelfHosted::Include,
1414 );
1415 }
1416 let frame = StackFrame {
1417 filename,
1418 function_name,
1419 line_number,
1420 column_number: column_number.value_,
1421 };
1422
1423 frames.push(frame);
1424 });
1425
1426 frames
1427}