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 devtools_traits::{DebuggerValue, PropertyDescriptor};
8use malloc_size_of_derive::MallocSizeOf;
9use serde::Serialize;
10use serde_json::{Map, Value};
11
12use crate::actor::{Actor, ActorEncode, ActorError, ActorRegistry};
13use crate::actors::property_iterator::PropertyIteratorActor;
14use crate::actors::symbol_iterator::SymbolIteratorActor;
15use crate::protocol::ClientRequest;
16use crate::{StreamId, debugger_value_to_json};
17
18#[derive(Serialize)]
19#[serde(rename_all = "camelCase")]
20enum EnumIteratorType {
21    PropertyIterator,
22    SymbolIterator,
23}
24
25#[derive(Serialize)]
26struct EnumIterator {
27    actor: String,
28    #[serde(rename = "type")]
29    type_: EnumIteratorType,
30    count: u32,
31}
32
33#[derive(Serialize)]
34struct EnumReply {
35    from: String,
36    iterator: EnumIterator,
37}
38
39#[derive(Serialize)]
40struct PrototypeReply {
41    from: String,
42    prototype: ObjectActorMsg,
43}
44
45#[derive(Serialize)]
46#[serde(rename_all = "camelCase")]
47pub(crate) struct ObjectPreview {
48    pub kind: String,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub own_properties: Option<HashMap<String, ObjectPropertyDescriptor>>,
51    #[serde(skip_serializing_if = "Option::is_none")]
52    pub own_properties_length: Option<u32>,
53    #[serde(flatten)]
54    #[serde(skip_serializing_if = "Option::is_none")]
55    pub function: Option<FunctionPreview>,
56    #[serde(skip_serializing_if = "Option::is_none")]
57    pub length: Option<u32>,
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub items: Option<Vec<Value>>,
60}
61
62#[derive(Serialize)]
63#[serde(rename_all = "camelCase")]
64pub struct FunctionPreview {
65    #[serde(skip_serializing_if = "Option::is_none")]
66    pub name: Option<String>,
67    #[serde(skip_serializing_if = "Option::is_none")]
68    pub display_name: Option<String>,
69    pub parameter_names: Vec<String>,
70    #[serde(skip_serializing_if = "Option::is_none")]
71    pub is_async: Option<bool>,
72    #[serde(skip_serializing_if = "Option::is_none")]
73    pub is_generator: Option<bool>,
74}
75
76#[derive(Serialize)]
77#[serde(rename_all = "camelCase")]
78pub(crate) struct ObjectActorMsg {
79    actor: String,
80    #[serde(rename = "type")]
81    type_: String,
82    class: String,
83    #[serde(skip_serializing_if = "Option::is_none")]
84    own_property_length: Option<u32>,
85    extensible: bool,
86    frozen: bool,
87    sealed: bool,
88    #[serde(skip_serializing_if = "Option::is_none")]
89    preview: Option<ObjectPreview>,
90}
91
92#[derive(Serialize)]
93#[serde(rename_all = "camelCase")]
94pub(crate) struct ObjectPropertyDescriptor {
95    pub value: Value,
96    pub configurable: bool,
97    pub enumerable: bool,
98    pub writable: bool,
99    pub is_accessor: bool,
100}
101
102impl ObjectPropertyDescriptor {
103    pub(crate) fn from_property_descriptor(
104        registry: &ActorRegistry,
105        prop: &PropertyDescriptor,
106    ) -> Self {
107        Self {
108            value: debugger_value_to_json(registry, prop.value.clone()),
109            configurable: prop.configurable,
110            enumerable: prop.enumerable,
111            writable: prop.writable,
112            is_accessor: prop.is_accessor,
113        }
114    }
115}
116
117#[derive(MallocSizeOf)]
118pub(crate) struct ObjectActor {
119    name: String,
120    _uuid: Option<String>,
121    class: String,
122    preview: Option<devtools_traits::ObjectPreview>,
123}
124
125impl Actor for ObjectActor {
126    fn name(&self) -> String {
127        self.name.clone()
128    }
129
130    // https://searchfox.org/firefox-main/source/devtools/shared/specs/object.js
131    fn handle_message(
132        &self,
133        request: ClientRequest,
134        registry: &ActorRegistry,
135        msg_type: &str,
136        _msg: &Map<String, Value>,
137        _id: StreamId,
138    ) -> Result<(), ActorError> {
139        match msg_type {
140            "enumProperties" => {
141                let properties = self.preview.as_ref().map_or_else(Vec::new, |preview| {
142                    if preview.kind == "ArrayLike" {
143                        // For arrays, convert items to indexed properties
144                        // <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor#description>
145                        let mut props: Vec<PropertyDescriptor> = preview
146                            .items
147                            .as_ref()
148                            .map(|items| {
149                                items
150                                    .iter()
151                                    .enumerate()
152                                    .map(|(index, value)| PropertyDescriptor {
153                                        name: index.to_string(),
154                                        value: value.clone(),
155                                        configurable: true,
156                                        enumerable: true,
157                                        writable: true,
158                                        is_accessor: false,
159                                    })
160                                    .collect()
161                            })
162                            .unwrap_or_default();
163                        // Add length property
164                        if let Some(length) = preview.array_length {
165                            // <https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/length#value>
166                            props.push(PropertyDescriptor {
167                                name: "length".to_string(),
168                                value: DebuggerValue::NumberValue(length as f64),
169                                configurable: false,
170                                enumerable: false,
171                                writable: true,
172                                is_accessor: false,
173                            });
174                        }
175                        props
176                    } else {
177                        preview.own_properties.clone().unwrap_or_default()
178                    }
179                });
180                let property_iterator_name = PropertyIteratorActor::register(registry, properties);
181                let property_iterator_actor =
182                    registry.find::<PropertyIteratorActor>(&property_iterator_name);
183                let count = property_iterator_actor.count();
184                let msg = EnumReply {
185                    from: self.name(),
186                    iterator: EnumIterator {
187                        actor: property_iterator_name,
188                        type_: EnumIteratorType::PropertyIterator,
189                        count,
190                    },
191                };
192
193                request.reply_final(&msg)?
194            },
195
196            "enumSymbols" => {
197                let symbol_iterator_name = SymbolIteratorActor::register(registry);
198                let msg = EnumReply {
199                    from: self.name(),
200                    iterator: EnumIterator {
201                        actor: symbol_iterator_name,
202                        type_: EnumIteratorType::SymbolIterator,
203                        count: 0,
204                    },
205                };
206                request.reply_final(&msg)?
207            },
208
209            "prototype" => {
210                let msg = PrototypeReply {
211                    from: self.name(),
212                    prototype: self.encode(registry),
213                };
214                request.reply_final(&msg)?
215            },
216
217            _ => return Err(ActorError::UnrecognizedPacketType),
218        };
219        Ok(())
220    }
221}
222
223impl ObjectActor {
224    pub fn register(
225        registry: &ActorRegistry,
226        uuid: Option<String>,
227        class: String,
228        preview: Option<devtools_traits::ObjectPreview>,
229    ) -> String {
230        let Some(uuid) = uuid else {
231            let name = registry.new_name::<Self>();
232            let actor = ObjectActor {
233                name: name.clone(),
234                _uuid: None,
235                class,
236                preview,
237            };
238            registry.register(actor);
239            return name;
240        };
241        if !registry.script_actor_registered(uuid.clone()) {
242            let name = registry.new_name::<Self>();
243            let actor = ObjectActor {
244                name: name.clone(),
245                _uuid: Some(uuid.clone()),
246                class,
247                preview,
248            };
249
250            registry.register_script_actor(uuid, name.clone());
251            registry.register(actor);
252
253            name
254        } else {
255            registry.script_to_actor(uuid)
256        }
257    }
258}
259
260impl ActorEncode<ObjectActorMsg> for ObjectActor {
261    fn encode(&self, registry: &ActorRegistry) -> ObjectActorMsg {
262        let mut msg = ObjectActorMsg {
263            actor: self.name(),
264            type_: "object".into(),
265            class: self.class.clone(),
266            extensible: true,
267            frozen: false,
268            sealed: false,
269            preview: None,
270            own_property_length: None,
271        };
272
273        // Build preview
274        // <https://searchfox.org/firefox-main/source/devtools/server/actors/object/previewers.js#849>
275        let Some(preview) = self.preview.clone() else {
276            return msg;
277        };
278        msg.own_property_length = preview.own_properties_length;
279
280        let function = preview.function.map(|function| FunctionPreview {
281            name: function.name.clone(),
282            display_name: function.display_name.clone(),
283            parameter_names: function.parameter_names.clone(),
284            is_async: function.is_async,
285            is_generator: function.is_generator,
286        });
287
288        let preview = ObjectPreview {
289            kind: preview.kind.clone(),
290            own_properties: preview.own_properties.map(|own_properties| {
291                own_properties
292                    .iter()
293                    .map(|prop| {
294                        (
295                            prop.name.clone(),
296                            ObjectPropertyDescriptor::from_property_descriptor(registry, prop),
297                        )
298                    })
299                    .collect()
300            }),
301            own_properties_length: preview.own_properties_length,
302            function,
303            length: preview.array_length,
304            items: preview.items.map(|items| {
305                items
306                    .iter()
307                    .map(|item| debugger_value_to_json(registry, item.clone()))
308                    .collect()
309            }),
310        };
311
312        msg.preview = Some(preview);
313        msg
314    }
315}