Skip to main content

apple_plist/
ser.rs

1//! [`Encoder`] and the encode-side entry points.
2
3use std::fmt;
4use std::io::Write;
5
6#[cfg(feature = "serde")]
7use serde::Serialize;
8
9use crate::error::Result;
10use crate::format::Format;
11use crate::value::Value;
12
13/// Writes property-list documents to a writer in a chosen [`Format`].
14///
15/// Construction is infallible; requesting a format whose cargo feature is
16/// compiled out fails at encode time with
17/// [`Error::FeatureDisabled`](crate::Error::FeatureDisabled). Each successful
18/// encode call writes exactly one complete document; repeated calls append
19/// documents back to back with no separator.
20///
21/// # Examples
22///
23/// ```
24/// use apple_plist::{Encoder, Value};
25///
26/// let mut out = Vec::new();
27/// Encoder::new(&mut out).encode_value(&Value::from(true))?;
28/// assert!(out.ends_with(b"<true/></plist>"));
29/// # Ok::<(), apple_plist::Error>(())
30/// ```
31pub struct Encoder<W: Write> {
32    #[cfg_attr(
33        not(any(feature = "xml", feature = "binary", feature = "openstep")),
34        expect(dead_code, reason = "no codec is compiled in to consume the writer")
35    )]
36    writer: W,
37    format: Format,
38    indent: String,
39}
40
41impl<W: Write> Encoder<W> {
42    /// Creates an encoder for the default format, XML.
43    ///
44    /// # Examples
45    ///
46    /// ```
47    /// use apple_plist::{Encoder, Value};
48    ///
49    /// let mut out = Vec::new();
50    /// Encoder::new(&mut out).encode_value(&Value::from("hi"))?;
51    /// assert!(out.starts_with(b"<?xml"));
52    /// # Ok::<(), apple_plist::Error>(())
53    /// ```
54    pub const fn new(writer: W) -> Self {
55        Self::for_format(writer, Format::Xml)
56    }
57
58    /// Creates an encoder for an explicit format.
59    ///
60    /// # Examples
61    ///
62    /// ```
63    /// use apple_plist::{Encoder, Format, Value};
64    ///
65    /// let mut out = Vec::new();
66    /// Encoder::for_format(&mut out, Format::OpenStep).encode_value(&Value::from("hi"))?;
67    /// assert_eq!(out, b"hi");
68    /// # Ok::<(), apple_plist::Error>(())
69    /// ```
70    pub const fn for_format(writer: W, format: Format) -> Self {
71        Self {
72            writer,
73            format,
74            indent: String::new(),
75        }
76    }
77
78    /// Creates an encoder for the binary (`bplist00`) format.
79    ///
80    /// # Examples
81    ///
82    /// ```
83    /// use apple_plist::{Encoder, Value};
84    ///
85    /// let mut out = Vec::new();
86    /// Encoder::binary(&mut out).encode_value(&Value::from(7u8))?;
87    /// assert!(out.starts_with(b"bplist00"));
88    /// # Ok::<(), apple_plist::Error>(())
89    /// ```
90    pub const fn binary(writer: W) -> Self {
91        Self::for_format(writer, Format::Binary)
92    }
93
94    /// Creates an encoder that lets the library pick the format — currently
95    /// binary, the automatic-format default.
96    ///
97    /// # Examples
98    ///
99    /// ```
100    /// use apple_plist::{Encoder, Value};
101    ///
102    /// let mut out = Vec::new();
103    /// Encoder::automatic(&mut out).encode_value(&Value::from(7u8))?;
104    /// assert!(out.starts_with(b"bplist00"));
105    /// # Ok::<(), apple_plist::Error>(())
106    /// ```
107    pub const fn automatic(writer: W) -> Self {
108        Self::for_format(writer, Format::Binary)
109    }
110
111    /// Turns on pretty-printing for the XML and text formats; the binary
112    /// format ignores it.
113    ///
114    /// The string is written verbatim, repeated per nesting depth, and
115    /// re-applied on every encode until changed. A non-empty indent also
116    /// switches the text formats' key delimiter from `=` to ` = `; the empty
117    /// string switches pretty-printing back off.
118    ///
119    /// # Examples
120    ///
121    /// ```
122    /// use apple_plist::{Encoder, Format, Value};
123    ///
124    /// let value = Value::from_iter([("a".to_owned(), Value::from("b"))]);
125    /// let mut out = Vec::new();
126    /// let mut encoder = Encoder::for_format(&mut out, Format::OpenStep);
127    /// encoder.set_indent("\t");
128    /// encoder.encode_value(&value)?;
129    /// assert_eq!(out, b"{\n\ta = b;\n}");
130    /// # Ok::<(), apple_plist::Error>(())
131    /// ```
132    pub fn set_indent(&mut self, indent: impl Into<String>) {
133        self.indent = indent.into();
134    }
135
136    /// Serializes `value` and writes one complete document.
137    ///
138    /// The value is serialized into a [`Value`] tree before the format is
139    /// consulted, so serialization failures win over format failures.
140    ///
141    /// # Errors
142    ///
143    /// Returns [`Error::NoRootElement`](crate::Error::NoRootElement) when the
144    /// root serializes to nothing (for example `None`), any error a custom
145    /// `Serialize` implementation reports, and then everything
146    /// [`encode_value`](Self::encode_value) can return.
147    ///
148    /// # Examples
149    ///
150    /// ```
151    /// use apple_plist::Encoder;
152    ///
153    /// let mut out = Vec::new();
154    /// Encoder::new(&mut out).encode("Hello")?;
155    /// assert!(out.ends_with(b"<string>Hello</string></plist>"));
156    /// # Ok::<(), apple_plist::Error>(())
157    /// ```
158    #[cfg(feature = "serde")]
159    pub fn encode<T>(&mut self, value: &T) -> Result<()>
160    where
161        T: Serialize + ?Sized,
162    {
163        let tree = crate::value::ser::to_value(value)?;
164        self.encode_value(&tree)
165    }
166
167    /// Writes one complete document holding `value`.
168    ///
169    /// # Errors
170    ///
171    /// Returns [`Error::FeatureDisabled`](crate::Error::FeatureDisabled) when
172    /// this encoder's format is behind a cargo feature that is compiled out
173    /// (`xml`, `binary`, or `openstep` — the latter covers both text
174    /// formats), [`Error::Io`](crate::Error::Io) when the writer fails, and
175    /// [`Error::Message`](crate::Error::Message) when a binary-format
176    /// container holds a `NaN` real.
177    ///
178    /// # Examples
179    ///
180    /// ```
181    /// use apple_plist::{Encoder, Format, Value};
182    ///
183    /// let mut out = Vec::new();
184    /// Encoder::for_format(&mut out, Format::GnuStep).encode_value(&Value::from(3u8))?;
185    /// assert_eq!(out, b"<*I3>");
186    /// # Ok::<(), apple_plist::Error>(())
187    /// ```
188    pub fn encode_value(&mut self, value: &Value) -> Result<()> {
189        match self.format {
190            Format::Xml => self.encode_xml(value),
191            Format::Binary => self.encode_binary(value),
192            Format::OpenStep | Format::GnuStep => self.encode_text(value),
193        }
194    }
195
196    #[cfg(feature = "xml")]
197    fn encode_xml(&mut self, value: &Value) -> Result<()> {
198        crate::xml::generator::generate(&mut self.writer, value, &self.indent)
199    }
200
201    #[cfg(not(feature = "xml"))]
202    fn encode_xml(&mut self, _value: &Value) -> Result<()> {
203        Err(crate::error::Error::FeatureDisabled {
204            format: Format::Xml,
205        })
206    }
207
208    #[cfg(feature = "binary")]
209    fn encode_binary(&mut self, value: &Value) -> Result<()> {
210        let document = crate::binary::generator::generate(value)?;
211        self.writer.write_all(&document)?;
212        Ok(())
213    }
214
215    #[cfg(not(feature = "binary"))]
216    fn encode_binary(&mut self, _value: &Value) -> Result<()> {
217        Err(crate::error::Error::FeatureDisabled {
218            format: Format::Binary,
219        })
220    }
221
222    #[cfg(feature = "openstep")]
223    fn encode_text(&mut self, value: &Value) -> Result<()> {
224        crate::text::generate(&mut self.writer, value, self.format, &self.indent)
225    }
226
227    #[cfg(not(feature = "openstep"))]
228    fn encode_text(&mut self, _value: &Value) -> Result<()> {
229        Err(crate::error::Error::FeatureDisabled {
230            format: self.format,
231        })
232    }
233}
234
235impl<W: Write> fmt::Debug for Encoder<W> {
236    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
237        f.debug_struct("Encoder")
238            .field("format", &self.format)
239            .field("indent", &self.indent)
240            .finish_non_exhaustive()
241    }
242}
243
244/// Serializes `value` into a new byte vector in the given format.
245///
246/// Equivalent to [`to_vec_indent`] with an empty indent.
247///
248/// # Errors
249///
250/// Everything [`Encoder::encode`] can return, except [`Error::Io`] — the
251/// in-memory writer cannot fail.
252///
253/// [`Error::Io`]: crate::Error::Io
254///
255/// # Examples
256///
257/// ```
258/// use apple_plist::Format;
259///
260/// let bytes = apple_plist::to_vec(&true, Format::OpenStep)?;
261/// assert_eq!(bytes, b"1");
262/// # Ok::<(), apple_plist::Error>(())
263/// ```
264#[cfg(feature = "serde")]
265pub fn to_vec<T: Serialize>(value: &T, format: Format) -> Result<Vec<u8>> {
266    to_vec_indent(value, format, "")
267}
268
269/// Serializes `value` into a new, pretty-printed byte vector.
270///
271/// # Errors
272///
273/// Everything [`Encoder::encode`] can return, except [`Error::Io`] — the
274/// in-memory writer cannot fail. On error no bytes are returned.
275///
276/// [`Error::Io`]: crate::Error::Io
277///
278/// # Examples
279///
280/// ```
281/// use std::collections::BTreeMap;
282///
283/// use apple_plist::Format;
284///
285/// let value = BTreeMap::from([("a", 1)]);
286/// let bytes = apple_plist::to_vec_indent(&value, Format::GnuStep, "\t")?;
287/// assert_eq!(bytes, b"{\n\ta = <*I1>;\n}");
288/// # Ok::<(), apple_plist::Error>(())
289/// ```
290#[cfg(feature = "serde")]
291pub fn to_vec_indent<T: Serialize>(value: &T, format: Format, indent: &str) -> Result<Vec<u8>> {
292    let mut buffer = Vec::new();
293    let mut encoder = Encoder::for_format(&mut buffer, format);
294    encoder.set_indent(indent);
295    encoder.encode(value)?;
296    Ok(buffer)
297}
298
299/// Serializes `value` to `writer` in the given format.
300///
301/// Behaves exactly like [`Encoder::for_format`] followed by
302/// [`Encoder::encode`]; indentation is encoder state, so there is no
303/// `to_writer_indent`.
304///
305/// # Errors
306///
307/// Everything [`Encoder::encode`] can return, including
308/// [`Error::Io`](crate::Error::Io) when `writer` fails.
309///
310/// # Examples
311///
312/// ```
313/// use apple_plist::Format;
314///
315/// let mut out = Vec::new();
316/// apple_plist::to_writer(&mut out, &7u8, Format::Binary)?;
317/// assert!(out.starts_with(b"bplist00"));
318/// # Ok::<(), apple_plist::Error>(())
319/// ```
320#[cfg(feature = "serde")]
321pub fn to_writer<W: Write, T: Serialize>(writer: W, value: &T, format: Format) -> Result<()> {
322    Encoder::for_format(writer, format).encode(value)
323}
324
325#[cfg(test)]
326mod tests {
327    use super::*;
328
329    fn sample() -> Value {
330        Value::from_iter([
331            ("name".to_owned(), Value::from("plist")),
332            ("count".to_owned(), Value::from(3_u8)),
333        ])
334    }
335
336    #[test]
337    fn debug_elides_the_writer() {
338        let encoder = Encoder::new(Vec::new());
339        let rendered = format!("{encoder:?}");
340        assert!(rendered.starts_with("Encoder"));
341        assert!(rendered.contains("Xml"));
342    }
343
344    #[cfg(all(feature = "xml", feature = "binary", feature = "openstep"))]
345    mod all_codecs {
346        #![expect(clippy::unwrap_used, reason = "test code: unwrap is the assertion")]
347
348        use super::*;
349        use crate::error::Error;
350
351        #[test]
352        fn new_defaults_to_xml_and_automatic_matches_binary() {
353            let mut xml = Vec::new();
354            Encoder::new(&mut xml).encode_value(&sample()).unwrap();
355            assert!(xml.starts_with(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
356
357            let mut automatic = Vec::new();
358            Encoder::automatic(&mut automatic)
359                .encode_value(&sample())
360                .unwrap();
361            let mut binary = Vec::new();
362            Encoder::binary(&mut binary)
363                .encode_value(&sample())
364                .unwrap();
365            assert_eq!(automatic, binary);
366            assert!(binary.starts_with(b"bplist00"));
367        }
368
369        #[test]
370        fn indent_state_persists_and_is_reapplied_every_encode() {
371            let mut out = Vec::new();
372            let mut encoder = Encoder::for_format(&mut out, Format::OpenStep);
373            encoder.encode_value(&sample()).unwrap();
374            encoder.set_indent("\t");
375            encoder.encode_value(&sample()).unwrap();
376            encoder.set_indent("");
377            encoder.encode_value(&sample()).unwrap();
378            let compact = "{name=plist;count=3;}";
379            let pretty = "{\n\tname = plist;\n\tcount = 3;\n}";
380            assert_eq!(out, format!("{compact}{pretty}{compact}").into_bytes());
381        }
382
383        #[test]
384        fn binary_ignores_indent() {
385            let mut plain = Vec::new();
386            Encoder::binary(&mut plain).encode_value(&sample()).unwrap();
387            let mut indented = Vec::new();
388            let mut encoder = Encoder::binary(&mut indented);
389            encoder.set_indent("\t");
390            encoder.encode_value(&sample()).unwrap();
391            assert_eq!(plain, indented);
392        }
393
394        #[test]
395        fn repeated_encodes_append_complete_documents() {
396            let mut out = Vec::new();
397            let mut encoder = Encoder::new(&mut out);
398            encoder.encode_value(&Value::from(true)).unwrap();
399            encoder.encode_value(&Value::from(false)).unwrap();
400            let text = String::from_utf8(out).unwrap();
401            assert_eq!(text.matches("<?xml").count(), 2);
402            assert!(text.ends_with("<false/></plist>"));
403        }
404
405        #[test]
406        fn failing_writers_surface_io_for_every_format() {
407            struct FailingWriter;
408            impl Write for FailingWriter {
409                fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
410                    Err(std::io::Error::other("sink failure"))
411                }
412                fn flush(&mut self) -> std::io::Result<()> {
413                    Ok(())
414                }
415            }
416            for format in [
417                Format::Xml,
418                Format::Binary,
419                Format::OpenStep,
420                Format::GnuStep,
421            ] {
422                let result = Encoder::for_format(FailingWriter, format).encode_value(&sample());
423                assert!(matches!(result, Err(Error::Io(_))), "{format}");
424            }
425        }
426
427        #[cfg(feature = "serde")]
428        #[test]
429        fn to_vec_equals_to_vec_indent_with_empty_indent() {
430            for format in [
431                Format::Xml,
432                Format::Binary,
433                Format::OpenStep,
434                Format::GnuStep,
435            ] {
436                assert_eq!(
437                    to_vec(&3_u8, format).unwrap(),
438                    to_vec_indent(&3_u8, format, "").unwrap(),
439                    "{format}"
440                );
441            }
442        }
443
444        #[cfg(feature = "serde")]
445        #[test]
446        fn nil_roots_fail_before_any_byte_is_written() {
447            struct PanickyWriter;
448            impl Write for PanickyWriter {
449                fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
450                    Err(std::io::Error::other("must not be reached"))
451                }
452                fn flush(&mut self) -> std::io::Result<()> {
453                    Ok(())
454                }
455            }
456            let result = Encoder::new(PanickyWriter).encode(&Option::<i32>::None);
457            assert!(matches!(result, Err(Error::NoRootElement)));
458        }
459
460        #[cfg(feature = "serde")]
461        #[test]
462        fn astral_runes_encode_in_every_format() {
463            let value = "grin 😀 end";
464            for format in [
465                Format::Xml,
466                Format::Binary,
467                Format::OpenStep,
468                Format::GnuStep,
469            ] {
470                let bytes = to_vec(&value, format).unwrap();
471                assert!(!bytes.is_empty(), "{format}");
472            }
473            // XML and binary round-trip astral strings faithfully.
474            let xml: String = crate::de::from_slice(&to_vec(&value, Format::Xml).unwrap()).unwrap();
475            assert_eq!(xml, value);
476            let binary: String =
477                crate::de::from_slice(&to_vec(&value, Format::Binary).unwrap()).unwrap();
478            assert_eq!(binary, value);
479        }
480    }
481}