unity_asset_yaml/
unity_yaml_serializer.rs

1//! Unity YAML serializer
2//!
3//! This module implements Unity-specific YAML serialization that maintains
4//! exact compatibility with Unity's YAML format, including:
5//! - Unity tags (!u!classid)
6//! - Anchor handling (&anchor)
7//! - Extra anchor data (stripped, etc.)
8//! - Proper formatting and line endings
9
10use crate::constants::{LineEnding, UNITY_TAG_URI, UNITY_YAML_VERSION};
11use std::fmt::Write;
12use unity_asset_core::{Result, UnityAssetError, UnityClass, UnityValue};
13
14/// Unity YAML serializer
15pub struct UnityYamlSerializer {
16    /// Line ending style to use
17    line_ending: LineEnding,
18    /// Indent size (Unity uses 2 spaces)
19    indent_size: usize,
20    /// Current indentation level
21    indent_level: usize,
22    /// Whether this is the first document
23    first_document: bool,
24}
25
26impl UnityYamlSerializer {
27    /// Create a new Unity YAML serializer
28    pub fn new() -> Self {
29        Self {
30            line_ending: LineEnding::default(),
31            indent_size: 2,
32            indent_level: 0,
33            first_document: true,
34        }
35    }
36
37    /// Set line ending style
38    pub fn with_line_ending(mut self, line_ending: LineEnding) -> Self {
39        self.line_ending = line_ending;
40        self
41    }
42
43    /// Serialize Unity classes to YAML string
44    pub fn serialize_to_string(&mut self, classes: &[UnityClass]) -> Result<String> {
45        let mut output = String::new();
46        self.serialize_to_writer(&mut output, classes)?;
47        Ok(output)
48    }
49
50    /// Serialize Unity classes to a writer
51    pub fn serialize_to_writer<W: Write>(
52        &mut self,
53        writer: &mut W,
54        classes: &[UnityClass],
55    ) -> Result<()> {
56        self.first_document = true;
57
58        // Write YAML header for first document
59        if !classes.is_empty() {
60            self.write_yaml_header(writer)?;
61        }
62
63        // Serialize each Unity class as a separate document
64        for (index, class) in classes.iter().enumerate() {
65            if index > 0 {
66                self.first_document = false;
67            }
68            self.serialize_unity_class(writer, class)?;
69        }
70
71        Ok(())
72    }
73
74    /// Write YAML header (version and tags)
75    fn write_yaml_header<W: Write>(&self, writer: &mut W) -> Result<()> {
76        // Write YAML version
77        write!(
78            writer,
79            "%YAML {}.{}{}",
80            UNITY_YAML_VERSION.0,
81            UNITY_YAML_VERSION.1,
82            self.line_ending.as_str()
83        )
84        .map_err(|e| UnityAssetError::format(format!("Failed to write YAML version: {}", e)))?;
85
86        // Write Unity tag
87        write!(
88            writer,
89            "%TAG !u! {}{}",
90            UNITY_TAG_URI,
91            self.line_ending.as_str()
92        )
93        .map_err(|e| UnityAssetError::format(format!("Failed to write Unity tag: {}", e)))?;
94
95        Ok(())
96    }
97
98    /// Serialize a single Unity class
99    fn serialize_unity_class<W: Write>(
100        &mut self,
101        writer: &mut W,
102        class: &UnityClass,
103    ) -> Result<()> {
104        // Write document separator with Unity tag and anchor
105        write!(writer, "--- !u!{} &{}", class.class_id, class.anchor).map_err(|e| {
106            UnityAssetError::format(format!("Failed to write document header: {}", e))
107        })?;
108
109        // Write extra anchor data if present
110        if !class.extra_anchor_data.is_empty() {
111            write!(writer, " {}", class.extra_anchor_data).map_err(|e| {
112                UnityAssetError::format(format!("Failed to write extra anchor data: {}", e))
113            })?;
114        }
115
116        write!(writer, "{}", self.line_ending.as_str())
117            .map_err(|e| UnityAssetError::format(format!("Failed to write line ending: {}", e)))?;
118
119        // Write class name and properties
120        write!(writer, "{}:{}", class.class_name, self.line_ending.as_str())
121            .map_err(|e| UnityAssetError::format(format!("Failed to write class name: {}", e)))?;
122
123        // Serialize properties
124        self.indent_level = 1;
125        for (key, value) in class.properties() {
126            self.serialize_property(writer, key, value)?;
127        }
128
129        Ok(())
130    }
131
132    /// Serialize a property key-value pair
133    fn serialize_property<W: Write>(
134        &mut self,
135        writer: &mut W,
136        key: &str,
137        value: &UnityValue,
138    ) -> Result<()> {
139        // Write indentation
140        self.write_indent(writer)?;
141
142        // Write property key
143        write!(writer, "{}: ", key)
144            .map_err(|e| UnityAssetError::format(format!("Failed to write property key: {}", e)))?;
145
146        // Write property value
147        self.serialize_value(writer, value, false)?;
148
149        Ok(())
150    }
151
152    /// Serialize a Unity value
153    fn serialize_value<W: Write>(
154        &mut self,
155        writer: &mut W,
156        value: &UnityValue,
157        inline: bool,
158    ) -> Result<()> {
159        match value {
160            UnityValue::Null => {
161                write!(writer, "{{fileID: 0}}{}", self.line_ending.as_str()).map_err(|e| {
162                    UnityAssetError::format(format!("Failed to write null value: {}", e))
163                })?;
164            }
165            UnityValue::Bool(b) => {
166                write!(
167                    writer,
168                    "{}{}",
169                    if *b { "1" } else { "0" },
170                    self.line_ending.as_str()
171                )
172                .map_err(|e| {
173                    UnityAssetError::format(format!("Failed to write bool value: {}", e))
174                })?;
175            }
176            UnityValue::Integer(i) => {
177                write!(writer, "{}{}", i, self.line_ending.as_str()).map_err(|e| {
178                    UnityAssetError::format(format!("Failed to write integer value: {}", e))
179                })?;
180            }
181            UnityValue::Float(f) => {
182                write!(writer, "{}{}", f, self.line_ending.as_str()).map_err(|e| {
183                    UnityAssetError::format(format!("Failed to write float value: {}", e))
184                })?;
185            }
186            UnityValue::String(s) => {
187                // Handle string quoting based on content
188                if self.needs_quoting(s) {
189                    write!(
190                        writer,
191                        "\"{}\"{}",
192                        self.escape_string(s),
193                        self.line_ending.as_str()
194                    )
195                } else {
196                    write!(writer, "{}{}", s, self.line_ending.as_str())
197                }
198                .map_err(|e| {
199                    UnityAssetError::format(format!("Failed to write string value: {}", e))
200                })?;
201            }
202            UnityValue::Array(arr) => {
203                if arr.is_empty() {
204                    write!(writer, "[]{}", self.line_ending.as_str()).map_err(|e| {
205                        UnityAssetError::format(format!("Failed to write empty array: {}", e))
206                    })?;
207                } else if inline || self.is_simple_array(arr) {
208                    // Write inline array
209                    write!(writer, "[").map_err(|e| {
210                        UnityAssetError::format(format!("Failed to write array start: {}", e))
211                    })?;
212                    for (i, item) in arr.iter().enumerate() {
213                        if i > 0 {
214                            write!(writer, ", ").map_err(|e| {
215                                UnityAssetError::format(format!(
216                                    "Failed to write array separator: {}",
217                                    e
218                                ))
219                            })?;
220                        }
221                        self.serialize_value_inline(writer, item)?;
222                    }
223                    write!(writer, "]{}", self.line_ending.as_str()).map_err(|e| {
224                        UnityAssetError::format(format!("Failed to write inline array end: {}", e))
225                    })?;
226                } else {
227                    // Write block array
228                    write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
229                        UnityAssetError::format(format!("Failed to write array start: {}", e))
230                    })?;
231                    self.indent_level += 1;
232                    for item in arr {
233                        self.write_indent(writer)?;
234                        write!(writer, "- ").map_err(|e| {
235                            UnityAssetError::format(format!(
236                                "Failed to write array item prefix: {}",
237                                e
238                            ))
239                        })?;
240                        self.serialize_value(writer, item, true)?;
241                    }
242                    self.indent_level -= 1;
243                }
244            }
245            UnityValue::Bytes(b) => {
246                if b.is_empty() {
247                    write!(writer, "[]{}", self.line_ending.as_str()).map_err(|e| {
248                        UnityAssetError::format(format!("Failed to write empty bytes: {}", e))
249                    })?;
250                } else if inline || b.len() <= 64 {
251                    write!(writer, "[").map_err(|e| {
252                        UnityAssetError::format(format!("Failed to write bytes start: {}", e))
253                    })?;
254                    for (i, item) in b.iter().enumerate() {
255                        if i > 0 {
256                            write!(writer, ", ").map_err(|e| {
257                                UnityAssetError::format(format!(
258                                    "Failed to write bytes separator: {}",
259                                    e
260                                ))
261                            })?;
262                        }
263                        write!(writer, "{}", item).map_err(|e| {
264                            UnityAssetError::format(format!("Failed to write byte value: {}", e))
265                        })?;
266                    }
267                    write!(writer, "]{}", self.line_ending.as_str()).map_err(|e| {
268                        UnityAssetError::format(format!("Failed to write bytes end: {}", e))
269                    })?;
270                } else {
271                    write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
272                        UnityAssetError::format(format!("Failed to write bytes start: {}", e))
273                    })?;
274                    self.indent_level += 1;
275                    for item in b {
276                        self.write_indent(writer)?;
277                        write!(writer, "- {}", item).map_err(|e| {
278                            UnityAssetError::format(format!(
279                                "Failed to write bytes item prefix: {}",
280                                e
281                            ))
282                        })?;
283                        write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
284                            UnityAssetError::format(format!(
285                                "Failed to write bytes line ending: {}",
286                                e
287                            ))
288                        })?;
289                    }
290                    self.indent_level -= 1;
291                }
292            }
293            UnityValue::Object(obj) => {
294                if obj.is_empty() {
295                    write!(writer, "{{}}{}", self.line_ending.as_str()).map_err(|e| {
296                        UnityAssetError::format(format!("Failed to write empty object: {}", e))
297                    })?;
298                } else if inline || self.is_simple_object(obj) {
299                    // Write inline object
300                    write!(writer, "{{").map_err(|e| {
301                        UnityAssetError::format(format!("Failed to write object start: {}", e))
302                    })?;
303                    for (i, (key, value)) in obj.iter().enumerate() {
304                        if i > 0 {
305                            write!(writer, ", ").map_err(|e| {
306                                UnityAssetError::format(format!(
307                                    "Failed to write object separator: {}",
308                                    e
309                                ))
310                            })?;
311                        }
312                        write!(writer, "{}: ", key).map_err(|e| {
313                            UnityAssetError::format(format!("Failed to write object key: {}", e))
314                        })?;
315                        self.serialize_value_inline(writer, value)?;
316                    }
317                    write!(writer, "}}{}", self.line_ending.as_str()).map_err(|e| {
318                        UnityAssetError::format(format!("Failed to write inline object end: {}", e))
319                    })?;
320                } else {
321                    // Write block object
322                    write!(writer, "{}", self.line_ending.as_str()).map_err(|e| {
323                        UnityAssetError::format(format!("Failed to write object start: {}", e))
324                    })?;
325                    self.indent_level += 1;
326                    for (key, value) in obj {
327                        self.serialize_property(writer, key, value)?;
328                    }
329                    self.indent_level -= 1;
330                }
331            }
332        }
333        Ok(())
334    }
335
336    /// Serialize a value inline (for arrays and objects)
337    fn serialize_value_inline<W: Write>(&self, writer: &mut W, value: &UnityValue) -> Result<()> {
338        match value {
339            UnityValue::Null => {
340                write!(writer, "{{fileID: 0}}").map_err(|e| {
341                    UnityAssetError::format(format!("Failed to write null value: {}", e))
342                })?;
343            }
344            UnityValue::Bool(b) => {
345                write!(writer, "{}", if *b { "1" } else { "0" }).map_err(|e| {
346                    UnityAssetError::format(format!("Failed to write bool value: {}", e))
347                })?;
348            }
349            UnityValue::Integer(i) => {
350                write!(writer, "{}", i).map_err(|e| {
351                    UnityAssetError::format(format!("Failed to write integer value: {}", e))
352                })?;
353            }
354            UnityValue::Float(f) => {
355                write!(writer, "{}", f).map_err(|e| {
356                    UnityAssetError::format(format!("Failed to write float value: {}", e))
357                })?;
358            }
359            UnityValue::String(s) => {
360                if self.needs_quoting(s) {
361                    write!(writer, "\"{}\"", self.escape_string(s)).map_err(|e| {
362                        UnityAssetError::format(format!("Failed to write quoted string: {}", e))
363                    })?;
364                } else {
365                    write!(writer, "{}", s).map_err(|e| {
366                        UnityAssetError::format(format!("Failed to write string: {}", e))
367                    })?;
368                }
369            }
370            UnityValue::Bytes(b) => {
371                write!(writer, "<bytes len={}>", b.len()).map_err(|e| {
372                    UnityAssetError::format(format!("Failed to write bytes: {}", e))
373                })?;
374            }
375            UnityValue::Array(_) | UnityValue::Object(_) => {
376                // For complex nested structures, we might need more sophisticated handling
377                write!(writer, "{{...}}").map_err(|e| {
378                    UnityAssetError::format(format!("Failed to write complex value: {}", e))
379                })?;
380            }
381        }
382        Ok(())
383    }
384
385    /// Write indentation
386    fn write_indent<W: Write>(&self, writer: &mut W) -> Result<()> {
387        for _ in 0..(self.indent_level * self.indent_size) {
388            write!(writer, " ").map_err(|e| {
389                UnityAssetError::format(format!("Failed to write indentation: {}", e))
390            })?;
391        }
392        Ok(())
393    }
394
395    /// Check if a string needs quoting
396    fn needs_quoting(&self, s: &str) -> bool {
397        s.is_empty()
398            || s.contains('\n')
399            || s.contains('\r')
400            || s.contains('"')
401            || s.contains('\'')
402            || s.contains(':')
403            || s.contains('[')
404            || s.contains(']')
405            || s.contains('{')
406            || s.contains('}')
407            || s.starts_with(' ')
408            || s.ends_with(' ')
409    }
410
411    /// Escape a string for YAML
412    fn escape_string(&self, s: &str) -> String {
413        s.replace('\\', "\\\\")
414            .replace('"', "\\\"")
415            .replace('\n', "\\n")
416            .replace('\r', "\\r")
417            .replace('\t', "\\t")
418    }
419
420    /// Check if an array should be written inline
421    fn is_simple_array(&self, arr: &[UnityValue]) -> bool {
422        arr.len() <= 3
423            && arr.iter().all(|v| match v {
424                UnityValue::Integer(_) | UnityValue::Float(_) | UnityValue::Bool(_) => true,
425                UnityValue::String(s) => s.len() < 20,
426                _ => false,
427            })
428    }
429
430    /// Check if an object should be written inline
431    fn is_simple_object(&self, obj: &indexmap::IndexMap<String, UnityValue>) -> bool {
432        obj.len() <= 3
433            && obj.values().all(|v| match v {
434                UnityValue::Integer(_) | UnityValue::Float(_) | UnityValue::Bool(_) => true,
435                UnityValue::String(s) => s.len() < 20,
436                _ => false,
437            })
438    }
439}
440
441impl Default for UnityYamlSerializer {
442    fn default() -> Self {
443        Self::new()
444    }
445}