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