Skip to main content

ical/tree/
wire.rs

1//! # Wire shape
2//!
3//! What a content line looked like on the wire, kept beside the logical line it
4//! parsed into.
5//!
6//! A real calendar folds at 75 octets, blank-lines its components apart and,
7//! under vCalendar 1.0, breaks a `QUOTED-PRINTABLE` value across physical
8//! lines. Every layer above the parser wants the *logical* line, so
9//! [`IcalLine::take`](crate::tree::line::IcalLine::take) resolves all three
10//! away. [`IcalWire`] is what makes that resolution reversible: a list of byte
11//! offsets into the logical line, each holding the bytes the wire carried
12//! there, so serialization reproduces the input exactly rather than a
13//! normalised paraphrase of it.
14//!
15//! ## Offsets are logical, and checked
16//!
17//! An offset indexes the line's logical bytes: its name, its parameters and its
18//! value, exactly as `IcalLine::write_bytes`
19//! lays them out, with the line ending excluded (which is why a blank line
20//! before the line is an insertion at offset 0). The logical length is recorded
21//! with them, and a shape whose length no longer matches is dropped rather than
22//! applied: an edit that changes a value's length moves every byte after it, so
23//! the old fold points would land in the wrong places. An edited line is
24//! written unfolded, which RFC 5545 3.1 permits (it recommends 75 octets, it
25//! does not require them).
26
27use alloc::{borrow::Cow, vec::Vec};
28
29/// One piece of wire the parser resolved away.
30#[derive(Clone, Debug)]
31pub enum IcalWirePart<'a> {
32    /// An RFC 5545 3.1 fold: a line break, then the single whitespace that
33    /// marked the continuation.
34    Fold {
35        /// Whether the break was `\r\n` rather than a bare `\n`.
36        crlf: bool,
37        /// The folding whitespace, a space or a tab.
38        wsp: u8,
39    },
40    /// A `QUOTED-PRINTABLE` soft line break: an `=` and the break after it.
41    Soft {
42        /// Whether the break was `\r\n` rather than a bare `\n`.
43        crlf: bool,
44    },
45    /// Bytes taken verbatim off the wire and dropped: the blank lines before a
46    /// content line, the whitespace of a dangling continuation, or a trailing
47    /// `=` left over from a soft break with nothing to continue.
48    Skipped(Cow<'a, str>),
49}
50
51impl IcalWirePart<'_> {
52    /// Write the piece back out.
53    fn write_bytes(&self, out: &mut Vec<u8>) {
54        match self {
55            Self::Fold { crlf, wsp } => {
56                write_eol(*crlf, out);
57                out.push(*wsp);
58            }
59            Self::Soft { crlf } => {
60                out.push(b'=');
61                write_eol(*crlf, out);
62            }
63            Self::Skipped(bytes) => out.extend_from_slice(bytes.as_bytes()),
64        }
65    }
66
67    /// Convert into an owned piece (`'static`).
68    fn into_static(self) -> IcalWirePart<'static> {
69        match self {
70            Self::Fold { crlf, wsp } => IcalWirePart::Fold { crlf, wsp },
71            Self::Soft { crlf } => IcalWirePart::Soft { crlf },
72            Self::Skipped(bytes) => IcalWirePart::Skipped(Cow::Owned(bytes.into_owned())),
73        }
74    }
75}
76
77fn write_eol(crlf: bool, out: &mut Vec<u8>) {
78    out.extend_from_slice(if crlf { b"\r\n" } else { b"\n" });
79}
80
81/// The wire shape of one content line: every piece the parser resolved away,
82/// with the offset it sat at and the logical length those offsets index.
83///
84/// Empty for a line that was built rather than parsed, and for a line whose
85/// wire shape *is* its logical shape (unfolded, with no blank line before it).
86#[derive(Clone, Debug, Default)]
87pub struct IcalWire<'a> {
88    /// The pieces, in the order they occur on the wire.
89    parts: Vec<(usize, IcalWirePart<'a>)>,
90    /// The logical length these offsets were taken against.
91    len: usize,
92}
93
94impl<'a> IcalWire<'a> {
95    /// Whether the line's wire shape is its logical shape.
96    pub fn is_empty(&self) -> bool {
97        self.parts.is_empty()
98    }
99
100    /// Record a fold at `offset`.
101    pub(crate) fn fold(&mut self, offset: usize, crlf: bool, wsp: u8) {
102        self.parts.push((offset, IcalWirePart::Fold { crlf, wsp }));
103    }
104
105    /// Record a `QUOTED-PRINTABLE` soft break at `offset`.
106    pub(crate) fn soft(&mut self, offset: usize, crlf: bool) {
107        self.parts.push((offset, IcalWirePart::Soft { crlf }));
108    }
109
110    /// Record bytes dropped verbatim at `offset`.
111    pub(crate) fn skipped(&mut self, offset: usize, bytes: &'a str) {
112        self.parts
113            .push((offset, IcalWirePart::Skipped(Cow::Borrowed(bytes))));
114    }
115
116    /// Pin the logical length the offsets were taken against.
117    pub(crate) fn seal(&mut self, len: usize) {
118        self.len = len;
119    }
120
121    /// Put `earlier`'s pieces before this shape's, keeping the sealed length.
122    ///
123    /// The tokeniser records what it resolved (blank lines, folds, soft breaks)
124    /// while the line splitter records only a trailing `=` at the very end, so
125    /// the two lists concatenate already ordered.
126    pub(crate) fn prepend(&mut self, mut earlier: IcalWire<'a>) {
127        if earlier.parts.is_empty() {
128            return;
129        }
130
131        earlier.parts.append(&mut self.parts);
132        self.parts = earlier.parts;
133    }
134
135    /// Write `logical` back to the wire, re-inserting every piece.
136    ///
137    /// A shape whose sealed length no longer matches `logical` is stale, left
138    /// by an edit, and is dropped: the logical bytes go out unfolded.
139    pub(crate) fn write_bytes(&self, logical: &[u8], out: &mut Vec<u8>) {
140        if self.parts.is_empty() || self.len != logical.len() {
141            out.extend_from_slice(logical);
142            return;
143        }
144
145        let mut at = 0;
146
147        for (offset, part) in &self.parts {
148            // NOTE: Clamped, so a shape recorded against other bytes can never
149            // index out of this line or walk backwards.
150            let offset = (*offset).clamp(at, logical.len());
151            out.extend_from_slice(&logical[at..offset]);
152            part.write_bytes(out);
153            at = offset;
154        }
155
156        out.extend_from_slice(&logical[at..]);
157    }
158
159    /// Convert into an owned shape (`'static`).
160    pub(crate) fn into_static(self) -> IcalWire<'static> {
161        IcalWire {
162            parts: self
163                .parts
164                .into_iter()
165                .map(|(offset, part)| (offset, part.into_static()))
166                .collect(),
167            len: self.len,
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use alloc::vec::Vec;
175
176    use crate::tree::wire::IcalWire;
177
178    fn written(wire: &IcalWire<'_>, logical: &[u8]) -> Vec<u8> {
179        let mut out = Vec::new();
180        wire.write_bytes(logical, &mut out);
181        out
182    }
183
184    #[test]
185    fn re_inserts_a_fold_where_it_was() {
186        let mut wire = IcalWire::default();
187        wire.fold(3, true, b' ');
188        wire.seal(6);
189
190        assert_eq!(written(&wire, b"foobar"), b"foo\r\n bar");
191    }
192
193    #[test]
194    fn re_inserts_a_blank_line_before_the_name() {
195        let mut wire = IcalWire::default();
196        wire.skipped(0, "\r\n");
197        wire.seal(6);
198
199        assert_eq!(written(&wire, b"foobar"), b"\r\nfoobar");
200    }
201
202    #[test]
203    fn re_inserts_a_soft_break() {
204        let mut wire = IcalWire::default();
205        wire.soft(3, false);
206        wire.seal(6);
207
208        assert_eq!(written(&wire, b"foobar"), b"foo=\nbar");
209    }
210
211    #[test]
212    fn drops_a_shape_taken_against_other_bytes() {
213        // NOTE: What an edit leaves behind: the value grew, so every fold point
214        // after it is wrong and the whole shape has to go.
215        let mut wire = IcalWire::default();
216        wire.fold(3, true, b' ');
217        wire.seal(6);
218
219        assert_eq!(written(&wire, b"foobarbaz"), b"foobarbaz");
220    }
221
222    #[test]
223    fn keeps_the_order_of_pieces_at_one_offset() {
224        let mut wire = IcalWire::default();
225        wire.skipped(0, "\r\n");
226        wire.skipped(0, " ");
227        wire.seal(3);
228
229        assert_eq!(written(&wire, b"foo"), b"\r\n foo");
230    }
231
232    #[test]
233    fn prepends_an_earlier_shape_before_a_later_one() {
234        let mut earlier = IcalWire::default();
235        earlier.skipped(0, "\r\n");
236
237        let mut wire = IcalWire::default();
238        wire.skipped(3, "=");
239        wire.seal(3);
240        wire.prepend(earlier);
241
242        assert_eq!(written(&wire, b"foo"), b"\r\nfoo=");
243    }
244}