Skip to main content

ical/
validator.rs

1//! # Validator
2//!
3//! The strict half of "liberal in, strict out", as a runtime predicate rather
4//! than a second data model.
5//!
6//! Validity and lossiness are orthogonal: a conformant calendar may still
7//! carry extensions (unknown components, properties, parameters and value
8//! kinds), so "valid" cannot be a type with no `Unknown` arms.
9//!
10//! [`Ical::validate`] therefore walks the whole component tree and checks its
11//! *known* parts against the per-property
12//! [`IcalPropSpec`](crate::prop::spec::IcalPropSpec) and per-component
13//! [`IcalComponentSpec`](crate::component::spec::IcalComponentSpec) for the
14//! calendar version, leaving the unknown parts alone. It reports:
15//!
16//! - a property the version does not define;
17//! - a value of a kind the property does not take;
18//! - a parameter the property does not take;
19//! - a property that appears more often than it may;
20//! - a property a component requires but does not carry (a `VEVENT` needs `UID`
21//!   and `DTSTAMP`, a `VALARM` needs `ACTION` and `TRIGGER`, ...);
22//! - a component nested where it may not be;
23//! - a recurrence rule that breaks RFC 5545 3.3.10.
24//!
25//! A passing check mints an [`IcalValid`] marker, the only way to obtain one,
26//! so holding an `IcalValid<Ical>` is proof the check passed. The same
27//! per-property check backs the [`IcalPropBuilder`]'s strict construction.
28//!
29//! [`IcalPropBuilder`]: crate::builder::IcalPropBuilder
30//!
31//! ## Example
32//!
33//! Decode a parsed calendar, validate it into a proof, and convert that back
34//! into a byte tree:
35//!
36//! ```rust
37//! # #[cfg(feature = "parser")] {
38//! use ical::tree::cst::IcalCst;
39//!
40//! let raw = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//x//EN\r\nEND:VCALENDAR\r\n";
41//! let cst = IcalCst::parse(raw).unwrap();
42//!
43//! // validate consumes the calendar and returns the proof (or the violations).
44//! let valid = cst.decode().validate().expect("a conformant 2.0 calendar");
45//!
46//! // The proof converts back into a byte tree for free.
47//! let out = IcalCst::from(valid);
48//! assert!(out.to_string().contains("PRODID:-//x//EN"));
49//! # }
50//! ```
51
52use 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/// A single conformance failure found by [`Ical::validate`].
70#[derive(Clone, Debug, PartialEq, Eq)]
71pub enum IcalValidateError {
72    /// A property appears in a version that does not define it.
73    PropVersion {
74        /// The offending property name.
75        prop: String,
76        /// The calendar version.
77        version: IcalVersion,
78    },
79    /// A component is missing a property its spec requires.
80    MissingProp {
81        /// The component name.
82        component: String,
83        /// The required property name.
84        prop: IcalPropKind,
85    },
86    /// A property carries a value of a kind its spec does not allow for the
87    /// version.
88    ValueKind {
89        /// The offending property name.
90        prop: IcalPropKind,
91        /// The kind the value actually has.
92        kind: IcalValueKind,
93    },
94    /// A property carries a known parameter its spec does not allow for the
95    /// version. An extension parameter always passes.
96    ParamNotAllowed {
97        /// The offending property name.
98        prop: IcalPropKind,
99        /// The parameter that may not appear on it.
100        param: IcalParamKind,
101    },
102    /// A property appears more times than its cardinality permits.
103    ///
104    /// Only the "too many" direction: absence is a
105    /// [`MissingProp`](Self::MissingProp), since being required depends on the
106    /// component a property sits in and the cardinality does not.
107    TooMany {
108        /// The component name.
109        component: String,
110        /// The repeated property.
111        prop: IcalPropKind,
112        /// How many times it appears.
113        count: usize,
114    },
115    /// A component nests a child its spec does not allow.
116    Nesting {
117        /// The parent component name.
118        parent: String,
119        /// The child it may not hold.
120        child: IcalComponentKind,
121    },
122    /// A recurrence rule breaks one of the RFC 5545 3.3.10 constraints.
123    Rule {
124        /// The property carrying the rule (`RRULE` or `EXRULE`).
125        prop: IcalPropKind,
126        /// What is wrong with it.
127        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    /// Validate the whole calendar, returning an [`IcalValid`] proof or every
195    /// conformance failure found.
196    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        // NOTE: The calendar envelope requires PRODID (VERSION is the
203        // hoisted-out indicator, always present in the model).
204        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
223/// Validate one component (recursively): its properties, its required-property
224/// set, how many times each property appears, what it nests, and its nested
225/// components.
226fn 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
247/// Push a [`MissingProp`](IcalValidateError::MissingProp) for every property a
248/// component of `kind` requires but does not carry.
249fn 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
268/// The per-property check, shared by [`Ical::validate`] and the
269/// [builder](crate::builder).
270///
271/// Unknown (extension) properties, parameters and value kinds always pass:
272/// validity is a runtime predicate over the *known* vocabulary, and an
273/// extension is outside it by definition.
274///
275/// A known property must exist in the calendar's version, take a value of a
276/// kind its spec allows there, and carry only parameters that spec allows
277/// there. A recurrence value is checked against RFC 5545 3.3.10 as well.
278pub(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(&param)
309        {
310            errors.push(IcalValidateError::ParamNotAllowed { prop: kind, param });
311        }
312    }
313
314    validate_rule(kind, prop, errors);
315}
316
317/// Check the rule a `RRULE` or `EXRULE` carries against RFC 5545 3.3.10.
318///
319/// A rule the typed layer cannot even read is left alone: parsing is liberal,
320/// and an unreadable rule is a parse-level fact, not a conformance one.
321fn 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
346/// Report a [`TooMany`](IcalValidateError::TooMany) per over-frequent property.
347///
348/// Only the "too many" direction: whether a property is *required* depends on
349/// the component it sits in, which the per-property cardinality does not know,
350/// so absence stays [`check_required`]'s job.
351fn 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
388/// Push a [`Nesting`](IcalValidateError::Nesting) for every child a component
389/// may not hold. An unknown child component always passes.
390fn 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/// A value that passed its validator. Only a validator can mint one, so
411/// holding it is proof of conformance.
412///
413/// Two validators mint it, at opposite ends of the crate: [`Ical::validate`]
414/// over a whole calendar, and
415/// [`IcalRecurRule::validate`](crate::recur::IcalRecurRule::validate) over one
416/// recurrence rule.
417#[derive(Clone, Copy, Debug, PartialEq, Eq)]
418pub struct IcalValid<T>(pub(crate) T);
419
420impl<T> IcalValid<T> {
421    /// Unwrap the validated value.
422    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    /// A conformant calendar wrapping one `VEVENT` built from `props`.
507    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        // NOTE: An unknown value has no kind to check, so it cannot be the
550        // wrong one.
551        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}