1use core::{fmt, str};
22
23use alloc::{borrow::Cow, string::String, vec, vec::Vec};
24
25use crate::tree::{
26 codec::mode::Escaper,
27 error::IcalParseError,
28 leaf::{IcalLeaf, IcalValueLeaf},
29 param::{IcalParamLens, IcalParamNode},
30 value::IcalValueNode,
31 wire::IcalWire,
32};
33
34#[derive(Clone, Debug)]
43pub struct IcalLine<'a> {
44 pub name: IcalLeaf<'a>,
46 pub params: Vec<IcalParamNode<'a>>,
48 pub value: IcalValueNode<'a>,
50 pub eol: IcalLeaf<'a>,
52 pub wire: IcalWire<'a>,
56}
57
58impl<'a> IcalLine<'a> {
59 pub fn text(name: impl Into<Cow<'a, str>>, value: impl Into<Cow<'a, str>>) -> Self {
62 Self {
63 name: IcalLeaf(name.into()),
64 params: Vec::new(),
65 value: IcalValueNode::from_components(
66 vec![vec![IcalValueLeaf::from(value.into())]],
67 Escaper::Modern,
68 ),
69 eol: IcalLeaf(Cow::Borrowed("\r\n")),
70 wire: IcalWire::default(),
71 }
72 }
73
74 pub fn take(rest: &'a [u8]) -> Result<(Self, &'a [u8]), IcalParseError> {
80 let mut wire = IcalWire::default();
83
84 let mut head = rest;
86 let (first, eol, mut tail) = loop {
87 if head.is_empty() {
88 return Err(IcalParseError::MissingCrlf(lossy(rest)));
89 }
90 let (content, eol, next) = physical_line(head);
91 if content.is_empty() {
92 head = next;
93 continue;
94 }
95 break (content, eol, next);
96 };
97
98 if head.len() < rest.len() {
99 wire.skipped(0, ascii(&rest[..rest.len() - head.len()]));
100 }
101
102 let indented = first;
108 let first = strip_leading_wsp(first);
109
110 if first.len() < indented.len() {
111 wire.skipped(0, ascii(&indented[..indented.len() - first.len()]));
112 }
113
114 if first.ends_with(b"=") && head_is_quoted_printable(first) {
119 let mut logical = Vec::from(&first[..first.len() - 1]);
120 wire.soft(logical.len(), is_crlf(eol));
121
122 let mut last_eol;
123 loop {
124 let (continuation, eol, next) = physical_line(tail);
125 last_eol = eol;
126 tail = next;
127 match continuation.strip_suffix(b"=") {
128 Some(head) => {
129 logical.extend_from_slice(head);
130 if tail.is_empty() {
131 wire.skipped(logical.len(), "=");
136 break;
137 }
138 wire.soft(logical.len(), is_crlf(eol));
139 }
140 None => {
141 logical.extend_from_slice(continuation);
142 break;
143 }
144 }
145 }
146
147 let mut line = Self::parse(&logical, b"")?.into_static();
148 line.eol = eol_leaf(last_eol);
149 line.wire.prepend(wire.into_static());
150 return Ok((line, tail));
151 }
152
153 if !starts_with_wsp(tail) {
154 let mut line = Self::parse(first, eol)?;
155 line.wire.prepend(wire);
156 return Ok((line, tail));
157 }
158
159 let mut logical = Vec::from(first);
160 let mut last_eol = eol;
161
162 while starts_with_wsp(tail) {
163 let (continuation, eol, next) = physical_line(&tail[1..]);
164 wire.fold(logical.len(), is_crlf(last_eol), tail[0]);
165 logical.extend_from_slice(continuation);
166 last_eol = eol;
167 tail = next;
168 }
169
170 let mut line = Self::parse(&logical, b"")?.into_static();
171 line.eol = eol_leaf(last_eol);
172 line.wire.prepend(wire.into_static());
173
174 Ok((line, tail))
175 }
176
177 pub fn take_physical(rest: &'a [u8]) -> (&'a [u8], &'a [u8]) {
184 let (content, eol, tail) = physical_line(rest);
185 (&rest[..content.len() + eol.len()], tail)
186 }
187
188 pub(crate) fn into_static(self) -> IcalLine<'static> {
190 IcalLine {
191 name: self.name.into_static(),
192 params: self
193 .params
194 .into_iter()
195 .map(IcalParamNode::into_static)
196 .collect(),
197 value: self.value.into_static(),
198 eol: self.eol.into_static(),
199 wire: self.wire.into_static(),
200 }
201 }
202
203 pub fn raw_value(&self) -> &[u8] {
205 self.value.first_value_bytes()
206 }
207
208 pub fn raw_value_str(&self) -> Cow<'_, str> {
211 String::from_utf8_lossy(self.value.first_value_bytes())
212 }
213
214 pub(crate) fn write_bytes(&self, out: &mut Vec<u8>) {
217 if self.wire.is_empty() {
218 self.write_logical(out);
219 } else {
220 let mut logical = Vec::new();
221 self.write_logical(&mut logical);
222 self.wire.write_bytes(&logical, out);
223 }
224
225 out.extend_from_slice(self.eol.get().as_bytes());
226 }
227
228 fn write_logical(&self, out: &mut Vec<u8>) {
232 out.extend_from_slice(self.name.get().as_bytes());
233
234 for param in &self.params {
235 out.push(b';');
236 param.write_bytes(out);
237 }
238
239 out.push(b':');
240 self.value.write_bytes(out);
241 }
242
243 pub fn param<P: IcalParamLens>(&self) -> Option<P::Target<'_>> {
245 self.params
246 .iter()
247 .find(|param| param.name.get().eq_ignore_ascii_case(&P::KIND))
248 .map(|param| P::decode(param))
249 }
250
251 pub fn param_mut<P: IcalParamLens>(&mut self) -> Option<&mut IcalParamNode<'a>> {
253 self.params
254 .iter_mut()
255 .find(|param| param.name.get().eq_ignore_ascii_case(&P::KIND))
256 }
257
258 fn parse<'b>(content: &'b [u8], eol: &'b [u8]) -> Result<IcalLine<'b>, IcalParseError> {
263 let Some(colon) = memchr::memchr(b':', content) else {
264 return Err(IcalParseError::MissingPropertyColon(lossy(content)));
265 };
266
267 let head = str::from_utf8(&content[..colon])
268 .map_err(|_| IcalParseError::NonUtf8Header(lossy(&content[..colon])))?;
269 let (name, params) = split_head(head);
270
271 let mut value = &content[colon + 1..];
272 let mut wire = IcalWire::default();
273
274 if head_is_quoted_printable(content) {
282 let full = value.len();
283 while value.last() == Some(&b'=') {
284 value = &value[..value.len() - 1];
285 }
286 if value.len() < full {
287 let end = colon + 1 + value.len();
288 wire.skipped(end, ascii(&content[end..colon + 1 + full]));
289 }
290 }
291
292 wire.seal(colon + 1 + value.len());
293
294 Ok(IcalLine {
295 name: IcalLeaf::from(name),
296 params,
297 value: IcalValueNode::parse(value),
298 eol: IcalLeaf::from(str::from_utf8(eol).unwrap_or("")),
299 wire,
300 })
301 }
302}
303
304impl fmt::Display for IcalLine<'_> {
305 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
308 if self.wire.is_empty() {
309 f.write_str(self.name.get())?;
310
311 for param in &self.params {
312 write!(f, ";{param}")?;
313 }
314
315 return write!(f, ":{}{}", self.value, self.eol.get());
316 }
317
318 let mut bytes = Vec::new();
319 self.write_bytes(&mut bytes);
320 f.write_str(&String::from_utf8_lossy(&bytes))
321 }
322}
323
324fn split_head(head: &str) -> (&str, Vec<IcalParamNode<'_>>) {
326 let (name, mut rest) = match head.find(';') {
327 Some(semi) => (&head[..semi], &head[semi..]),
328 None => return (head, Vec::new()),
329 };
330
331 let mut params = Vec::new();
332
333 while let Some(after) = rest.strip_prefix(';') {
334 let (param, tail) = match after.find(';') {
335 Some(semi) => (&after[..semi], &after[semi..]),
336 None => (after, ""),
337 };
338
339 params.push(IcalParamNode::parse(param));
340 rest = tail;
341 }
342
343 (name, params)
344}
345
346fn physical_line(rest: &[u8]) -> (&[u8], &[u8], &[u8]) {
350 let Some(lf) = memchr::memchr(b'\n', rest) else {
351 return (rest, b"", b"");
352 };
353
354 let tail = &rest[lf + 1..];
355
356 let (content, eol) = if lf > 0 && rest[lf - 1] == b'\r' {
357 (&rest[..lf - 1], &rest[lf - 1..lf + 1])
358 } else {
359 (&rest[..lf], &rest[lf..lf + 1])
360 };
361
362 (content, eol, tail)
363}
364
365fn starts_with_wsp(rest: &[u8]) -> bool {
367 matches!(rest.first(), Some(b' ' | b'\t'))
368}
369
370fn strip_leading_wsp(mut bytes: &[u8]) -> &[u8] {
374 while matches!(bytes.first(), Some(b' ' | b'\t' | b'\r' | b'\n')) {
375 bytes = &bytes[1..];
376 }
377 bytes
378}
379
380fn head_is_quoted_printable(line: &[u8]) -> bool {
383 let head = match memchr::memchr(b':', line) {
384 Some(colon) => &line[..colon],
385 None => return false,
386 };
387
388 head.split(|&b| b == b';').any(|token| {
389 token.eq_ignore_ascii_case(b"QUOTED-PRINTABLE")
390 || token.eq_ignore_ascii_case(b"ENCODING=QUOTED-PRINTABLE")
391 })
392}
393
394fn eol_leaf(bytes: &[u8]) -> IcalLeaf<'static> {
396 IcalLeaf::from(String::from_utf8_lossy(bytes).into_owned())
397}
398
399fn is_crlf(eol: &[u8]) -> bool {
401 eol.starts_with(b"\r")
402}
403
404fn ascii(bytes: &[u8]) -> &str {
408 str::from_utf8(bytes).unwrap_or("")
409}
410
411fn lossy(bytes: &[u8]) -> String {
413 String::from_utf8_lossy(bytes).into_owned()
414}
415
416#[cfg(test)]
417mod tests {
418 use alloc::string::ToString;
419
420 use crate::tree::{line::IcalLine, value::IcalValueNode};
421
422 #[test]
423 fn takes_one_line_and_leaves_the_rest() {
424 let (line, rest) = IcalLine::take(b"FN:John\r\nEND:VCALENDAR\r\n").unwrap();
425 assert_eq!(line.name.get(), "FN");
426 assert_eq!(line.to_string(), "FN:John\r\n");
427 assert_eq!(rest, b"END:VCALENDAR\r\n");
428 }
429
430 #[test]
431 fn splits_parameters_off_the_head_then_round_trips() {
432 let (line, _) = IcalLine::take(b"TEL;TYPE=work,home:123\r\n").unwrap();
433 assert_eq!(line.params.len(), 1);
434 assert_eq!(line.to_string(), "TEL;TYPE=work,home:123\r\n");
435 }
436
437 #[test]
438 fn accepts_a_bare_lf_ending() {
439 let (line, _) = IcalLine::take(b"FN:John\n").unwrap();
440 assert_eq!(line.to_string(), "FN:John\n");
441 }
442
443 #[test]
444 fn unfolds_space_and_tab_continuations() {
445 let (line, rest) =
446 IcalLine::take(b"NOTE:foo\r\n bar\r\n\tbaz\r\nEND:VCALENDAR\r\n").unwrap();
447 assert_eq!(line.name.get(), "NOTE");
448 assert_eq!(line.raw_value_str(), "foobarbaz");
449 assert_eq!(rest, b"END:VCALENDAR\r\n");
450 }
451
452 #[test]
453 fn serializes_a_folded_line_back_folded() {
454 let (line, _) = IcalLine::take(b"NOTE:foo\r\n bar\r\n").unwrap();
455 assert_eq!(line.raw_value_str(), "foobar");
456 assert_eq!(line.to_string(), "NOTE:foo\r\n bar\r\n");
457 }
458
459 #[test]
460 fn keeps_the_folding_whitespace_and_the_break_it_arrived_with() {
461 let (line, _) = IcalLine::take(b"NOTE:foo\n\tbar\r\n").unwrap();
462 assert_eq!(line.to_string(), "NOTE:foo\n\tbar\r\n");
463 }
464
465 #[test]
466 fn serializes_a_skipped_blank_line_back() {
467 let (line, _) = IcalLine::take(b"\r\n\r\nFN:John\r\n").unwrap();
468 assert_eq!(line.to_string(), "\r\n\r\nFN:John\r\n");
469 }
470
471 #[test]
472 fn drops_the_fold_points_once_the_value_is_edited() {
473 let (mut line, _) = IcalLine::take(b"NOTE:foo\r\n bar\r\n").unwrap();
476 line.value = IcalValueNode::parse(b"something else entirely");
477 assert_eq!(line.to_string(), "NOTE:something else entirely\r\n");
478 }
479
480 #[test]
481 fn keeps_the_fold_points_when_an_edit_keeps_the_length() {
482 let (mut line, _) = IcalLine::take(b"NOTE:foo\r\n bar\r\n").unwrap();
485 line.value = IcalValueNode::parse(b"BARFOO");
486 assert_eq!(line.to_string(), "NOTE:BAR\r\n FOO\r\n");
487 }
488
489 #[test]
490 fn keeps_whitespace_beyond_the_single_fold_indicator() {
491 let (line, _) = IcalLine::take(b"NOTE:foo\r\n bar\r\n").unwrap();
494 assert_eq!(line.raw_value_str(), "foo bar");
495 }
496
497 #[test]
498 fn skips_blank_lines_before_the_next_line() {
499 let (line, rest) = IcalLine::take(b"\r\n\r\nFN:John\r\nEND:VCALENDAR\r\n").unwrap();
500 assert_eq!(line.name.get(), "FN");
501 assert_eq!(rest, b"END:VCALENDAR\r\n");
502 }
503
504 #[test]
505 fn tolerates_a_missing_final_line_break() {
506 let (line, rest) = IcalLine::take(b"END:VCALENDAR").unwrap();
507 assert_eq!(line.name.get(), "END");
508 assert_eq!(line.to_string(), "END:VCALENDAR");
509 assert_eq!(rest, b"");
510 }
511
512 #[test]
513 fn joins_a_quoted_printable_soft_broken_line() {
514 let (line, _) = IcalLine::take(
517 b"NOTE;ENCODING=QUOTED-PRINTABLE:caf=\r\n=C3=\r\n=A9\r\nEND:VCALENDAR\r\n",
518 )
519 .unwrap();
520 assert_eq!(line.name.get(), "NOTE");
521 assert_eq!(line.raw_value_str(), "caf=C3=A9");
522 assert_eq!(line.raw_value(), b"caf=C3=A9");
523 }
524
525 #[test]
526 fn errors_when_there_is_no_content_line() {
527 assert!(IcalLine::take(b"").is_err());
528 assert!(IcalLine::take(b"\r\n\r\n").is_err());
529 }
530
531 #[test]
532 fn rejects_a_non_utf8_head() {
533 let mut raw = b"X-".to_vec();
534 raw.push(0xff);
535 raw.extend_from_slice(b":v\r\n");
536 assert!(IcalLine::take(&raw).is_err());
537 }
538
539 #[test]
540 fn finds_a_parameter_mutably() {
541 use crate::tree::param::language::LANGUAGE;
542
543 let (mut line, _) = IcalLine::take(b"SUMMARY;LANGUAGE=en:Lunch\r\n").unwrap();
544 assert!(line.param_mut::<LANGUAGE>().is_some());
545 }
546
547 #[test]
548 fn a_trailing_equals_without_a_colon_is_not_quoted_printable() {
549 assert!(IcalLine::take(b"abc=\r\n").is_err());
552 }
553
554 #[test]
555 fn quoted_printable_join_stops_at_an_empty_tail() {
556 let (line, rest) = IcalLine::take(b"NOTE;ENCODING=QUOTED-PRINTABLE:a=\r\nb=\r\n").unwrap();
560 assert_eq!(line.raw_value_str(), "ab");
561 assert_eq!(rest, b"");
562 }
563}