1use std::convert::TryFrom;
6use std::ptr::{self, NonNull};
7use std::slice;
8
9use devtools_traits::{
10 ConsoleArgument, ConsoleLogLevel, ConsoleMessage, ConsoleMessageFields,
11 ScriptToDevtoolsControlMsg, StackFrame, get_time_stamp,
12};
13use embedder_traits::EmbedderMsg;
14use js::conversions::jsstr_to_string;
15use js::jsapi::{self, ESClass, PropertyDescriptor};
16use js::jsval::{Int32Value, UndefinedValue};
17use js::rust::wrappers::{
18 GetBuiltinClass, GetPropertyKeys, JS_GetOwnPropertyDescriptorById, JS_GetPropertyById,
19 JS_IdToValue, JS_Stringify, JS_ValueToSource,
20};
21use js::rust::{
22 CapturedJSStack, HandleObject, HandleValue, IdVector, ToString, describe_scripted_caller,
23};
24use script_bindings::conversions::get_dom_class;
25
26use crate::dom::bindings::codegen::Bindings::ConsoleBinding::consoleMethods;
27use crate::dom::bindings::inheritance::Castable;
28use crate::dom::bindings::str::DOMString;
29use crate::dom::globalscope::GlobalScope;
30use crate::dom::workerglobalscope::WorkerGlobalScope;
31use crate::script_runtime::JSContext;
32
33const MAX_LOG_DEPTH: usize = 10;
35const MAX_LOG_CHILDREN: usize = 15;
37
38#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
40pub(crate) struct Console;
41
42impl Console {
43 #[expect(unsafe_code)]
44 fn build_message(
45 level: ConsoleLogLevel,
46 arguments: Vec<ConsoleArgument>,
47 stacktrace: Option<Vec<StackFrame>>,
48 ) -> ConsoleMessage {
49 let cx = GlobalScope::get_cx();
50 let caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
51
52 ConsoleMessage {
53 fields: ConsoleMessageFields {
54 level,
55 filename: caller.filename,
56 line_number: caller.line,
57 column_number: caller.col,
58 time_stamp: get_time_stamp(),
59 },
60 arguments,
61 stacktrace,
62 }
63 }
64
65 fn send_string_message(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
67 let prefix = global.current_group_label().unwrap_or_default();
68 let formatted_message = format!("{prefix}{message}");
69
70 Self::send_to_embedder(global, level.clone(), formatted_message);
71
72 let console_message = Self::build_message(level, vec![message.into()], None);
73
74 Self::send_to_devtools(global, console_message);
75 }
76
77 fn method(
78 global: &GlobalScope,
79 level: ConsoleLogLevel,
80 messages: Vec<HandleValue>,
81 include_stacktrace: IncludeStackTrace,
82 ) {
83 let cx = GlobalScope::get_cx();
84
85 let arguments = messages
86 .iter()
87 .map(|msg| console_argument_from_handle_value(cx, *msg))
88 .collect();
89 let stacktrace = (include_stacktrace == IncludeStackTrace::Yes)
90 .then_some(get_js_stack(*GlobalScope::get_cx()));
91 let console_message = Self::build_message(level.clone(), arguments, stacktrace);
92
93 Console::send_to_devtools(global, console_message);
94
95 let prefix = global.current_group_label().unwrap_or_default();
96 let msgs = stringify_handle_values(&messages);
97 let formatted_message = format!("{prefix}{msgs}");
98
99 Self::send_to_embedder(global, level, formatted_message);
100 }
101
102 fn send_to_devtools(global: &GlobalScope, message: ConsoleMessage) {
103 if let Some(chan) = global.devtools_chan() {
104 let worker_id = global
105 .downcast::<WorkerGlobalScope>()
106 .map(|worker| worker.worker_id());
107 let devtools_message =
108 ScriptToDevtoolsControlMsg::ConsoleAPI(global.pipeline_id(), message, worker_id);
109 chan.send(devtools_message).unwrap();
110 }
111 }
112
113 fn send_to_embedder(global: &GlobalScope, level: ConsoleLogLevel, message: String) {
114 global.send_to_embedder(EmbedderMsg::ShowConsoleApiMessage(
115 global.webview_id(),
116 level,
117 message,
118 ));
119 }
120
121 pub(crate) fn internal_warn(global: &GlobalScope, message: DOMString) {
123 Console::send_string_message(global, ConsoleLogLevel::Warn, String::from(message.clone()));
124 }
125}
126
127#[expect(unsafe_code)]
128unsafe fn handle_value_to_string(cx: *mut jsapi::JSContext, value: HandleValue) -> DOMString {
129 rooted!(in(cx) let mut js_string = std::ptr::null_mut::<jsapi::JSString>());
130 match std::ptr::NonNull::new(unsafe { JS_ValueToSource(cx, value) }) {
131 Some(js_str) => {
132 js_string.set(js_str.as_ptr());
133 DOMString::from_string(unsafe { jsstr_to_string(cx, js_str) })
134 },
135 None => "<error converting value to string>".into(),
136 }
137}
138
139#[expect(unsafe_code)]
140fn console_argument_from_handle_value(cx: JSContext, handle_value: HandleValue) -> ConsoleArgument {
141 if handle_value.is_string() {
142 let js_string = ptr::NonNull::new(handle_value.to_string()).unwrap();
143 let dom_string = unsafe { jsstr_to_string(*cx, js_string) };
144 return ConsoleArgument::String(dom_string);
145 }
146
147 if handle_value.is_int32() {
148 let integer = handle_value.to_int32();
149 return ConsoleArgument::Integer(integer);
150 }
151
152 if handle_value.is_number() {
153 let number = handle_value.to_number();
154 return ConsoleArgument::Number(number);
155 }
156
157 let stringified_value = stringify_handle_value(handle_value);
159 ConsoleArgument::String(stringified_value.into())
160}
161
162#[expect(unsafe_code)]
163fn stringify_handle_value(message: HandleValue) -> DOMString {
164 let cx = GlobalScope::get_cx();
165 unsafe {
166 if message.is_string() {
167 let jsstr = std::ptr::NonNull::new(message.to_string()).unwrap();
168 return DOMString::from_string(jsstr_to_string(*cx, jsstr));
169 }
170 unsafe fn stringify_object_from_handle_value(
171 cx: *mut jsapi::JSContext,
172 value: HandleValue,
173 parents: Vec<u64>,
174 ) -> DOMString {
175 rooted!(in(cx) let mut obj = value.to_object());
176 let mut object_class = ESClass::Other;
177 if !unsafe { GetBuiltinClass(cx, obj.handle(), &mut object_class as *mut _) } {
178 return DOMString::from("/* invalid */");
179 }
180 let mut ids = unsafe { IdVector::new(cx) };
181 if !unsafe {
182 GetPropertyKeys(
183 cx,
184 obj.handle(),
185 jsapi::JSITER_OWNONLY | jsapi::JSITER_SYMBOLS,
186 ids.handle_mut(),
187 )
188 } {
189 return DOMString::from("/* invalid */");
190 }
191 let truncate = ids.len() > MAX_LOG_CHILDREN;
192 if object_class != ESClass::Array && object_class != ESClass::Object {
193 if truncate {
194 return DOMString::from("…");
195 } else {
196 return unsafe { handle_value_to_string(cx, value) };
197 }
198 }
199
200 let mut explicit_keys = object_class == ESClass::Object;
201 let mut props = Vec::with_capacity(ids.len());
202 for id in ids.iter().take(MAX_LOG_CHILDREN) {
203 rooted!(in(cx) let id = *id);
204 rooted!(in(cx) let mut desc = PropertyDescriptor::default());
205
206 let mut is_none = false;
207 if !unsafe {
208 JS_GetOwnPropertyDescriptorById(
209 cx,
210 obj.handle(),
211 id.handle(),
212 desc.handle_mut(),
213 &mut is_none,
214 )
215 } {
216 return DOMString::from("/* invalid */");
217 }
218
219 rooted!(in(cx) let mut property = UndefinedValue());
220 if !unsafe {
221 JS_GetPropertyById(cx, obj.handle(), id.handle(), property.handle_mut())
222 } {
223 return DOMString::from("/* invalid */");
224 }
225
226 if !explicit_keys {
227 if id.is_int() {
228 if let Ok(id_int) = usize::try_from(id.to_int()) {
229 explicit_keys = props.len() != id_int;
230 } else {
231 explicit_keys = false;
232 }
233 } else {
234 explicit_keys = false;
235 }
236 }
237 let value_string = stringify_inner(
238 unsafe { JSContext::from_ptr(cx) },
239 property.handle(),
240 parents.clone(),
241 );
242 if explicit_keys {
243 let key = if id.is_string() || id.is_symbol() || id.is_int() {
244 rooted!(in(cx) let mut key_value = UndefinedValue());
245 let raw_id: jsapi::HandleId = id.handle().into();
246 if !unsafe { JS_IdToValue(cx, *raw_id.ptr, key_value.handle_mut()) } {
247 return DOMString::from("/* invalid */");
248 }
249 unsafe { handle_value_to_string(cx, key_value.handle()) }
250 } else {
251 return DOMString::from("/* invalid */");
252 };
253 props.push(format!("{}: {}", key, value_string,));
254 } else {
255 props.push(value_string.to_string());
256 }
257 }
258 if truncate {
259 props.push("…".to_string());
260 }
261 if object_class == ESClass::Array {
262 DOMString::from(format!("[{}]", itertools::join(props, ", ")))
263 } else {
264 DOMString::from(format!("{{{}}}", itertools::join(props, ", ")))
265 }
266 }
267 fn stringify_inner(cx: JSContext, value: HandleValue, mut parents: Vec<u64>) -> DOMString {
268 if parents.len() >= MAX_LOG_DEPTH {
269 return DOMString::from("...");
270 }
271 let value_bits = value.asBits_;
272 if parents.contains(&value_bits) {
273 return DOMString::from("[circular]");
274 }
275 if value.is_undefined() {
276 return DOMString::from("undefined");
278 } else if !value.is_object() {
279 return unsafe { handle_value_to_string(*cx, value) };
280 }
281 parents.push(value_bits);
282
283 if value.is_object() {
284 if let Some(repr) = maybe_stringify_dom_object(cx, value) {
285 return repr;
286 }
287 }
288 unsafe { stringify_object_from_handle_value(*cx, value, parents) }
289 }
290 stringify_inner(cx, message, Vec::new())
291 }
292}
293
294#[expect(unsafe_code)]
295fn maybe_stringify_dom_object(cx: JSContext, value: HandleValue) -> Option<DOMString> {
296 rooted!(in(*cx) let obj = value.to_object());
301 let is_dom_class = unsafe { get_dom_class(obj.get()).is_ok() };
302 if !is_dom_class {
303 return None;
304 }
305 rooted!(in(*cx) let class_name = unsafe { ToString(*cx, value) });
306 let Some(class_name) = NonNull::new(class_name.get()) else {
307 return Some("<error converting DOM object to string>".into());
308 };
309 let class_name = unsafe {
310 jsstr_to_string(*cx, class_name)
311 .replace("[object ", "")
312 .replace("]", "")
313 };
314 let mut repr = format!("{} ", class_name);
315 rooted!(in(*cx) let mut value = value.get());
316
317 #[expect(unsafe_code)]
318 unsafe extern "C" fn stringified(
319 string: *const u16,
320 len: u32,
321 data: *mut std::ffi::c_void,
322 ) -> bool {
323 let s = data as *mut String;
324 let string_chars = unsafe { slice::from_raw_parts(string, len as usize) };
325 unsafe { (*s).push_str(&String::from_utf16_lossy(string_chars)) };
326 true
327 }
328
329 rooted!(in(*cx) let space = Int32Value(2));
330 let stringify_result = unsafe {
331 JS_Stringify(
332 *cx,
333 value.handle_mut(),
334 HandleObject::null(),
335 space.handle(),
336 Some(stringified),
337 &mut repr as *mut String as *mut _,
338 )
339 };
340 if !stringify_result {
341 return Some("<error converting DOM object to string>".into());
342 }
343 Some(repr.into())
344}
345
346fn stringify_handle_values(messages: &[HandleValue]) -> DOMString {
347 DOMString::from(itertools::join(
348 messages.iter().copied().map(stringify_handle_value),
349 " ",
350 ))
351}
352
353#[derive(Debug, Eq, PartialEq)]
354enum IncludeStackTrace {
355 Yes,
356 No,
357}
358
359impl consoleMethods<crate::DomTypeHolder> for Console {
360 fn Log(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
362 Console::method(
363 global,
364 ConsoleLogLevel::Log,
365 messages,
366 IncludeStackTrace::No,
367 );
368 }
369
370 fn Clear(global: &GlobalScope) {
372 if let Some(chan) = global.devtools_chan() {
373 let worker_id = global
374 .downcast::<WorkerGlobalScope>()
375 .map(|worker| worker.worker_id());
376 let devtools_message =
377 ScriptToDevtoolsControlMsg::ClearConsole(global.pipeline_id(), worker_id);
378 if let Err(error) = chan.send(devtools_message) {
379 log::warn!("Error sending clear message to devtools: {error:?}");
380 }
381 }
382 }
383
384 fn Debug(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
386 Console::method(
387 global,
388 ConsoleLogLevel::Debug,
389 messages,
390 IncludeStackTrace::No,
391 );
392 }
393
394 fn Info(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
396 Console::method(
397 global,
398 ConsoleLogLevel::Info,
399 messages,
400 IncludeStackTrace::No,
401 );
402 }
403
404 fn Warn(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
406 Console::method(
407 global,
408 ConsoleLogLevel::Warn,
409 messages,
410 IncludeStackTrace::No,
411 );
412 }
413
414 fn Error(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
416 Console::method(
417 global,
418 ConsoleLogLevel::Error,
419 messages,
420 IncludeStackTrace::No,
421 );
422 }
423
424 fn Trace(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
426 Console::method(
427 global,
428 ConsoleLogLevel::Trace,
429 messages,
430 IncludeStackTrace::Yes,
431 );
432 }
433
434 fn Assert(_cx: JSContext, global: &GlobalScope, condition: bool, messages: Vec<HandleValue>) {
436 if !condition {
437 let message = format!("Assertion failed: {}", stringify_handle_values(&messages));
438
439 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
440 }
441 }
442
443 fn Time(global: &GlobalScope, label: DOMString) {
445 if let Ok(()) = global.time(label.clone()) {
446 let message = format!("{label}: timer started");
447 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
448 }
449 }
450
451 fn TimeLog(_cx: JSContext, global: &GlobalScope, label: DOMString, data: Vec<HandleValue>) {
453 if let Ok(delta) = global.time_log(&label) {
454 let message = format!("{label}: {delta}ms {}", stringify_handle_values(&data));
455
456 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
457 }
458 }
459
460 fn TimeEnd(global: &GlobalScope, label: DOMString) {
462 if let Ok(delta) = global.time_end(&label) {
463 let message = format!("{label}: {delta}ms");
464
465 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
466 }
467 }
468
469 fn Group(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
471 global.push_console_group(stringify_handle_values(&messages));
472 }
473
474 fn GroupCollapsed(_cx: JSContext, global: &GlobalScope, messages: Vec<HandleValue>) {
476 global.push_console_group(stringify_handle_values(&messages));
477 }
478
479 fn GroupEnd(global: &GlobalScope) {
481 global.pop_console_group();
482 }
483
484 fn Count(global: &GlobalScope, label: DOMString) {
486 let count = global.increment_console_count(&label);
487 let message = format!("{label}: {count}");
488
489 Console::send_string_message(global, ConsoleLogLevel::Log, message.clone());
490 }
491
492 fn CountReset(global: &GlobalScope, label: DOMString) {
494 if global.reset_console_count(&label).is_err() {
495 Self::internal_warn(
496 global,
497 DOMString::from(format!("Counter “{label}” doesn’t exist.")),
498 )
499 }
500 }
501}
502
503#[expect(unsafe_code)]
504fn get_js_stack(cx: *mut jsapi::JSContext) -> Vec<StackFrame> {
505 const MAX_FRAME_COUNT: u32 = 128;
506
507 let mut frames = vec![];
508 rooted!(in(cx) let mut handle = ptr::null_mut());
509 let captured_js_stack = unsafe { CapturedJSStack::new(cx, handle, Some(MAX_FRAME_COUNT)) };
510 let Some(captured_js_stack) = captured_js_stack else {
511 return frames;
512 };
513
514 captured_js_stack.for_each_stack_frame(|frame| {
515 rooted!(in(cx) let mut result: *mut jsapi::JSString = ptr::null_mut());
516
517 unsafe {
519 jsapi::GetSavedFrameFunctionDisplayName(
520 cx,
521 ptr::null_mut(),
522 frame.into(),
523 result.handle_mut().into(),
524 jsapi::SavedFrameSelfHosted::Include,
525 );
526 }
527 let function_name = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
528 unsafe { jsstr_to_string(cx, nonnull_result) }
529 } else {
530 "<anonymous>".into()
531 };
532
533 result.set(ptr::null_mut());
535 unsafe {
536 jsapi::GetSavedFrameSource(
537 cx,
538 ptr::null_mut(),
539 frame.into(),
540 result.handle_mut().into(),
541 jsapi::SavedFrameSelfHosted::Include,
542 );
543 }
544 let filename = if let Some(nonnull_result) = ptr::NonNull::new(*result) {
545 unsafe { jsstr_to_string(cx, nonnull_result) }
546 } else {
547 "<anonymous>".into()
548 };
549
550 let mut line_number = 0;
552 unsafe {
553 jsapi::GetSavedFrameLine(
554 cx,
555 ptr::null_mut(),
556 frame.into(),
557 &mut line_number,
558 jsapi::SavedFrameSelfHosted::Include,
559 );
560 }
561
562 let mut column_number = jsapi::JS::TaggedColumnNumberOneOrigin { value_: 0 };
563 unsafe {
564 jsapi::GetSavedFrameColumn(
565 cx,
566 ptr::null_mut(),
567 frame.into(),
568 &mut column_number,
569 jsapi::SavedFrameSelfHosted::Include,
570 );
571 }
572 let frame = StackFrame {
573 filename,
574 function_name,
575 line_number,
576 column_number: column_number.value_,
577 };
578
579 frames.push(frame);
580 });
581
582 frames
583}