yaml_rust2/
emitter.rs

1//! YAML serialization helpers.
2
3use crate::char_traits;
4use crate::yaml::{Hash, Yaml};
5use std::convert::From;
6use std::error::Error;
7use std::fmt::{self, Display};
8
9/// An error when emitting YAML.
10#[derive(Copy, Clone, Debug)]
11pub enum EmitError {
12    /// A formatting error.
13    FmtError(fmt::Error),
14}
15
16impl Error for EmitError {
17    fn cause(&self) -> Option<&dyn Error> {
18        None
19    }
20}
21
22impl Display for EmitError {
23    fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
24        match *self {
25            EmitError::FmtError(ref err) => Display::fmt(err, formatter),
26        }
27    }
28}
29
30impl From<fmt::Error> for EmitError {
31    fn from(f: fmt::Error) -> Self {
32        EmitError::FmtError(f)
33    }
34}
35
36/// The YAML serializer.
37///
38/// ```
39/// # use yaml_rust2::{YamlLoader, YamlEmitter};
40/// let input_string = "a: b\nc: d";
41/// let yaml = YamlLoader::load_from_str(input_string).unwrap();
42///
43/// let mut output = String::new();
44/// YamlEmitter::new(&mut output).dump(&yaml[0]).unwrap();
45///
46/// assert_eq!(output, r#"---
47/// a: b
48/// c: d"#);
49/// ```
50#[allow(clippy::module_name_repetitions)]
51pub struct YamlEmitter<'a> {
52    writer: &'a mut dyn fmt::Write,
53    best_indent: usize,
54    compact: bool,
55    level: isize,
56    multiline_strings: bool,
57}
58
59/// A convenience alias for emitter functions that may fail without returning a value.
60pub type EmitResult = Result<(), EmitError>;
61
62// from serialize::json
63fn escape_str(wr: &mut dyn fmt::Write, v: &str) -> Result<(), fmt::Error> {
64    wr.write_str("\"")?;
65
66    let mut start = 0;
67
68    for (i, byte) in v.bytes().enumerate() {
69        let escaped = match byte {
70            b'"' => "\\\"",
71            b'\\' => "\\\\",
72            b'\x00' => "\\u0000",
73            b'\x01' => "\\u0001",
74            b'\x02' => "\\u0002",
75            b'\x03' => "\\u0003",
76            b'\x04' => "\\u0004",
77            b'\x05' => "\\u0005",
78            b'\x06' => "\\u0006",
79            b'\x07' => "\\u0007",
80            b'\x08' => "\\b",
81            b'\t' => "\\t",
82            b'\n' => "\\n",
83            b'\x0b' => "\\u000b",
84            b'\x0c' => "\\f",
85            b'\r' => "\\r",
86            b'\x0e' => "\\u000e",
87            b'\x0f' => "\\u000f",
88            b'\x10' => "\\u0010",
89            b'\x11' => "\\u0011",
90            b'\x12' => "\\u0012",
91            b'\x13' => "\\u0013",
92            b'\x14' => "\\u0014",
93            b'\x15' => "\\u0015",
94            b'\x16' => "\\u0016",
95            b'\x17' => "\\u0017",
96            b'\x18' => "\\u0018",
97            b'\x19' => "\\u0019",
98            b'\x1a' => "\\u001a",
99            b'\x1b' => "\\u001b",
100            b'\x1c' => "\\u001c",
101            b'\x1d' => "\\u001d",
102            b'\x1e' => "\\u001e",
103            b'\x1f' => "\\u001f",
104            b'\x7f' => "\\u007f",
105            _ => continue,
106        };
107
108        if start < i {
109            wr.write_str(&v[start..i])?;
110        }
111
112        wr.write_str(escaped)?;
113
114        start = i + 1;
115    }
116
117    if start != v.len() {
118        wr.write_str(&v[start..])?;
119    }
120
121    wr.write_str("\"")?;
122    Ok(())
123}
124
125impl<'a> YamlEmitter<'a> {
126    /// Create a new emitter serializing into `writer`.
127    pub fn new(writer: &'a mut dyn fmt::Write) -> YamlEmitter<'a> {
128        YamlEmitter {
129            writer,
130            best_indent: 2,
131            compact: true,
132            level: -1,
133            multiline_strings: false,
134        }
135    }
136
137    /// Set 'compact inline notation' on or off, as described for block
138    /// [sequences](http://www.yaml.org/spec/1.2/spec.html#id2797382)
139    /// and
140    /// [mappings](http://www.yaml.org/spec/1.2/spec.html#id2798057).
141    ///
142    /// In this form, blocks cannot have any properties (such as anchors
143    /// or tags), which should be OK, because this emitter doesn't
144    /// (currently) emit those anyways.
145    pub fn compact(&mut self, compact: bool) {
146        self.compact = compact;
147    }
148
149    /// Determine if this emitter is using 'compact inline notation'.
150    #[must_use]
151    pub fn is_compact(&self) -> bool {
152        self.compact
153    }
154
155    /// Render strings containing multiple lines in [literal style].
156    ///
157    /// # Examples
158    ///
159    /// ```rust
160    /// use yaml_rust2::{Yaml, YamlEmitter, YamlLoader};
161    ///
162    /// let input = r#"{foo: "bar!\nbar!", baz: 42}"#;
163    /// let parsed = YamlLoader::load_from_str(input).unwrap();
164    /// eprintln!("{:?}", parsed);
165    ///
166    /// let mut output = String::new();
167    /// let mut emitter = YamlEmitter::new(&mut output);
168    /// emitter.multiline_strings(true);
169    /// emitter.dump(&parsed[0]).unwrap();
170    /// assert_eq!(output.as_str(), "\
171    /// ---
172    /// foo: |-
173    ///   bar!
174    ///   bar!
175    /// baz: 42");
176    /// ```
177    ///
178    /// [literal style]: https://yaml.org/spec/1.2/spec.html#id2795688
179    pub fn multiline_strings(&mut self, multiline_strings: bool) {
180        self.multiline_strings = multiline_strings;
181    }
182
183    /// Determine if this emitter will emit multiline strings when appropriate.
184    #[must_use]
185    pub fn is_multiline_strings(&self) -> bool {
186        self.multiline_strings
187    }
188
189    /// Dump Yaml to an output stream.
190    /// # Errors
191    /// Returns `EmitError` when an error occurs.
192    pub fn dump(&mut self, doc: &Yaml) -> EmitResult {
193        // write DocumentStart
194        writeln!(self.writer, "---")?;
195        self.level = -1;
196        self.emit_node(doc)
197    }
198
199    fn write_indent(&mut self) -> EmitResult {
200        if self.level <= 0 {
201            return Ok(());
202        }
203        for _ in 0..self.level {
204            for _ in 0..self.best_indent {
205                write!(self.writer, " ")?;
206            }
207        }
208        Ok(())
209    }
210
211    fn emit_node(&mut self, node: &Yaml) -> EmitResult {
212        match *node {
213            Yaml::Array(ref v) => self.emit_array(v),
214            Yaml::Hash(ref h) => self.emit_hash(h),
215            Yaml::String(ref v) => {
216                if self.multiline_strings
217                    && v.contains('\n')
218                    && char_traits::is_valid_literal_block_scalar(v)
219                {
220                    self.emit_literal_block(v)?;
221                } else if need_quotes(v) {
222                    escape_str(self.writer, v)?;
223                } else {
224                    write!(self.writer, "{v}")?;
225                }
226                Ok(())
227            }
228            Yaml::Boolean(v) => {
229                if v {
230                    self.writer.write_str("true")?;
231                } else {
232                    self.writer.write_str("false")?;
233                }
234                Ok(())
235            }
236            Yaml::Integer(v) => {
237                write!(self.writer, "{v}")?;
238                Ok(())
239            }
240            Yaml::Real(ref v) => {
241                write!(self.writer, "{v}")?;
242                Ok(())
243            }
244            Yaml::Null | Yaml::BadValue => {
245                write!(self.writer, "~")?;
246                Ok(())
247            }
248            // XXX(chenyh) Alias
249            Yaml::Alias(_) => Ok(()),
250        }
251    }
252
253    fn emit_literal_block(&mut self, v: &str) -> EmitResult {
254        let ends_with_newline = v.ends_with('\n');
255        if ends_with_newline {
256            self.writer.write_str("|")?;
257        } else {
258            self.writer.write_str("|-")?;
259        }
260
261        self.level += 1;
262        // lines() will omit the last line if it is empty.
263        for line in v.lines() {
264            writeln!(self.writer)?;
265            self.write_indent()?;
266            // It's literal text, so don't escape special chars.
267            self.writer.write_str(line)?;
268        }
269        self.level -= 1;
270        Ok(())
271    }
272
273    fn emit_array(&mut self, v: &[Yaml]) -> EmitResult {
274        if v.is_empty() {
275            write!(self.writer, "[]")?;
276        } else {
277            self.level += 1;
278            for (cnt, x) in v.iter().enumerate() {
279                if cnt > 0 {
280                    writeln!(self.writer)?;
281                    self.write_indent()?;
282                }
283                write!(self.writer, "-")?;
284                self.emit_val(true, x)?;
285            }
286            self.level -= 1;
287        }
288        Ok(())
289    }
290
291    fn emit_hash(&mut self, h: &Hash) -> EmitResult {
292        if h.is_empty() {
293            self.writer.write_str("{}")?;
294        } else {
295            self.level += 1;
296            for (cnt, (k, v)) in h.iter().enumerate() {
297                let complex_key = matches!(*k, Yaml::Hash(_) | Yaml::Array(_));
298                if cnt > 0 {
299                    writeln!(self.writer)?;
300                    self.write_indent()?;
301                }
302                if complex_key {
303                    write!(self.writer, "?")?;
304                    self.emit_val(true, k)?;
305                    writeln!(self.writer)?;
306                    self.write_indent()?;
307                    write!(self.writer, ":")?;
308                    self.emit_val(true, v)?;
309                } else {
310                    self.emit_node(k)?;
311                    write!(self.writer, ":")?;
312                    self.emit_val(false, v)?;
313                }
314            }
315            self.level -= 1;
316        }
317        Ok(())
318    }
319
320    /// Emit a yaml as a hash or array value: i.e., which should appear
321    /// following a ":" or "-", either after a space, or on a new line.
322    /// If `inline` is true, then the preceding characters are distinct
323    /// and short enough to respect the compact flag.
324    fn emit_val(&mut self, inline: bool, val: &Yaml) -> EmitResult {
325        match *val {
326            Yaml::Array(ref v) => {
327                if (inline && self.compact) || v.is_empty() {
328                    write!(self.writer, " ")?;
329                } else {
330                    writeln!(self.writer)?;
331                    self.level += 1;
332                    self.write_indent()?;
333                    self.level -= 1;
334                }
335                self.emit_array(v)
336            }
337            Yaml::Hash(ref h) => {
338                if (inline && self.compact) || h.is_empty() {
339                    write!(self.writer, " ")?;
340                } else {
341                    writeln!(self.writer)?;
342                    self.level += 1;
343                    self.write_indent()?;
344                    self.level -= 1;
345                }
346                self.emit_hash(h)
347            }
348            _ => {
349                write!(self.writer, " ")?;
350                self.emit_node(val)
351            }
352        }
353    }
354}
355
356/// Check if the string requires quoting.
357/// Strings starting with any of the following characters must be quoted.
358/// :, &, *, ?, |, -, <, >, =, !, %, @
359/// Strings containing any of the following characters must be quoted.
360/// {, }, \[, t \], ,, #, `
361///
362/// If the string contains any of the following control characters, it must be escaped with double quotes:
363/// \0, \x01, \x02, \x03, \x04, \x05, \x06, \a, \b, \t, \n, \v, \f, \r, \x0e, \x0f, \x10, \x11, \x12, \x13, \x14, \x15, \x16, \x17, \x18, \x19, \x1a, \e, \x1c, \x1d, \x1e, \x1f, \N, \_, \L, \P
364///
365/// Finally, there are other cases when the strings must be quoted, no matter if you're using single or double quotes:
366/// * When the string is true or false (otherwise, it would be treated as a boolean value);
367/// * When the string is null or ~ (otherwise, it would be considered as a null value);
368/// * When the string looks like a number, such as integers (e.g. 2, 14, etc.), floats (e.g. 2.6, 14.9) and exponential numbers (e.g. 12e7, etc.) (otherwise, it would be treated as a numeric value);
369/// * When the string looks like a date (e.g. 2014-12-31) (otherwise it would be automatically converted into a Unix timestamp).
370#[allow(clippy::doc_markdown)]
371fn need_quotes(string: &str) -> bool {
372    fn need_quotes_spaces(string: &str) -> bool {
373        string.starts_with(' ') || string.ends_with(' ')
374    }
375
376    string.is_empty()
377        || need_quotes_spaces(string)
378        || string.starts_with(|character: char| {
379            matches!(
380                character,
381                '&' | '*' | '?' | '|' | '-' | '<' | '>' | '=' | '!' | '%' | '@'
382            )
383        })
384        || string.contains(|character: char| {
385            matches!(character, ':'
386            | '{'
387            | '}'
388            | '['
389            | ']'
390            | ','
391            | '#'
392            | '`'
393            | '\"'
394            | '\''
395            | '\\'
396            | '\0'..='\x06'
397            | '\t'
398            | '\n'
399            | '\r'
400            | '\x0e'..='\x1a'
401            | '\x1c'..='\x1f')
402        })
403        || [
404            // Canonical forms of the boolean values in the Core schema.
405            "true", "false", "True", "False", "TRUE", "FALSE",
406            // Canonical forms of the null value in the Core schema.
407            "null", "Null", "NULL", "~",
408            // These can be quoted when emitting so that YAML 1.1 parsers do not parse them as
409            // booleans. This doesn't cause any issue with YAML 1.2 parsers.
410            "y", "Y", "n", "N", "yes", "Yes", "YES", "no", "No", "NO", "True", "TRUE", "False",
411            "FALSE", "on", "On", "ON", "off", "Off", "OFF",
412        ]
413        .contains(&string)
414        || string.starts_with('.')
415        || string.starts_with("0x")
416        || string.parse::<i64>().is_ok()
417        || string.parse::<f64>().is_ok()
418}
419
420#[cfg(test)]
421mod test {
422    use super::YamlEmitter;
423    use crate::YamlLoader;
424
425    #[test]
426    fn test_multiline_string() {
427        let input = r#"{foo: "bar!\nbar!", baz: 42}"#;
428        let parsed = YamlLoader::load_from_str(input).unwrap();
429        let mut output = String::new();
430        let mut emitter = YamlEmitter::new(&mut output);
431        emitter.multiline_strings(true);
432        emitter.dump(&parsed[0]).unwrap();
433    }
434}