Skip to main content

one_brackets/
writer.rs

1//! Writing in the platform layout: a nested list starts on a new line,
2//! and the closing bracket moves to a new line if the last item is a list. No BOM is written.
3
4use std::fmt;
5use std::io;
6
7use crate::{Node, Text, Value};
8
9impl Node {
10    /// Writes the bracket file text to any [`io::Write`].
11    pub fn write_to<W: io::Write>(&self, writer: W) -> io::Result<()> {
12        let mut adapter = IoAdapter {
13            inner: writer,
14            error: None,
15        };
16        match write(self, &mut adapter) {
17            Ok(()) => Ok(()),
18            Err(fmt::Error) => Err(adapter
19                .error
20                .unwrap_or_else(|| io::Error::other("formatter error"))),
21        }
22    }
23}
24
25impl fmt::Display for Node {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        write(self, f)
28    }
29}
30
31impl From<&Node> for String {
32    fn from(node: &Node) -> Self {
33        node.to_string()
34    }
35}
36
37impl From<&Node> for Vec<u8> {
38    fn from(node: &Node) -> Self {
39        node.to_string().into_bytes()
40    }
41}
42
43/// Not recursive: forms can be deeply nested.
44fn write(root: &Node, out: &mut impl fmt::Write) -> fmt::Result {
45    // (list items, index of the next item)
46    let mut stack: Vec<(&[Node], usize)> = Vec::new();
47    open(root, out, &mut stack)?;
48    while let Some(top) = stack.last_mut() {
49        let (items, next) = *top;
50        top.1 += 1;
51        if let Some(item) = items.get(next) {
52            if next > 0 {
53                out.write_char(',')?;
54            }
55            if item.is_list() {
56                out.write_str("\r\n")?;
57            }
58            open(item, out, &mut stack)?;
59        } else {
60            if items.last().is_some_and(Node::is_list) {
61                out.write_str("\r\n")?;
62            }
63            out.write_char('}')?;
64            stack.pop();
65        }
66    }
67    Ok(())
68}
69
70/// Writes a scalar entirely, or opens a list and pushes it onto the stack.
71fn open<'a>(
72    node: &'a Node,
73    out: &mut impl fmt::Write,
74    stack: &mut Vec<(&'a [Node], usize)>,
75) -> fmt::Result {
76    match node.value() {
77        Value::List(items) => {
78            out.write_char('{')?;
79            stack.push((items, 0));
80        }
81        Value::Str(s) => {
82            out.write_char('"')?;
83            write_quoted(s, out)?;
84            out.write_char('"')?;
85        }
86        // Without escapes an unpaired surrogate cannot be written: it becomes U+FFFD.
87        Value::Atom(s) => out.write_str(&s.to_string_lossy())?,
88    }
89    Ok(())
90}
91
92/// Text of a quoted string: a quote is doubled, and each surrogate, paired or not, is written
93/// as an escape `"\xxxx`, as the platform does: `a😀` is `a"\d83d"\de00`.
94fn write_quoted(text: &Text, out: &mut impl fmt::Write) -> fmt::Result {
95    let escape = |unit: u32, out: &mut dyn fmt::Write| write!(out, "\"\\{unit:04x}");
96    let Some(s) = text.as_str() else {
97        for c in text.code_points() {
98            match char::from_u32(c) {
99                Some(c) => write_char(c, out, escape)?,
100                None => escape(c, out)?,
101            }
102        }
103        return Ok(());
104    };
105    let mut rest = s;
106    while let Some(i) = rest.find(|c: char| c == '"' || c > '\u{ffff}') {
107        out.write_str(&rest[..i])?;
108        let c = rest[i..].chars().next().expect("found at i");
109        write_char(c, out, escape)?;
110        rest = &rest[i + c.len_utf8()..];
111    }
112    out.write_str(rest)
113}
114
115fn write_char(
116    c: char,
117    out: &mut impl fmt::Write,
118    escape: impl Fn(u32, &mut dyn fmt::Write) -> fmt::Result,
119) -> fmt::Result {
120    match c {
121        '"' => out.write_str("\"\""),
122        '\u{10000}'.. => c
123            .encode_utf16(&mut [0; 2])
124            .iter()
125            .try_for_each(|&unit| escape(u32::from(unit), out)),
126        _ => out.write_char(c),
127    }
128}
129
130struct IoAdapter<W> {
131    inner: W,
132    error: Option<io::Error>,
133}
134
135impl<W: io::Write> fmt::Write for IoAdapter<W> {
136    fn write_str(&mut self, s: &str) -> fmt::Result {
137        self.inner.write_all(s.as_bytes()).map_err(|e| {
138            self.error = Some(e);
139            fmt::Error
140        })
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147
148    #[test]
149    fn round_trips_platform_layout() {
150        let text = "{27,\r\n{18,\r\n{\r\n{1,1,\r\n{\"ru\",\"Форма\"}\r\n},25,4294967295},\
151                    \r\n{0}\r\n},\"\",-1,\r\n{2,c2f34349-b1e0-4060-8abf-3c59f38ac69f,}\r\n}";
152        assert_eq!(Node::try_from(text).unwrap().to_string(), text);
153    }
154
155    #[test]
156    fn keeps_base64_with_line_breaks() {
157        let text = "{1,\r\n{#base64:QUJD\r\r\nREVG\r\r\n},2}";
158        assert_eq!(Node::try_from(text).unwrap().to_string(), text);
159    }
160
161    #[test]
162    fn builds_and_writes() {
163        let node = Node::from(vec![
164            Node::from(1),
165            Node::string("say \"hi\""),
166            Node::from(vec![Node::from(true), Node::atom("")]),
167        ]);
168        insta::assert_snapshot!(node.to_string().replace("\r\n", "⏎"), @r#"{1,"say ""hi""",⏎{1,}⏎}"#);
169
170        let mut bytes = Vec::new();
171        node.write_to(&mut bytes).unwrap();
172        assert_eq!(bytes, Vec::<u8>::from(&node));
173        assert_eq!(Node::try_from(bytes.as_slice()).unwrap(), node);
174    }
175
176    #[test]
177    fn deep_nesting_without_recursion() {
178        let depth = 5_000;
179        let text = format!("{}{}", "{".repeat(depth), "}".repeat(depth));
180        let node = Node::try_from(text.as_str()).unwrap();
181        assert_eq!(node.to_string().replace("\r\n", ""), text);
182    }
183}