Skip to main content

devtools/actors/
object.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::collections::HashMap;
6
7use atomic_refcell::AtomicRefCell;
8use devtools_traits::{DebuggerValue, PropertyDescriptor};
9use malloc_size_of_derive::MallocSizeOf;
10use serde::Serialize;
11use serde_json::{Map, Value};
12
13use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry, new_actor_name};
14use crate::actors::property_iterator::{PropertyIteratorActor, PropertyIteratorEntry};
15use crate::actors::symbol_iterator::SymbolIteratorActor;
16use crate::protocol::ClientRequest;
17use crate::{StreamId, debugger_value_to_json};
18
19#[derive(Serialize)]
20#[serde(rename_all = "camelCase")]
21enum EnumIteratorType {
22    PropertyIterator,
23    SymbolIterator,
24}
25
26#[derive(Serialize)]
27struct EnumIterator {
28    actor: String,
29    #[serde(rename = "type")]
30    type_: EnumIteratorType,
31    count: u32,
32}
33
34#[derive(Serialize)]
35struct EnumReply {
36    from: String,
37    iterator: EnumIterator,
38}
39
40#[derive(Serialize)]
41struct PrototypeReply {
42    from: String,
43    prototype: Value,
44}
45
46#[derive(Serialize)]
47#[serde(rename_all = "camelCase")]
48pub(crate) struct ObjectPreview {
49    pub kind: String,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub size: Option<u32>,
52    #[serde(skip_serializing_if = "Option::is_none")]
53    pub entries: Option<Vec<(Value, Value)>>,
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub own_properties: Option<HashMap<String, ObjectPropertyDescriptor>>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub own_properties_length: Option<u32>,
58    #[serde(flatten)]
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub function: Option<FunctionPreview>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub length: Option<u32>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub items: Option<Vec<Value>>,
65}
66
67#[derive(Clone, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct FunctionPreview {
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub name: Option<String>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub display_name: Option<String>,
74    pub parameter_names: Vec<String>,
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub is_async: Option<bool>,
77    #[serde(skip_serializing_if = "Option::is_none")]
78    pub is_generator: Option<bool>,
79}
80
81#[derive(Serialize)]
82#[serde(rename_all = "camelCase")]
83pub(crate) struct ObjectActorMsg {
84    actor: String,
85    #[serde(rename = "type")]
86    type_: String,
87    class: String,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    own_property_length: Option<u32>,
90    extensible: bool,
91    frozen: bool,
92    sealed: bool,
93    #[serde(flatten)]
94    #[serde(skip_serializing_if = "Option::is_none")]
95    function: Option<FunctionPreview>,
96    #[serde(skip_serializing_if = "Option::is_none")]
97    preview: Option<ObjectPreview>,
98}
99
100#[derive(Serialize)]
101#[serde(rename_all = "camelCase")]
102pub(crate) struct ObjectPropertyDescriptor {
103    pub value: Value,
104    #[serde(skip_serializing_if = "Option::is_none")]
105    pub configurable: Option<bool>,
106    pub enumerable: bool,
107    #[serde(skip_serializing_if = "Option::is_none")]
108    pub writable: Option<bool>,
109    #[serde(skip_serializing_if = "Option::is_none")]
110    pub is_accessor: Option<bool>,
111}
112
113impl ObjectPropertyDescriptor {
114    pub(crate) fn from_property_descriptor(
115        registry: &ActorRegistry,
116        prop: &PropertyDescriptor,
117    ) -> Self {
118        Self {
119            value: debugger_value_to_json(registry, prop.value.clone()),
120            configurable: Some(prop.configurable),
121            enumerable: prop.enumerable,
122            writable: Some(prop.writable),
123            is_accessor: Some(prop.is_accessor),
124        }
125    }
126}
127
128#[derive(Clone, MallocSizeOf)]
129struct ObjectActorData {
130    class: String,
131    own_property_length: Option<u32>,
132    preview: Option<devtools_traits::ObjectPreview>,
133    prototype: Option<DebuggerValue>,
134}
135
136#[derive(MallocSizeOf)]
137pub(crate) struct ObjectActor {
138    name: String,
139    data: AtomicRefCell<ObjectActorData>,
140}
141
142impl Actor for ObjectActor {
143    fn name(&self) -> &str {
144        &self.name
145    }
146
147    // https://searchfox.org/firefox-main/source/devtools/shared/specs/object.js
148    fn handle_message(
149        &self,
150        request: ClientRequest,
151        registry: &ActorRegistry,
152        msg_type: &str,
153        _msg: &Map<String, Value>,
154        _id: StreamId,
155    ) -> Result<(), ActorError> {
156        match msg_type {
157            "enumProperties" => {
158                let preview = self.data.borrow().preview.clone();
159                let properties = preview.as_ref().map_or_else(Vec::new, |preview| {
160                    if preview.kind == "ArrayLike" {
161                        // For arrays, convert items to indexed properties
162                        // <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description>
163                        let mut props: Vec<PropertyDescriptor> = preview
164                            .items
165                            .as_ref()
166                            .map(|items| {
167                                items
168                                    .iter()
169                                    .enumerate()
170                                    .map(|(index, value)| PropertyDescriptor {
171                                        name: index.to_string(),
172                                        value: value.clone(),
173                                        configurable: true,
174                                        enumerable: true,
175                                        writable: true,
176                                        is_accessor: false,
177                                    })
178                                    .collect()
179                            })
180                            .unwrap_or_default();
181                        // Add length property
182                        if let Some(length) = preview.array_length {
183                            // <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length#value>
184                            props.push(PropertyDescriptor {
185                                name: "length".to_string(),
186                                value: DebuggerValue::NumberValue(length as f64),
187                                configurable: false,
188                                enumerable: false,
189                                writable: true,
190                                is_accessor: false,
191                            });
192                        }
193                        props
194                    } else {
195                        preview.own_properties.clone().unwrap_or_default()
196                    }
197                });
198                let entries = properties
199                    .into_iter()
200                    .map(PropertyIteratorEntry::Property)
201                    .collect();
202                self.reply_property_iterator(request, registry, entries)?
203            },
204
205            "enumEntries" => {
206                let mut entries = Vec::new();
207                let preview = self.data.borrow().preview.clone();
208                if let Some(preview) = preview &&
209                    let Some(map_entries) = preview.entries
210                {
211                    for (key, value) in map_entries {
212                        entries.push(PropertyIteratorEntry::MapEntry(key, value));
213                    }
214                }
215                self.reply_property_iterator(request, registry, entries)?
216            },
217
218            "enumSymbols" => {
219                let symbol_iterator_actor = SymbolIteratorActor::register(registry);
220                let msg = EnumReply {
221                    from: self.name().into(),
222                    iterator: EnumIterator {
223                        actor: symbol_iterator_actor.name().into(),
224                        type_: EnumIteratorType::SymbolIterator,
225                        count: 0,
226                    },
227                };
228                request.reply_final(&msg)?
229            },
230
231            "prototype" => {
232                let prototype = self
233                    .data
234                    .borrow()
235                    .prototype
236                    .clone()
237                    .unwrap_or(DebuggerValue::NullValue(false));
238                let msg = PrototypeReply {
239                    from: self.name().into(),
240                    prototype: debugger_value_to_json(registry, prototype),
241                };
242                request.reply_final(&msg)?
243            },
244
245            _ => return Err(ActorError::UnrecognizedPacketType),
246        };
247        Ok(())
248    }
249}
250
251impl ObjectActor {
252    fn reply_property_iterator(
253        &self,
254        request: ClientRequest,
255        registry: &ActorRegistry,
256        entries: Vec<PropertyIteratorEntry>,
257    ) -> Result<(), ActorError> {
258        let property_iterator_actor = PropertyIteratorActor::register(registry, entries);
259        let msg = EnumReply {
260            from: self.name().into(),
261            iterator: EnumIterator {
262                actor: property_iterator_actor.name().into(),
263                type_: EnumIteratorType::PropertyIterator,
264                count: property_iterator_actor.count(),
265            },
266        };
267        request.reply_final(&msg)
268    }
269
270    pub fn register(
271        registry: &ActorRegistry,
272        actor_name: Option<String>,
273        class: String,
274        own_property_length: Option<u32>,
275        preview: Option<devtools_traits::ObjectPreview>,
276        prototype: Option<DebuggerValue>,
277    ) -> String {
278        if let Some(name) = actor_name {
279            let actor = registry.find::<Self>(&name);
280            let mut data = actor.data.borrow_mut();
281
282            data.class = class;
283            data.own_property_length = own_property_length;
284
285            if preview.is_some() || data.preview.is_none() {
286                data.preview = preview;
287            }
288            if prototype.is_some() || data.prototype.is_none() {
289                data.prototype = prototype;
290            }
291
292            return name;
293        }
294
295        let name = new_actor_name::<Self>();
296        let actor = ObjectActor {
297            name: name.clone(),
298            data: AtomicRefCell::new(ObjectActorData {
299                class,
300                own_property_length,
301                preview,
302                prototype,
303            }),
304        };
305        registry.register(actor);
306        name
307    }
308}
309
310impl ActorEncode<ObjectActorMsg> for ObjectActor {
311    fn encode(&self, registry: &ActorRegistry) -> ObjectActorMsg {
312        let data = self.data.borrow().clone();
313        let mut msg = ObjectActorMsg {
314            actor: self.name().into(),
315            type_: "object".into(),
316            class: data.class.clone(),
317            extensible: true,
318            frozen: false,
319            sealed: false,
320            function: None,
321            preview: None,
322            own_property_length: data.own_property_length,
323        };
324
325        // Build preview
326        // <https://searchfox.org/firefox-main/source/devtools/server/actors/object/previewers.js#849>
327        let Some(preview) = data.preview.clone() else {
328            return msg;
329        };
330
331        let function = preview.function.map(|function| FunctionPreview {
332            name: function.name.clone(),
333            display_name: function.display_name.clone(),
334            parameter_names: function.parameter_names.clone(),
335            is_async: function.is_async,
336            is_generator: function.is_generator,
337        });
338
339        if data.class == "Function" {
340            msg.function = function.clone();
341        }
342
343        let preview = ObjectPreview {
344            kind: preview.kind.clone(),
345            size: preview.size,
346            entries: preview.entries.map(|entries| {
347                entries
348                    .iter()
349                    .map(|(key, value)| {
350                        (
351                            debugger_value_to_json(registry, key.clone()),
352                            debugger_value_to_json(registry, value.clone()),
353                        )
354                    })
355                    .collect()
356            }),
357            own_properties: preview.own_properties.map(|own_properties| {
358                own_properties
359                    .iter()
360                    .map(|prop| {
361                        (
362                            prop.name.clone(),
363                            ObjectPropertyDescriptor::from_property_descriptor(registry, prop),
364                        )
365                    })
366                    .collect()
367            }),
368            own_properties_length: preview.own_properties_length,
369            function,
370            length: preview.array_length,
371            items: preview.items.map(|items| {
372                items
373                    .iter()
374                    .map(|item| debugger_value_to_json(registry, item.clone()))
375                    .collect()
376            }),
377        };
378
379        msg.preview = Some(preview);
380        msg
381    }
382}