1use core::{error, fmt, ops};
53
54use alloc::{
55 string::{String, ToString},
56 vec::Vec,
57};
58
59use crate::{
60 component::{IcalComponent, IcalComponentKind, IcalComponentName, spec::component_spec},
61 ical::Ical,
62 param::IcalParamKind,
63 prop::{IcalProp, IcalPropKind, IcalPropName, spec::prop_spec},
64 recur::validate::IcalRecurRuleProblem,
65 value::IcalValueKind,
66 version::IcalVersion,
67};
68
69#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum IcalValidateError {
72 PropVersion {
74 prop: String,
76 version: IcalVersion,
78 },
79 MissingProp {
81 component: String,
83 prop: IcalPropKind,
85 },
86 ValueKind {
89 prop: IcalPropKind,
91 kind: IcalValueKind,
93 },
94 ParamNotAllowed {
97 prop: IcalPropKind,
99 param: IcalParamKind,
101 },
102 TooMany {
108 component: String,
110 prop: IcalPropKind,
112 count: usize,
114 },
115 Nesting {
117 parent: String,
119 child: IcalComponentKind,
121 },
122 Rule {
124 prop: IcalPropKind,
126 problem: IcalRecurRuleProblem,
128 },
129}
130
131impl fmt::Display for IcalValidateError {
132 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
133 match self {
134 Self::PropVersion { prop, version } => {
135 write!(
136 f,
137 "Property `{prop}` is not defined in version {}",
138 &**version
139 )
140 }
141 Self::MissingProp { component, prop } => {
142 write!(
143 f,
144 "Component `{component}` is missing required property `{}`",
145 &**prop
146 )
147 }
148 Self::ValueKind { prop, kind } => {
149 write!(
150 f,
151 "Property `{}` does not take a {} value",
152 &**prop, &**kind
153 )
154 }
155 Self::ParamNotAllowed { prop, param } => {
156 write!(
157 f,
158 "Property `{}` does not take the `{}` parameter",
159 &**prop, &**param
160 )
161 }
162 Self::TooMany {
163 component,
164 prop,
165 count,
166 } => {
167 write!(
168 f,
169 "Component `{component}` carries property `{}` {count} times",
170 &**prop
171 )
172 }
173 Self::Nesting { parent, child } => {
174 write!(
175 f,
176 "Component `{parent}` does not nest a `{}` component",
177 &**child
178 )
179 }
180 Self::Rule { prop, problem } => {
181 write!(
182 f,
183 "Property `{}` carries an invalid rule: {problem}",
184 &**prop
185 )
186 }
187 }
188 }
189}
190
191impl error::Error for IcalValidateError {}
192
193impl Ical<'_> {
194 pub fn validate(self) -> Result<IcalValid<Self>, Vec<IcalValidateError>> {
197 let mut errors = Vec::new();
198
199 for prop in &self.props {
200 validate_prop(prop, self.version, &mut errors);
201 }
202 check_required(
205 IcalComponentKind::VCalendar,
206 "VCALENDAR",
207 &self.props,
208 &mut errors,
209 );
210
211 for component in &self.components {
212 validate_component(component, self.version, &mut errors);
213 }
214
215 if errors.is_empty() {
216 Ok(IcalValid(self))
217 } else {
218 Err(errors)
219 }
220 }
221}
222
223fn validate_component(
227 component: &IcalComponent<'_>,
228 version: IcalVersion,
229 errors: &mut Vec<IcalValidateError>,
230) {
231 for prop in &component.props {
232 validate_prop(prop, version, errors);
233 }
234
235 check_cardinality(&component.name, &component.props, version, errors);
236
237 if let IcalComponentName::Kind(kind) = component.name {
238 check_required(kind, &component.name, &component.props, errors);
239 check_nesting(kind, &component.name, &component.components, errors);
240 }
241
242 for child in &component.components {
243 validate_component(child, version, errors);
244 }
245}
246
247fn check_required(
250 kind: IcalComponentKind,
251 name: &str,
252 props: &[IcalProp<'_>],
253 errors: &mut Vec<IcalValidateError>,
254) {
255 for &required in (component_spec(kind).required_props)() {
256 let present = props
257 .iter()
258 .any(|prop| matches!(prop.name, IcalPropName::Kind(k) if k == required));
259 if !present {
260 errors.push(IcalValidateError::MissingProp {
261 component: name.to_string(),
262 prop: required,
263 });
264 }
265 }
266}
267
268pub(crate) fn validate_prop(
279 prop: &IcalProp<'_>,
280 version: IcalVersion,
281 errors: &mut Vec<IcalValidateError>,
282) {
283 let IcalPropName::Kind(kind) = prop.name else {
284 return;
285 };
286
287 let spec = prop_spec(kind);
288
289 if !(spec.allowed_versions)().contains(&version) {
290 errors.push(IcalValidateError::PropVersion {
291 prop: (*kind).to_string(),
292 version,
293 });
294 }
295
296 if let Some(value) = prop.value.kind()
297 && !(spec.allowed_values)(version).contains(&value)
298 {
299 errors.push(IcalValidateError::ValueKind {
300 prop: kind,
301 kind: value,
302 });
303 }
304
305 let allowed_params = (spec.allowed_params)(version);
306 for param in &prop.params {
307 if let Some(param) = param.kind()
308 && !allowed_params.contains(¶m)
309 {
310 errors.push(IcalValidateError::ParamNotAllowed { prop: kind, param });
311 }
312 }
313
314 validate_rule(kind, prop, errors);
315}
316
317fn validate_rule(kind: IcalPropKind, prop: &IcalProp<'_>, errors: &mut Vec<IcalValidateError>) {
322 use crate::{recur::IcalRecurRule, value::IcalValue};
323
324 if !matches!(kind, IcalPropKind::RRule | IcalPropKind::ExRule) {
325 return;
326 }
327
328 let IcalValue::Recur(recur) = &prop.value else {
329 return;
330 };
331
332 let Ok(rule) = IcalRecurRule::parse(&recur.0) else {
333 return;
334 };
335
336 errors.extend(
337 rule.problems()
338 .into_iter()
339 .map(|problem| IcalValidateError::Rule {
340 prop: kind,
341 problem,
342 }),
343 );
344}
345
346fn check_cardinality(
352 name: &str,
353 props: &[IcalProp<'_>],
354 version: IcalVersion,
355 errors: &mut Vec<IcalValidateError>,
356) {
357 use crate::prop::cardinality::IcalPropCardinality::{AtMostOne, ExactlyOne};
358
359 let mut seen: Vec<(IcalPropKind, usize)> = Vec::new();
360
361 for prop in props {
362 let IcalPropName::Kind(kind) = prop.name else {
363 continue;
364 };
365
366 match seen.iter_mut().find(|(held, _)| *held == kind) {
367 Some((_, count)) => *count += 1,
368 None => seen.push((kind, 1)),
369 }
370 }
371
372 for (kind, count) in seen {
373 if count > 1
374 && matches!(
375 (prop_spec(kind).cardinality)(version),
376 ExactlyOne | AtMostOne
377 )
378 {
379 errors.push(IcalValidateError::TooMany {
380 component: name.to_string(),
381 prop: kind,
382 count,
383 });
384 }
385 }
386}
387
388fn check_nesting(
391 kind: IcalComponentKind,
392 name: &str,
393 children: &[IcalComponent<'_>],
394 errors: &mut Vec<IcalValidateError>,
395) {
396 let allowed = (component_spec(kind).allowed_children)();
397
398 for child in children {
399 if let IcalComponentName::Kind(child_kind) = child.name
400 && !allowed.contains(&child_kind)
401 {
402 errors.push(IcalValidateError::Nesting {
403 parent: name.to_string(),
404 child: child_kind,
405 });
406 }
407 }
408}
409
410#[derive(Clone, Copy, Debug, PartialEq, Eq)]
418pub struct IcalValid<T>(pub(crate) T);
419
420impl<T> IcalValid<T> {
421 pub fn into_inner(self) -> T {
423 self.0
424 }
425}
426
427impl<T> ops::Deref for IcalValid<T> {
428 type Target = T;
429
430 fn deref(&self) -> &Self::Target {
431 &self.0
432 }
433}
434
435impl<'a> TryFrom<Ical<'a>> for IcalValid<Ical<'a>> {
436 type Error = Vec<IcalValidateError>;
437
438 fn try_from(cal: Ical<'a>) -> Result<Self, Self::Error> {
439 cal.validate()
440 }
441}
442
443#[cfg(test)]
444mod tests {
445 use alloc::vec;
446
447 use crate::{
448 component::{IcalComponent, IcalComponentKind},
449 ical::Ical,
450 param::IcalParamKind,
451 prop::{IcalProp, IcalPropKind},
452 validator::IcalValidateError,
453 value::{IcalValue, IcalValueKind, datetime::IcalDateTime, text::IcalText},
454 version::IcalVersion,
455 };
456
457 fn prop(kind: IcalPropKind, value: IcalValue<'static>) -> IcalProp<'static> {
458 IcalProp {
459 name: kind.into(),
460 params: vec![],
461 value,
462 }
463 }
464
465 #[test]
466 fn accepts_a_conformant_calendar() {
467 let cal = Ical {
468 version: IcalVersion::V2_0,
469 props: vec![prop(
470 IcalPropKind::ProdId,
471 IcalValue::Text(IcalText("-//x//EN".into())),
472 )],
473 components: vec![IcalComponent {
474 name: IcalComponentKind::VEvent.into(),
475 props: vec![
476 prop(IcalPropKind::Uid, IcalValue::Text(IcalText("1".into()))),
477 prop(
478 IcalPropKind::DtStamp,
479 IcalValue::DateTime(IcalDateTime("20260101T000000Z".into())),
480 ),
481 ],
482 components: vec![],
483 }],
484 };
485 assert!(cal.validate().is_ok());
486 }
487
488 #[test]
489 fn flags_a_component_missing_a_required_property() {
490 let cal = Ical {
491 version: IcalVersion::V2_0,
492 props: vec![prop(
493 IcalPropKind::ProdId,
494 IcalValue::Text(IcalText("-//x//EN".into())),
495 )],
496 components: vec![IcalComponent {
497 name: IcalComponentKind::VEvent.into(),
498 props: vec![],
499 components: vec![],
500 }],
501 };
502 let errors = cal.validate().unwrap_err();
503 assert_eq!(errors.len(), 2);
504 }
505
506 fn around(props: vec::Vec<IcalProp<'static>>) -> Ical<'static> {
508 let mut event = vec![
509 prop(IcalPropKind::Uid, IcalValue::Text(IcalText("1".into()))),
510 prop(
511 IcalPropKind::DtStamp,
512 IcalValue::DateTime(IcalDateTime("20260101T000000Z".into())),
513 ),
514 ];
515 event.extend(props);
516
517 Ical {
518 version: IcalVersion::V2_0,
519 props: vec![prop(
520 IcalPropKind::ProdId,
521 IcalValue::Text(IcalText("-//x//EN".into())),
522 )],
523 components: vec![IcalComponent {
524 name: IcalComponentKind::VEvent.into(),
525 props: event,
526 components: vec![],
527 }],
528 }
529 }
530
531 #[test]
532 fn flags_a_value_of_the_wrong_kind() {
533 let cal = around(vec![prop(
534 IcalPropKind::Summary,
535 IcalValue::DateTime(IcalDateTime("20260101T000000Z".into())),
536 )]);
537
538 assert_eq!(
539 cal.validate().unwrap_err(),
540 [IcalValidateError::ValueKind {
541 prop: IcalPropKind::Summary,
542 kind: IcalValueKind::DateTime,
543 }]
544 );
545 }
546
547 #[test]
548 fn passes_an_extension_value_kind() {
549 let cal = around(vec![IcalProp {
552 name: "X-THING".into(),
553 params: vec![],
554 value: IcalValue::DateTime(IcalDateTime("20260101T000000Z".into())),
555 }]);
556
557 assert!(cal.validate().is_ok());
558 }
559
560 #[test]
561 fn flags_a_parameter_the_property_does_not_take() {
562 use crate::param::IcalParam;
563
564 let cal = around(vec![IcalProp {
565 name: IcalPropKind::Summary.into(),
566 params: vec![IcalParam::PartStat("ACCEPTED".into())],
567 value: IcalValue::Text(IcalText("Lunch".into())),
568 }]);
569
570 assert_eq!(
571 cal.validate().unwrap_err(),
572 [IcalValidateError::ParamNotAllowed {
573 prop: IcalPropKind::Summary,
574 param: IcalParamKind::PartStat,
575 }]
576 );
577 }
578
579 #[test]
580 fn passes_an_extension_parameter() {
581 use crate::param::IcalParam;
582
583 let cal = around(vec![IcalProp {
584 name: IcalPropKind::Summary.into(),
585 params: vec![IcalParam::Unknown {
586 name: "X-THING".into(),
587 values: vec!["1".into()],
588 }],
589 value: IcalValue::Text(IcalText("Lunch".into())),
590 }]);
591
592 assert!(cal.validate().is_ok());
593 }
594
595 #[test]
596 fn flags_a_single_valued_property_that_repeats() {
597 let cal = around(vec![
598 prop(IcalPropKind::Summary, IcalValue::Text(IcalText("a".into()))),
599 prop(IcalPropKind::Summary, IcalValue::Text(IcalText("b".into()))),
600 ]);
601
602 assert_eq!(
603 cal.validate().unwrap_err(),
604 [IcalValidateError::TooMany {
605 component: "VEVENT".into(),
606 prop: IcalPropKind::Summary,
607 count: 2,
608 }]
609 );
610 }
611
612 #[test]
613 fn passes_a_repeatable_property_that_repeats() {
614 let cal = around(vec![
615 prop(
616 IcalPropKind::Comment,
617 IcalValue::Text(IcalText("one".into())),
618 ),
619 prop(
620 IcalPropKind::Comment,
621 IcalValue::Text(IcalText("two".into())),
622 ),
623 ]);
624
625 assert!(cal.validate().is_ok());
626 }
627
628 #[test]
629 fn flags_a_component_nested_where_it_may_not_be() {
630 let mut cal = around(vec![]);
631 cal.components[0].components.push(IcalComponent {
632 name: IcalComponentKind::VTimezone.into(),
633 props: vec![prop(
634 IcalPropKind::TzId,
635 IcalValue::Text(IcalText("Europe/Paris".into())),
636 )],
637 components: vec![],
638 });
639
640 let errors = cal.validate().unwrap_err();
641 assert!(errors.contains(&IcalValidateError::Nesting {
642 parent: "VEVENT".into(),
643 child: IcalComponentKind::VTimezone,
644 }));
645 }
646
647 #[test]
648 fn passes_an_extension_component() {
649 let mut cal = around(vec![]);
650 cal.components[0].components.push(IcalComponent {
651 name: "X-THING".into(),
652 props: vec![],
653 components: vec![],
654 });
655
656 assert!(cal.validate().is_ok());
657 }
658
659 #[test]
660 fn flags_a_rule_the_rfc_forbids() {
661 use crate::{
662 recur::{IcalRecurFreq, validate::IcalRecurPart, validate::IcalRecurRuleProblem},
663 value::recur::IcalRecur,
664 };
665
666 let cal = around(vec![prop(
667 IcalPropKind::RRule,
668 IcalValue::Recur(IcalRecur("FREQ=MONTHLY;BYWEEKNO=3".into())),
669 )]);
670
671 assert_eq!(
672 cal.validate().unwrap_err(),
673 [IcalValidateError::Rule {
674 prop: IcalPropKind::RRule,
675 problem: IcalRecurRuleProblem::PartFreq {
676 part: IcalRecurPart::ByWeekNo,
677 freq: IcalRecurFreq::Monthly,
678 },
679 }]
680 );
681 }
682}