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}