openstep_plist/
ser.rs

1use serde::{ser, Serialize};
2use smol_str::{SmolStr, SmolStrBuilder};
3
4use crate::{
5    error::{Error, Result},
6    is_alnum_strict, is_numeric,
7};
8
9pub struct Serializer {
10    output: Vec<SmolStr>,
11    // Stack to buffer sequence elements and decide inline vs block formatting
12    seq_stack: Vec<SeqState>,
13    // Track nesting of maps to append trailing semicolon for top-level map
14    map_depth: usize,
15}
16
17struct SeqState {
18    elements: Vec<Vec<SmolStr>>, // serialized tokens per element
19    all_simple: bool,
20    all_numeric: bool, // true if all elements are numeric (affects comma spacing)
21}
22
23macro_rules! forward_to {
24    ($method_from: ident, $t: ty, $method_to:ident, $conversion:expr) => {
25        fn $method_from(self, v: $t) -> Result<()> {
26            self.$method_to($conversion(v))
27        }
28    };
29}
30
31const FLOAT_PRECISION: i32 = 4;
32
33pub fn to_string<T>(value: &T) -> Result<String>
34where
35    T: Serialize,
36{
37    let mut serializer = Serializer {
38        output: Vec::new(),
39        seq_stack: Vec::new(),
40        map_depth: 0,
41    };
42    value.serialize(&mut serializer)?;
43    Ok(serializer.output.join(""))
44}
45
46impl ser::Serializer for &mut Serializer {
47    type Ok = ();
48    type Error = Error;
49    type SerializeSeq = Self;
50    type SerializeTuple = Self;
51    type SerializeTupleStruct = Self;
52    type SerializeTupleVariant = Self;
53    type SerializeMap = Self;
54    type SerializeStruct = Self;
55    type SerializeStructVariant = Self;
56
57    fn serialize_bool(self, v: bool) -> Result<()> {
58        self.output
59            .push(SmolStr::new_static(if v { "1" } else { "0" }));
60        Ok(())
61    }
62    forward_to!(serialize_i8, i8, serialize_i64, i64::from);
63    forward_to!(serialize_i16, i16, serialize_i64, i64::from);
64    forward_to!(serialize_i32, i32, serialize_i64, i64::from);
65    forward_to!(serialize_u8, u8, serialize_u64, u64::from);
66    forward_to!(serialize_u16, u16, serialize_u64, u64::from);
67    forward_to!(serialize_u32, u32, serialize_u64, u64::from);
68
69    fn serialize_i64(self, v: i64) -> Result<()> {
70        self.output.push(SmolStr::new(format!("{v}")));
71        Ok(())
72    }
73
74    fn serialize_u64(self, v: u64) -> Result<()> {
75        self.output.push(SmolStr::new(format!("{v}")));
76        Ok(())
77    }
78
79    forward_to!(serialize_f32, f32, serialize_f64, f64::from);
80
81    fn serialize_f64(self, v: f64) -> Result<()> {
82        self.output.push(SmolStr::new(format!(
83            "{}",
84            (v * 10_f64.powi(FLOAT_PRECISION)).round() / 10_f64.powi(FLOAT_PRECISION)
85        )));
86        Ok(())
87    }
88
89    fn serialize_char(self, v: char) -> Result<()> {
90        self.output.push(SmolStr::new_inline(&format!("{v}")));
91        Ok(())
92    }
93
94    fn serialize_str(self, v: &str) -> Result<()> {
95        escape_string(&mut self.output, v);
96        Ok(())
97    }
98
99    fn serialize_bytes(self, data: &[u8]) -> Result<()> {
100        let mut builder = SmolStrBuilder::new();
101        builder.push('<');
102        for byte in data {
103            let [one, two] = hex_digits_for_byte(*byte);
104            builder.push(one);
105            builder.push(two);
106        }
107        builder.push('>');
108        self.output.push(builder.finish());
109        Ok(())
110    }
111
112    fn serialize_none(self) -> Result<()> {
113        self.serialize_unit()
114    }
115
116    fn serialize_some<T>(self, value: &T) -> Result<()>
117    where
118        T: ?Sized + Serialize,
119    {
120        value.serialize(self)
121    }
122
123    fn serialize_unit(self) -> Result<()> {
124        // ????
125        self.output.push(SmolStr::new_static("null"));
126        Ok(())
127    }
128
129    fn serialize_unit_struct(self, _name: &'static str) -> Result<()> {
130        self.serialize_unit()
131    }
132
133    fn serialize_unit_variant(
134        self,
135        _name: &'static str,
136        _variant_index: u32,
137        variant: &'static str,
138    ) -> Result<()> {
139        self.serialize_str(variant)
140    }
141
142    fn serialize_newtype_struct<T>(self, _name: &'static str, value: &T) -> Result<()>
143    where
144        T: ?Sized + Serialize,
145    {
146        value.serialize(self)
147    }
148
149    fn serialize_newtype_variant<T>(
150        self,
151        _name: &'static str,
152        _variant_index: u32,
153        variant: &'static str,
154        value: &T,
155    ) -> Result<()>
156    where
157        T: ?Sized + Serialize,
158    {
159        self.output.push(SmolStr::new_static("{newtype"));
160        variant.serialize(&mut *self)?;
161        self.output.push(SmolStr::new_static(" = "));
162        value.serialize(&mut *self)?;
163        self.output.push(SmolStr::new_static(";}"));
164        Ok(())
165    }
166
167    fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq> {
168        self.seq_stack.push(SeqState {
169            elements: Vec::new(),
170            all_simple: true,
171            all_numeric: true,
172        });
173        Ok(self)
174    }
175
176    fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple> {
177        self.output.push(SmolStr::new_static("("));
178        Ok(self)
179    }
180
181    // Tuple structs look just like sequences in JSON.
182    fn serialize_tuple_struct(
183        self,
184        _name: &'static str,
185        len: usize,
186    ) -> Result<Self::SerializeTupleStruct> {
187        self.serialize_seq(Some(len))
188    }
189
190    fn serialize_tuple_variant(
191        self,
192        _name: &'static str,
193        _variant_index: u32,
194        variant: &'static str,
195        _len: usize,
196    ) -> Result<Self::SerializeTupleVariant> {
197        self.output.push(SmolStr::new_static("{tuplevariant"));
198        variant.serialize(&mut *self)?;
199        self.output.push(SmolStr::new_static(" = ("));
200        Ok(self)
201    }
202
203    fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap> {
204        self.map_depth += 1;
205        self.output.push(SmolStr::new_static("{"));
206        Ok(self)
207    }
208
209    fn serialize_struct(self, _name: &'static str, len: usize) -> Result<Self::SerializeStruct> {
210        self.serialize_map(Some(len))
211    }
212
213    fn serialize_struct_variant(
214        self,
215        _name: &'static str,
216        _variant_index: u32,
217        variant: &'static str,
218        _len: usize,
219    ) -> Result<Self::SerializeStructVariant> {
220        self.output.push(SmolStr::new_static("{structvariant"));
221        variant.serialize(&mut *self)?;
222        self.output.push(SmolStr::new_static(" = }"));
223        Ok(self)
224    }
225}
226
227impl ser::SerializeSeq for &mut Serializer {
228    // Must match the `Ok` type of the serializer.
229    type Ok = ();
230    // Must match the `Error` type of the serializer.
231    type Error = Error;
232
233    // Serialize a single element of the sequence.
234    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
235    where
236        T: ?Sized + Serialize,
237    {
238        // Serialize element into a temporary serializer to inspect complexity
239        let mut tmp = Serializer {
240            output: Vec::new(),
241            seq_stack: Vec::new(),
242            map_depth: 0,
243        };
244        value.serialize(&mut tmp)?;
245        // Determine if element is complex (starts with map or sequence)
246        let complex = match tmp.output.first() {
247            Some(first) => {
248                let s = first.as_str();
249                s.starts_with("(\n") || s == "(" || s == "{"
250            }
251            None => false,
252        };
253        // Check if all output tokens are numeric (for comma spacing decision)
254        let all_numeric = tmp.output.iter().all(|tok| {
255            let s = tok.as_str();
256            s.parse::<f64>().is_ok() || s == "-" || s == "."
257        });
258        if let Some(state) = self.seq_stack.last_mut() {
259            if complex {
260                state.all_simple = false;
261            }
262            if !all_numeric {
263                state.all_numeric = false;
264            }
265            state.elements.push(tmp.output);
266        }
267        Ok(())
268    }
269
270    // Close the sequence.
271    fn end(self) -> Result<()> {
272        if let Some(state) = self.seq_stack.pop() {
273            if state.all_simple {
274                // Inline formatting: (a,b,c) or (a, b, c) depending on type
275                self.output.push(SmolStr::new_static("("));
276                for (i, elem) in state.elements.iter().enumerate() {
277                    if i > 0 {
278                        // Numbers: no space. Strings/other: space after comma
279                        if state.all_numeric {
280                            self.output.push(SmolStr::new_static(","));
281                        } else {
282                            self.output.push(SmolStr::new_static(", "));
283                        }
284                    }
285                    // Append element tokens verbatim
286                    for tok in elem {
287                        self.output.push(tok.clone());
288                    }
289                }
290                self.output.push(SmolStr::new_static(")"));
291            } else {
292                // Block formatting with newlines
293                self.output.push(SmolStr::new_static("(\n"));
294                for (i, elem) in state.elements.iter().enumerate() {
295                    if i > 0 {
296                        self.output.push(SmolStr::new_static(",\n"));
297                    }
298                    for tok in elem {
299                        self.output.push(tok.clone());
300                    }
301                }
302                self.output.push(SmolStr::new_static("\n)"));
303            }
304            // Semicolons are never added after arrays - they're only added by
305            // serialize_value for dictionary values
306            Ok(())
307        } else {
308            // Should not happen
309            Ok(())
310        }
311    }
312}
313
314// Same thing but for tuples.
315// Tuple (pos, node, etc.) have no space between elements and no newlines.
316impl ser::SerializeTuple for &mut Serializer {
317    type Ok = ();
318    type Error = Error;
319
320    fn serialize_element<T>(&mut self, value: &T) -> Result<()>
321    where
322        T: ?Sized + Serialize,
323    {
324        if !self.output.ends_with(&[SmolStr::new_static("(")]) {
325            self.output.push(SmolStr::new_static(","));
326        }
327        value.serialize(&mut **self)
328    }
329
330    fn end(self) -> Result<()> {
331        self.output.push(SmolStr::new_static(")"));
332        Ok(())
333    }
334}
335
336// Same thing but for tuple structs.
337impl ser::SerializeTupleStruct for &mut Serializer {
338    type Ok = ();
339    type Error = Error;
340
341    fn serialize_field<T>(&mut self, value: &T) -> Result<()>
342    where
343        T: ?Sized + Serialize,
344    {
345        if !self.output.ends_with(&[SmolStr::new_static("(")]) {
346            self.output.push(SmolStr::new_static(", "));
347        }
348        value.serialize(&mut **self)
349    }
350
351    fn end(self) -> Result<()> {
352        self.output.push(SmolStr::new_static(")"));
353        Ok(())
354    }
355}
356
357impl ser::SerializeTupleVariant for &mut Serializer {
358    type Ok = ();
359    type Error = Error;
360
361    fn serialize_field<T>(&mut self, value: &T) -> Result<()>
362    where
363        T: ?Sized + Serialize,
364    {
365        if !self.output.ends_with(&[SmolStr::new_static("(")]) {
366            self.output.push(SmolStr::new_static(", "));
367        }
368        value.serialize(&mut **self)
369    }
370
371    fn end(self) -> Result<()> {
372        self.output.push(SmolStr::new_static(");}"));
373        Ok(())
374    }
375}
376
377impl ser::SerializeMap for &mut Serializer {
378    type Ok = ();
379    type Error = Error;
380
381    fn serialize_key<T>(&mut self, key: &T) -> Result<()>
382    where
383        T: ?Sized + Serialize,
384    {
385        // if !self.output.ends_with('{') {
386        //     self.output.push(SmolStr::new_static(";"));
387        // }
388        self.output.push(SmolStr::new_static("\n"));
389        key.serialize(&mut **self)
390    }
391
392    fn serialize_value<T>(&mut self, value: &T) -> Result<()>
393    where
394        T: ?Sized + Serialize,
395    {
396        self.output.push(SmolStr::new_static(" = "));
397        value.serialize(&mut **self)?;
398        self.output.push(SmolStr::new_static(";"));
399        Ok(())
400    }
401
402    fn end(self) -> Result<()> {
403        self.output.push(SmolStr::new_static("\n}"));
404        // Never add semicolon after closing brace - semicolons are only added
405        // by serialize_value for dictionary values
406        self.map_depth = self.map_depth.saturating_sub(1);
407        Ok(())
408    }
409}
410
411impl ser::SerializeStruct for &mut Serializer {
412    type Ok = ();
413    type Error = Error;
414
415    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
416    where
417        T: ?Sized + Serialize,
418    {
419        self.output.push(SmolStr::new_static("\n"));
420        key.serialize(&mut **self)?;
421        self.output.push(SmolStr::new_static(" = "));
422        value.serialize(&mut **self)?;
423        self.output.push(SmolStr::new_static(";"));
424        Ok(())
425    }
426
427    fn end(self) -> Result<()> {
428        if self.output.last() == Some(&SmolStr::new_static(";")) {
429            self.output.pop();
430            self.output.push(SmolStr::new_static(";"));
431        }
432        self.output.push(SmolStr::new_static("\n}"));
433        Ok(())
434    }
435}
436
437impl ser::SerializeStructVariant for &mut Serializer {
438    type Ok = ();
439    type Error = Error;
440
441    fn serialize_field<T>(&mut self, key: &'static str, value: &T) -> Result<()>
442    where
443        T: ?Sized + Serialize,
444    {
445        if !self.output.ends_with(&[SmolStr::new_static("{")]) {
446            self.output.push(SmolStr::new_static("; "));
447        }
448        key.serialize(&mut **self)?;
449        self.output.push(SmolStr::new_static(" = "));
450        value.serialize(&mut **self)
451    }
452
453    fn end(self) -> Result<()> {
454        self.output.push(SmolStr::new_static("};}"));
455        Ok(())
456    }
457}
458
459fn escape_string(buf: &mut Vec<SmolStr>, s: &str) {
460    if !s.is_empty()
461        && (s.as_bytes().iter().all(|&b| is_alnum_strict(b))
462            && !s.as_bytes().iter().all(|&b| is_numeric(b)))
463    {
464        buf.push(SmolStr::new(s));
465    } else {
466        buf.push(SmolStr::new_static("\""));
467        let mut start = 0;
468        let mut ix = start;
469        while ix < s.len() {
470            let b = s.as_bytes()[ix];
471            match b {
472                b'"' | b'\\' => {
473                    buf.push(SmolStr::new(&s[start..ix]));
474                    buf.push(SmolStr::new_static("\\"));
475                    start = ix;
476                }
477                _ => (),
478            }
479            ix += 1;
480        }
481        buf.push(SmolStr::new(&s[start..]));
482        buf.push(SmolStr::new_static("\""));
483    }
484}
485
486#[inline]
487fn hex_digits_for_byte(byte: u8) -> [char; 2] {
488    fn to_hex_digit(val: u8) -> char {
489        match val {
490            0..=9 => ('0' as u32 as u8 + val).into(),
491            10..=15 => (('a' as u32 as u8) + val - 10).into(),
492            _ => unreachable!("only called with values in range 0..=15"),
493        }
494    }
495
496    [to_hex_digit(byte >> 4), to_hex_digit(byte & 0x0f)]
497}
498
499#[cfg(test)]
500mod tests {
501    use crate::Plist;
502
503    use super::*;
504
505    #[test]
506    fn hex_to_ascii() {
507        assert_eq!(hex_digits_for_byte(0x01), ['0', '1']);
508        assert_eq!(hex_digits_for_byte(0x00), ['0', '0']);
509        assert_eq!(hex_digits_for_byte(0xff), ['f', 'f']);
510        assert_eq!(hex_digits_for_byte(0xf0), ['f', '0']);
511        assert_eq!(hex_digits_for_byte(0x0f), ['0', 'f']);
512    }
513
514    #[test]
515    fn test_serialize() {
516        let plist: Plist = vec![
517            Plist::String("hello".to_string()),
518            Plist::String("world".to_string()),
519        ]
520        .into();
521        let s = to_string(&plist).unwrap();
522        assert_eq!(s, r#"(hello, world)"#);
523    }
524
525    #[test]
526    fn test_serialize_map() {
527        let plist_str = "{\nfoo = bar;\nhello = world;\ntuple = (1,2);\n}";
528        let plist: Plist = Plist::parse(plist_str).unwrap();
529        let s = to_string(&plist).unwrap();
530        assert_eq!(s, plist_str);
531    }
532
533    #[test]
534    fn test_serialize_struct() {
535        // Based on real Glyphs file format: dictionaries in arrays have no semicolon,
536        // top-level dict has no semicolon or trailing newline
537        let plist_str = r#"
538{
539axes = (
540{
541hidden = 1;
542name = Weight;
543tag = wght;
544}
545);
546}"#
547        .trim(); // Remove leading newline
548        let plist: Plist = Plist::parse(plist_str).unwrap();
549        let s = to_string(&plist).unwrap();
550        assert_eq!(s, plist_str);
551    }
552
553    #[test]
554    fn test_vec_axis() {
555        #[derive(Serialize, Debug, Default, Clone)]
556        struct Axis {
557            /// If the axis should be visible in the UI.
558            #[serde(default)]
559            pub hidden: bool,
560            /// The name of the axis (e.g. `Weight``)
561            pub name: String,
562            /// The axis tag (e.g. `wght`)
563            pub tag: String,
564        }
565        let foo = vec![Axis {
566            hidden: true,
567            name: "Weight".to_string(),
568            tag: "wght".to_string(),
569        }];
570        let s = to_string(&foo).unwrap();
571        // Block format with dict inside array: (\n{...}\n)
572        // When formatted on one line for comparison, this becomes: ({ ... })
573        let expected = "(\n{\nhidden = 1;\nname = Weight;\ntag = wght;\n}\n)";
574        assert_eq!(s, expected);
575    }
576
577    #[test]
578    fn test_string_escaping() {
579        let str = "files/LinkedFontv3.glyphs";
580        let mut buf = Vec::new();
581        escape_string(&mut buf, str);
582        let output = buf.join("");
583        assert_eq!(output, "\"files/LinkedFontv3.glyphs\"");
584    }
585}