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