rbx_xml/
serializer.rs

1use std::{borrow::Cow, collections::BTreeMap, io::Write};
2
3use ahash::{HashMap, HashMapExt};
4use rbx_dom_weak::{
5    types::{Ref, SharedString, SharedStringHash, Variant, VariantType},
6    WeakDom,
7};
8use rbx_reflection::{DataType, PropertyKind, PropertySerialization, ReflectionDatabase};
9
10use crate::{
11    conversion::ConvertVariant,
12    core::find_serialized_property_descriptor,
13    error::{EncodeError as NewEncodeError, EncodeErrorKind},
14    types::write_value_xml,
15};
16
17use crate::serializer_core::{XmlEventWriter, XmlWriteEvent};
18
19pub fn encode_internal<W: Write>(
20    output: W,
21    tree: &WeakDom,
22    ids: &[Ref],
23    options: EncodeOptions,
24) -> Result<(), NewEncodeError> {
25    let mut writer = XmlEventWriter::from_output(output);
26    let mut state = EmitState::new(options);
27
28    writer.write(XmlWriteEvent::start_element("roblox").attr("version", "4"))?;
29
30    let mut property_buffer = Vec::new();
31    for id in ids {
32        serialize_instance(&mut writer, &mut state, tree, *id, &mut property_buffer)?;
33    }
34
35    serialize_shared_strings(&mut writer, &mut state)?;
36
37    writer.write(XmlWriteEvent::end_element())?;
38
39    Ok(())
40}
41
42/// Describes the strategy that rbx_xml should use when serializing properties.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44#[non_exhaustive]
45pub enum EncodePropertyBehavior {
46    /// Ignores properties that aren't known by rbx_xml.
47    ///
48    /// This is the default.
49    IgnoreUnknown,
50
51    /// Write unrecognized properties.
52    ///
53    /// With this option set, properties that are newer than rbx_xml's
54    /// reflection database will show up. It may be problematic to depend on
55    /// these properties, since rbx_xml may start supporting them with
56    /// non-reflection specific names at a future date.
57    WriteUnknown,
58
59    /// Returns an error if any properties are found that aren't known by
60    /// rbx_xml.
61    ErrorOnUnknown,
62
63    /// Completely turns off rbx_xml's reflection database. Property names and
64    /// types will appear exactly as they are in the tree.
65    ///
66    /// This setting is useful for debugging the model format. It leaves the
67    /// user to deal with oddities like how `Part.FormFactor` is actually
68    /// serialized as `Part.formFactorRaw`.
69    NoReflection,
70}
71
72/// Options available for serializing an XML-format model or place.
73#[derive(Debug, Clone)]
74pub struct EncodeOptions<'db> {
75    property_behavior: EncodePropertyBehavior,
76    database: &'db ReflectionDatabase<'db>,
77}
78
79impl<'db> EncodeOptions<'db> {
80    /// Constructs a `EncodeOptions` with all values set to their defaults.
81    #[inline]
82    pub fn new() -> Self {
83        EncodeOptions {
84            property_behavior: EncodePropertyBehavior::IgnoreUnknown,
85            database: rbx_reflection_database::get().unwrap(),
86        }
87    }
88
89    /// Determines how rbx_xml will serialize properties, especially unknown
90    /// ones.
91    #[inline]
92    pub fn property_behavior(self, property_behavior: EncodePropertyBehavior) -> Self {
93        EncodeOptions {
94            property_behavior,
95            ..self
96        }
97    }
98
99    /// Determines what reflection database rbx_xml will use to serialize
100    /// properties.
101    #[inline]
102    pub fn reflection_database(self, database: &'db ReflectionDatabase<'db>) -> Self {
103        EncodeOptions { database, ..self }
104    }
105
106    pub(crate) fn use_reflection(&self) -> bool {
107        self.property_behavior != EncodePropertyBehavior::NoReflection
108    }
109}
110
111impl<'db> Default for EncodeOptions<'db> {
112    fn default() -> EncodeOptions<'db> {
113        EncodeOptions::new()
114    }
115}
116
117pub struct EmitState<'db> {
118    options: EncodeOptions<'db>,
119
120    /// A map of IDs written so far to the generated referent that they use.
121    /// This map is used to correctly emit Ref properties.
122    referent_map: HashMap<Ref, u32>,
123
124    /// The referent value that will be used for emitting the next instance.
125    next_referent: u32,
126
127    /// A map of all shared strings referenced so far while generating XML. This
128    /// map will be written as the file's SharedString dictionary.
129    shared_strings_to_emit: BTreeMap<SharedStringHash, SharedString>,
130}
131
132impl<'db> EmitState<'db> {
133    pub fn new(options: EncodeOptions<'db>) -> EmitState<'db> {
134        EmitState {
135            options,
136            referent_map: HashMap::new(),
137            next_referent: 0,
138            shared_strings_to_emit: BTreeMap::new(),
139        }
140    }
141
142    pub fn map_id(&mut self, id: Ref) -> u32 {
143        match self.referent_map.get(&id) {
144            Some(&value) => value,
145            None => {
146                let referent = self.next_referent;
147                self.referent_map.insert(id, referent);
148                self.next_referent += 1;
149                referent
150            }
151        }
152    }
153
154    pub fn add_shared_string(&mut self, value: SharedString) {
155        self.shared_strings_to_emit.insert(value.hash(), value);
156    }
157}
158
159/// Serialize a single instance.
160///
161/// `property_buffer` is a Vec that can be reused between calls to
162/// serialize_instance to make sorting properties more efficient.
163fn serialize_instance<'dom, W: Write>(
164    writer: &mut XmlEventWriter<W>,
165    state: &mut EmitState,
166    tree: &'dom WeakDom,
167    id: Ref,
168    property_buffer: &mut Vec<(&'dom str, &'dom Variant)>,
169) -> Result<(), NewEncodeError> {
170    let instance = tree.get_by_ref(id).unwrap();
171    let mapped_id = state.map_id(id);
172
173    writer.write(
174        XmlWriteEvent::start_element("Item")
175            .attr("class", &instance.class)
176            .attr("referent", &mapped_id.to_string()),
177    )?;
178
179    writer.write(XmlWriteEvent::start_element("Properties"))?;
180
181    write_value_xml(
182        writer,
183        state,
184        "Name",
185        &Variant::String(instance.name.clone()),
186    )?;
187
188    // Move references to our properties into property_buffer so we can sort
189    // them and iterate them in order.
190    property_buffer.extend(instance.properties.iter().map(|(k, v)| (k.as_str(), v)));
191    property_buffer.sort_unstable_by_key(|(key, _)| *key);
192
193    for (property_name, value) in property_buffer.drain(..) {
194        let maybe_serialized_descriptor = if state.options.use_reflection() {
195            find_serialized_property_descriptor(
196                &instance.class,
197                property_name,
198                state.options.database,
199            )
200        } else {
201            None
202        };
203
204        if let Some(serialized_descriptor) = maybe_serialized_descriptor {
205            let data_type = match &serialized_descriptor.data_type {
206                DataType::Value(data_type) => *data_type,
207                DataType::Enum(_enum_name) => VariantType::Enum,
208                _ => unimplemented!(),
209            };
210
211            let mut serialized_name = serialized_descriptor.name.as_ref();
212
213            let mut converted_value = match value.try_convert_ref(instance.class, data_type) {
214                Ok(value) => value,
215                Err(message) => {
216                    return Err(
217                        writer.error(EncodeErrorKind::UnsupportedPropertyConversion {
218                            class_name: instance.class.to_string(),
219                            property_name: property_name.to_string(),
220                            expected_type: data_type,
221                            actual_type: value.ty(),
222                            message,
223                        }),
224                    )
225                }
226            };
227
228            // Perform migrations during serialization
229            if let PropertyKind::Canonical {
230                serialization: PropertySerialization::Migrate(migration),
231            } = &serialized_descriptor.kind
232            {
233                // If the migration fails, there's no harm in us doing nothing
234                // since old values will still load in Studio.
235                if let Ok(new_value) = migration.perform(&converted_value) {
236                    converted_value = Cow::Owned(new_value);
237                    serialized_name = &migration.new_property_name
238                }
239            }
240
241            write_value_xml(writer, state, serialized_name, &converted_value)?;
242        } else {
243            match state.options.property_behavior {
244                EncodePropertyBehavior::IgnoreUnknown => {}
245                EncodePropertyBehavior::WriteUnknown | EncodePropertyBehavior::NoReflection => {
246                    // We'll take this value as-is with no conversions on
247                    // either the name or value.
248
249                    write_value_xml(writer, state, property_name, value)?;
250                }
251                EncodePropertyBehavior::ErrorOnUnknown => {
252                    return Err(writer.error(EncodeErrorKind::UnknownProperty {
253                        class_name: instance.class.to_string(),
254                        property_name: property_name.to_string(),
255                    }));
256                }
257            }
258        }
259    }
260
261    writer.write(XmlWriteEvent::end_element())?;
262
263    for child_id in instance.children() {
264        serialize_instance(writer, state, tree, *child_id, property_buffer)?;
265    }
266
267    writer.write(XmlWriteEvent::end_element())?;
268
269    Ok(())
270}
271
272fn serialize_shared_strings<W: Write>(
273    writer: &mut XmlEventWriter<W>,
274    state: &mut EmitState,
275) -> Result<(), NewEncodeError> {
276    if state.shared_strings_to_emit.is_empty() {
277        return Ok(());
278    }
279
280    writer.write(XmlWriteEvent::start_element("SharedStrings"))?;
281
282    for value in state.shared_strings_to_emit.values() {
283        // Roblox expects SharedString hashes to be the same length as an MD5
284        // hash: 16 bytes, so we truncate our larger hashes to fit.
285        let full_hash = value.hash();
286        let truncated_hash = &full_hash.as_bytes()[..16];
287
288        writer.write(
289            XmlWriteEvent::start_element("SharedString")
290                .attr("md5", &base64::encode(truncated_hash)),
291        )?;
292
293        writer.write_string(&base64::encode(value.data()))?;
294        writer.end_element()?;
295    }
296
297    writer.end_element()?;
298    Ok(())
299}