1use core::{fmt, iter, mem};
45
46use alloc::{
47 borrow::Cow,
48 boxed::Box,
49 string::{String, ToString},
50 vec,
51 vec::Vec,
52};
53
54use crate::{
55 component::spec::IcalComponentSpec,
56 prop::IcalProp,
57 tree::{codec::mode::Escaper, error::IcalParseError, line::IcalLine, prop::lens::IcalPropLens},
58 version::IcalVersion,
59};
60
61#[derive(Clone, Debug)]
63pub enum IcalItem<'a> {
64 Prop(IcalLine<'a>),
66 Component(Box<IcalCst<'a>>),
69 Opaque(Cow<'a, [u8]>),
74}
75
76#[derive(Clone, Debug)]
82pub struct IcalCst<'a> {
83 pub begin: Option<IcalLine<'a>>,
86 pub items: Vec<IcalItem<'a>>,
88 pub end: Option<IcalLine<'a>>,
90 pub trailing: Cow<'a, str>,
94}
95
96impl<'a> IcalCst<'a> {
97 pub fn v2() -> Self {
100 Self {
101 begin: Some(IcalLine::text("BEGIN", "VCALENDAR")),
102 items: vec![IcalItem::Prop(IcalLine::text(
103 "VERSION",
104 &*IcalVersion::V2_0,
105 ))],
106 end: Some(IcalLine::text("END", "VCALENDAR")),
107 trailing: Cow::Borrowed(""),
108 }
109 }
110
111 pub fn parse<T: AsRef<[u8]> + ?Sized>(input: &'a T) -> Result<Self, IcalParseError> {
118 let input = input.as_ref();
119 let (first, _rest) = IcalLine::take(input)?;
120
121 if first.name.get().eq_ignore_ascii_case("BEGIN") {
122 let (mut cst, rest) = Self::take_component(input)?;
123 cst.take_trailing(rest);
124 let escaper = Escaper::for_version_str(&cst.version_str());
125 cst.stamp_escaper(escaper);
126 Ok(cst)
127 } else {
128 Self::parse_bare(input)
129 }
130 }
131
132 fn parse_bare(input: &'a [u8]) -> Result<Self, IcalParseError> {
134 let mut items: Vec<IcalItem<'a>> = Vec::new();
135 let mut rest = input;
136
137 while !is_blank(rest) {
138 let (line, tail) = IcalLine::take(rest)?;
139 items.push(IcalItem::Prop(line));
140 rest = tail;
141 }
142
143 let mut cst = Self {
144 begin: None,
145 items,
146 end: None,
147 trailing: Cow::Borrowed(""),
148 };
149 cst.take_trailing(rest);
150 let escaper = Escaper::for_version_str(&cst.version_str());
151 cst.stamp_escaper(escaper);
152 Ok(cst)
153 }
154
155 pub fn parse_many<T: AsRef<[u8]> + ?Sized>(
162 input: &'a T,
163 ) -> impl Iterator<Item = Result<Self, IcalParseError>> {
164 let mut rest = input.as_ref();
165
166 iter::from_fn(move || {
167 if is_blank(rest) {
168 return None;
169 }
170
171 match Self::take_component(rest) {
172 Ok((mut cst, tail)) => {
173 rest = cst.take_trailing(tail);
174 let escaper = Escaper::for_version_str(&cst.version_str());
175 cst.stamp_escaper(escaper);
176 Some(Ok(cst))
177 }
178 Err(error) => {
179 rest = b"";
180 Some(Err(error))
181 }
182 }
183 })
184 }
185
186 pub fn parse_recovering<T: AsRef<[u8]> + ?Sized>(input: &'a T) -> IcalRecovery<'a> {
199 let mut rest = input.as_ref();
200 let mut recovery = IcalRecovery::default();
201
202 let mut loose: Vec<IcalItem<'a>> = Vec::new();
205
206 while !is_blank(rest) {
207 match IcalLine::take(rest) {
208 Ok((line, _tail)) if line.name.get().eq_ignore_ascii_case("BEGIN") => {
209 recovery.close_loose(&mut loose);
210
211 let (mut cst, tail) = Self::take_component_recovering(rest, &mut recovery);
212 rest = tail;
213 let escaper = Escaper::for_version_str(&cst.version_str());
214 cst.stamp_escaper(escaper);
215 recovery.calendars.push(cst);
216 }
217 Ok((line, tail)) => {
218 loose.push(IcalItem::Prop(line));
219 rest = tail;
220 }
221 Err(error) => {
222 let (opaque, tail) = IcalLine::take_physical(rest);
223 loose.push(IcalItem::Opaque(Cow::Borrowed(opaque)));
224 recovery.problems.push(error);
225 rest = tail;
226 }
227 }
228 }
229
230 recovery.close_loose(&mut loose);
231
232 if let Some(last) = recovery.calendars.last_mut() {
233 last.take_trailing(rest);
234 } else {
235 let mut bare = Self::bare(Vec::new());
236 bare.take_trailing(rest);
237 recovery.calendars.push(bare);
238 }
239
240 recovery
241 }
242
243 fn take_component_recovering(
247 input: &'a [u8],
248 recovery: &mut IcalRecovery<'a>,
249 ) -> (Self, &'a [u8]) {
250 let (begin, mut rest) = IcalLine::take(input).expect("a BEGIN line");
253 let name = begin.raw_value_str().into_owned();
254
255 let mut items: Vec<IcalItem<'a>> = Vec::new();
256
257 loop {
258 if is_blank(rest) {
259 recovery.problems.push(IcalParseError::MissingEnd(name));
260 return (
261 Self {
262 begin: Some(begin),
263 items,
264 end: None,
265 trailing: Cow::Borrowed(""),
266 },
267 rest,
268 );
269 }
270
271 match IcalLine::take(rest) {
272 Ok((line, tail)) => {
273 let line_name = line.name.get();
274
275 if line_name.eq_ignore_ascii_case("END") {
276 return (
277 Self {
278 begin: Some(begin),
279 items,
280 end: Some(line),
281 trailing: Cow::Borrowed(""),
282 },
283 tail,
284 );
285 }
286
287 if line_name.eq_ignore_ascii_case("BEGIN") {
288 let (child, next) = Self::take_component_recovering(rest, recovery);
289 items.push(IcalItem::Component(Box::new(child)));
290 rest = next;
291 continue;
292 }
293
294 items.push(IcalItem::Prop(line));
295 rest = tail;
296 }
297 Err(error) => {
298 let (opaque, tail) = IcalLine::take_physical(rest);
299 items.push(IcalItem::Opaque(Cow::Borrowed(opaque)));
300 recovery.problems.push(error);
301 rest = tail;
302 }
303 }
304 }
305 }
306
307 fn bare(items: Vec<IcalItem<'a>>) -> Self {
309 Self {
310 begin: None,
311 items,
312 end: None,
313 trailing: Cow::Borrowed(""),
314 }
315 }
316
317 fn take_trailing(&mut self, rest: &'a [u8]) -> &'a [u8] {
320 if !is_blank(rest) {
321 return rest;
322 }
323
324 self.trailing = Cow::Borrowed(str::from_utf8(rest).unwrap_or(""));
325 b""
326 }
327
328 fn take_component(input: &'a [u8]) -> Result<(Self, &'a [u8]), IcalParseError> {
332 let (begin, mut rest) = IcalLine::take(input)?;
333
334 if !begin.name.get().eq_ignore_ascii_case("BEGIN") {
335 return Err(IcalParseError::ExpectedBegin(begin.name.get().to_string()));
336 }
337
338 let mut items: Vec<IcalItem<'a>> = Vec::new();
339
340 loop {
341 if rest.is_empty() {
342 return Err(IcalParseError::MissingEnd(
345 begin.raw_value_str().into_owned(),
346 ));
347 }
348
349 let (line, tail) = IcalLine::take(rest)?;
350 let name = line.name.get();
351
352 if name.eq_ignore_ascii_case("END") {
353 return Ok((
354 Self {
355 begin: Some(begin),
356 items,
357 end: Some(line),
358 trailing: Cow::Borrowed(""),
359 },
360 tail,
361 ));
362 }
363
364 if name.eq_ignore_ascii_case("BEGIN") {
365 let (child, next) = Self::take_component(rest)?;
366 items.push(IcalItem::Component(Box::new(child)));
367 rest = next;
368 continue;
369 }
370
371 items.push(IcalItem::Prop(line));
372 rest = tail;
373 }
374 }
375
376 fn stamp_escaper(&mut self, escaper: Escaper) {
380 for item in &mut self.items {
381 match item {
382 IcalItem::Prop(line) => {
383 line.value.escaper = escaper;
384 for param in &mut line.params {
385 param.escaper = escaper;
386 }
387 }
388 IcalItem::Component(child) => child.stamp_escaper(escaper),
389 IcalItem::Opaque(_) => {}
390 }
391 }
392 }
393
394 fn version_str(&self) -> Cow<'_, str> {
396 self.items
397 .iter()
398 .find_map(|item| match item {
399 IcalItem::Prop(line) if line.name.get().eq_ignore_ascii_case("VERSION") => {
400 Some(line.raw_value_str())
401 }
402 _ => None,
403 })
404 .unwrap_or(Cow::Borrowed(""))
405 }
406
407 pub fn version(&self) -> IcalVersion {
411 self.version_str().parse().unwrap_or(IcalVersion::V2_0)
412 }
413
414 pub fn push(&mut self, prop: IcalProp<'a>) -> &mut Self {
416 let escaper = Escaper::for_version_str(&self.version_str());
417 self.items.push(IcalItem::Prop(prop.encode(escaper)));
418 self
419 }
420
421 pub fn push_component(&mut self, component: IcalCst<'a>) -> &mut Self {
423 self.items.push(IcalItem::Component(Box::new(component)));
424 self
425 }
426
427 pub fn remove<L: IcalPropLens>(&mut self) -> &mut Self {
430 self.items.retain(|item| match item {
431 IcalItem::Prop(line) => !line.name.get().eq_ignore_ascii_case(&L::KIND),
432 IcalItem::Component(_) => true,
433 IcalItem::Opaque(_) => true,
434 });
435 self
436 }
437
438 pub fn prop<L: IcalPropLens>(&self) -> Option<L::Target<'_>> {
440 let version = self.version();
441 self.items.iter().find_map(|item| match item {
442 IcalItem::Prop(line) if line.name.get().eq_ignore_ascii_case(&L::KIND) => {
443 Some(L::decode(line, version))
444 }
445 _ => None,
446 })
447 }
448
449 pub fn prop_mut<L: IcalPropLens>(&mut self) -> Option<L::Cursor<'_, 'a>> {
452 self.items.iter_mut().find_map(|item| match item {
453 IcalItem::Prop(line) if line.name.get().eq_ignore_ascii_case(&L::KIND) => {
454 Some(L::cursor(line))
455 }
456 _ => None,
457 })
458 }
459
460 pub fn component<C: IcalComponentSpec>(&self) -> Option<&IcalCst<'a>> {
462 self.items.iter().find_map(|item| match item {
463 IcalItem::Component(child) if child.is_kind::<C>() => Some(&**child),
464 _ => None,
465 })
466 }
467
468 pub fn component_mut<C: IcalComponentSpec>(&mut self) -> Option<&mut IcalCst<'a>> {
470 self.items.iter_mut().find_map(|item| match item {
471 IcalItem::Component(child) if child.is_kind::<C>() => Some(&mut **child),
472 _ => None,
473 })
474 }
475
476 pub fn components<C: IcalComponentSpec>(&self) -> impl Iterator<Item = &IcalCst<'a>> {
478 self.items.iter().filter_map(|item| match item {
479 IcalItem::Component(child) if child.is_kind::<C>() => Some(&**child),
480 _ => None,
481 })
482 }
483
484 fn is_kind<C: IcalComponentSpec>(&self) -> bool {
486 self.begin
487 .as_ref()
488 .map(|begin| begin.raw_value_str().eq_ignore_ascii_case(&C::KIND))
489 .unwrap_or(false)
490 }
491
492 pub(crate) fn component_name(&self) -> Cow<'_, str> {
495 self.begin
496 .as_ref()
497 .map(|begin| begin.raw_value_str())
498 .unwrap_or(Cow::Borrowed(""))
499 }
500
501 pub fn into_static(self) -> IcalCst<'static> {
504 IcalCst {
505 begin: self.begin.map(IcalLine::into_static),
506 items: self
507 .items
508 .into_iter()
509 .map(|item| match item {
510 IcalItem::Prop(line) => IcalItem::Prop(line.into_static()),
511 IcalItem::Component(child) => {
512 IcalItem::Component(Box::new(child.into_static()))
513 }
514 IcalItem::Opaque(bytes) => IcalItem::Opaque(Cow::Owned(bytes.into_owned())),
515 })
516 .collect(),
517 end: self.end.map(IcalLine::into_static),
518 trailing: Cow::Owned(self.trailing.into_owned()),
519 }
520 }
521
522 pub fn to_bytes(&self) -> Vec<u8> {
524 let mut out = Vec::new();
525 self.write_bytes(&mut out);
526 out
527 }
528
529 fn write_bytes(&self, out: &mut Vec<u8>) {
530 if let Some(begin) = &self.begin {
531 begin.write_bytes(out);
532 }
533 for item in &self.items {
534 match item {
535 IcalItem::Prop(line) => line.write_bytes(out),
536 IcalItem::Opaque(bytes) => out.extend_from_slice(bytes),
537 IcalItem::Component(child) => child.write_bytes(out),
538 }
539 }
540 if let Some(end) = &self.end {
541 end.write_bytes(out);
542 }
543 out.extend_from_slice(self.trailing.as_bytes());
544 }
545}
546
547#[derive(Clone, Debug, Default)]
553pub struct IcalRecovery<'a> {
554 pub calendars: Vec<IcalCst<'a>>,
557 pub problems: Vec<IcalParseError>,
559}
560
561impl<'a> IcalRecovery<'a> {
562 pub fn is_clean(&self) -> bool {
565 self.problems.is_empty()
566 }
567
568 pub fn to_bytes(&self) -> Vec<u8> {
570 let mut out = Vec::new();
571
572 for cst in &self.calendars {
573 cst.write_bytes(&mut out);
574 }
575
576 out
577 }
578
579 fn close_loose(&mut self, loose: &mut Vec<IcalItem<'a>>) {
581 if loose.is_empty() {
582 return;
583 }
584
585 self.calendars.push(IcalCst::bare(mem::take(loose)));
586 }
587}
588
589fn is_blank(bytes: &[u8]) -> bool {
591 bytes.iter().all(|byte| matches!(byte, b'\r' | b'\n'))
592}
593
594impl fmt::Display for IcalCst<'_> {
595 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
596 if let Some(begin) = &self.begin {
597 write!(f, "{begin}")?;
598 }
599 for item in &self.items {
600 match item {
601 IcalItem::Prop(line) => write!(f, "{line}")?,
602 IcalItem::Opaque(bytes) => f.write_str(&String::from_utf8_lossy(bytes))?,
603 IcalItem::Component(child) => write!(f, "{child}")?,
604 }
605 }
606 if let Some(end) = &self.end {
607 write!(f, "{end}")?;
608 }
609 Ok(())
610 }
611}
612
613#[cfg(test)]
614mod tests {
615 use alloc::{
616 string::{String, ToString},
617 vec::Vec,
618 };
619
620 use crate::{
621 component::vevent::VEVENT,
622 prop::{prodid::PRODID, summary::SUMMARY},
623 tree::{cst::IcalCst, error::IcalParseError},
624 version::IcalVersion,
625 };
626
627 const CAL: &str = concat!(
628 "BEGIN:VCALENDAR\r\n",
629 "VERSION:2.0\r\n",
630 "PRODID:-//Example//EN\r\n",
631 "BEGIN:VEVENT\r\n",
632 "UID:1\r\n",
633 "DTSTAMP:20260101T000000Z\r\n",
634 "SUMMARY:Lunch\r\n",
635 "BEGIN:VALARM\r\n",
636 "ACTION:DISPLAY\r\n",
637 "TRIGGER:-PT15M\r\n",
638 "END:VALARM\r\n",
639 "END:VEVENT\r\n",
640 "END:VCALENDAR\r\n",
641 );
642
643 #[test]
644 fn round_trips_a_nested_calendar_byte_for_byte() {
645 let cst = IcalCst::parse(CAL).unwrap();
646 assert_eq!(cst.to_string(), CAL);
647 }
648
649 #[test]
650 fn reads_a_nested_property_through_component_and_prop_lenses() {
651 let cst = IcalCst::parse(CAL).unwrap();
652 let event = cst.component::<VEVENT>().expect("a VEVENT");
653 assert_eq!(&*event.prop::<SUMMARY>().unwrap().0, "Lunch");
654 }
655
656 #[test]
657 fn edits_a_nested_property_leaving_every_other_byte_intact() {
658 let mut cst = IcalCst::parse(CAL).unwrap();
659 cst.component_mut::<VEVENT>()
660 .unwrap()
661 .prop_mut::<SUMMARY>()
662 .unwrap()
663 .set_text("Dinner");
664 assert_eq!(
665 cst.to_string(),
666 CAL.replace("SUMMARY:Lunch", "SUMMARY:Dinner")
667 );
668 }
669
670 #[test]
671 fn reports_the_version() {
672 let cst = IcalCst::parse(CAL).unwrap();
673 assert_eq!(cst.version(), IcalVersion::V2_0);
674 }
675
676 #[test]
677 fn round_trips_a_folded_calendar_byte_for_byte() {
678 let raw = concat!(
681 "BEGIN:VCALENDAR\r\n",
682 "VERSION:2.0\r\n",
683 "PRODID:-//Example//EN\r\n",
684 "\r\n",
685 "BEGIN:VEVENT\r\n",
686 "UID:1\r\n",
687 "DTSTAMP:20260101T000000Z\r\n",
688 "DESCRIPTION:a very long description that an exporter would fold at s\r\n",
689 " ome column\r\n",
690 "END:VEVENT\r\n",
691 "END:VCALENDAR\r\n",
692 "\r\n",
693 );
694
695 let cst = IcalCst::parse(raw).unwrap();
696 assert_eq!(String::from_utf8(cst.to_bytes()).unwrap(), raw);
697 }
698
699 #[test]
700 fn round_trips_a_leading_blank_line() {
701 let raw = "\r\nBEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n";
702 let cst = IcalCst::parse(raw).unwrap();
703 assert_eq!(String::from_utf8(cst.to_bytes()).unwrap(), raw);
704 }
705
706 #[test]
707 fn round_trips_a_quoted_printable_value_ending_on_two_equals() {
708 let raw = concat!(
713 "BEGIN:VCALENDAR\r\n",
714 "VERSION:2.0\r\n",
715 "NOTE;ENCODING=QUOTED-PRINTABLE:x==\r\n",
716 "\r\n",
717 "END:VCALENDAR\r\n",
718 );
719
720 let cst = IcalCst::parse(raw).unwrap();
721 let bytes = cst.to_bytes();
722
723 assert_eq!(String::from_utf8(bytes.clone()).unwrap(), raw);
724 assert_eq!(IcalCst::parse(&bytes).unwrap().to_bytes(), bytes);
725 }
726
727 #[test]
728 fn round_trips_a_whole_multi_calendar_file() {
729 let raw = concat!(
733 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n",
734 "\r\n",
735 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nEND:VCALENDAR\r\n",
736 );
737
738 let mut out = Vec::new();
739 for cst in IcalCst::parse_many(raw) {
740 out.extend_from_slice(&cst.unwrap().to_bytes());
741 }
742
743 assert_eq!(String::from_utf8(out).unwrap(), raw);
744 }
745
746 #[test]
747 fn recovers_a_line_with_no_colon() {
748 let raw = concat!(
749 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\n",
750 "this line has no colon\r\n",
751 "PRODID:-//Example//EN\r\nEND:VCALENDAR\r\n",
752 );
753
754 assert!(IcalCst::parse(raw).is_err());
755
756 let recovery = IcalCst::parse_recovering(raw);
757 assert_eq!(String::from_utf8(recovery.to_bytes()).unwrap(), raw);
758 assert_eq!(recovery.calendars.len(), 1);
759 assert!(matches!(
760 recovery.problems.as_slice(),
761 [IcalParseError::MissingPropertyColon(_)]
762 ));
763
764 let cal = &recovery.calendars[0];
765 assert_eq!(&*cal.prop::<PRODID>().unwrap().0, "-//Example//EN");
766 }
767
768 #[test]
769 fn recovers_a_component_with_no_end() {
770 let raw = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\nUID:1\r\n";
771
772 assert!(IcalCst::parse(raw).is_err());
773
774 let recovery = IcalCst::parse_recovering(raw);
775 assert_eq!(String::from_utf8(recovery.to_bytes()).unwrap(), raw);
776 assert_eq!(
777 recovery.problems,
778 [
779 IcalParseError::MissingEnd("VEVENT".into()),
780 IcalParseError::MissingEnd("VCALENDAR".into()),
781 ]
782 );
783 assert!(recovery.calendars[0].component::<VEVENT>().is_some());
784 }
785
786 #[test]
787 fn reports_nothing_for_a_calendar_the_strict_parser_accepts() {
788 let recovery = IcalCst::parse_recovering(CAL);
789 assert!(recovery.is_clean());
790 assert_eq!(String::from_utf8(recovery.to_bytes()).unwrap(), CAL);
791 }
792
793 #[test]
794 fn refolds_nothing_once_a_value_is_edited() {
795 let raw = concat!(
796 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n",
797 "SUMMARY:a summary long enough to have been fol\r\n ded by its exporter\r\n",
798 "END:VEVENT\r\nEND:VCALENDAR\r\n",
799 );
800
801 let mut cst = IcalCst::parse(raw).unwrap();
802 cst.component_mut::<VEVENT>()
803 .unwrap()
804 .prop_mut::<SUMMARY>()
805 .unwrap()
806 .set_text("Dinner");
807
808 assert_eq!(
809 String::from_utf8(cst.to_bytes()).unwrap(),
810 concat!(
811 "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nBEGIN:VEVENT\r\n",
812 "SUMMARY:Dinner\r\n",
813 "END:VEVENT\r\nEND:VCALENDAR\r\n",
814 )
815 );
816 }
817}