• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lennart-k / ical-rs / #68

12 Jan 2026 10:17AM UTC coverage: 80.485% (+2.7%) from 77.802%
#68

Pull #2

lennart-k
fixes
Pull Request #2: Major overhaul

935 of 1181 new or added lines in 30 files covered. (79.17%)

21 existing lines in 9 files now uncovered.

1262 of 1568 relevant lines covered (80.48%)

2.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

81.97
/src/parser/mod.rs
1
//! Wrapper around `PropertyParser`
2
//!
3
//! #### Warning
4
//!   The parsers (`VcardParser` / `IcalParser`) only parse the content and set to uppercase
5
//!   the case-insensitive fields.  No checks are made on the fields validity.
6
//!
7
//!
8
pub mod ical;
9
pub mod vcard;
10
use crate::types::{CalDateTimeError, InvalidDuration};
11
use crate::{
12
    LineReader,
13
    property::{ContentLine, PropertyError, PropertyParser},
14
};
15
use std::collections::HashMap;
16
use std::io::BufRead;
17
use std::marker::PhantomData;
18

19
mod property;
20
pub use property::*;
21

22
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
23
pub enum ParserError {
24
    #[error("empty input")]
25
    EmptyInput,
26
    #[error("too many components in input, expected one")]
27
    TooManyComponents,
28
    #[error("invalid component: {0}")]
29
    InvalidComponent(String),
30
    #[error("incomplete object")]
31
    NotComplete,
32
    #[error("missing header")]
33
    MissingHeader,
34
    #[error("property error: {0}")]
35
    PropertyError(#[from] PropertyError),
36
    #[error("missing property: {0}")]
37
    MissingProperty(&'static str),
38
    #[error("missing property: UID")]
39
    MissingUID,
40
    #[error("property conflict: {0}")]
41
    PropertyConflict(&'static str),
42
    #[error(transparent)]
43
    InvalidDuration(#[from] InvalidDuration),
44
    #[error("invalid property value: {0}")]
45
    InvalidPropertyValue(String),
46
    #[error("invalid property value type for: {0}")]
47
    InvalidPropertyType(String),
48
    #[error(transparent)]
49
    RRule(#[from] rrule::RRuleError),
50
    #[error(transparent)]
51
    DateTime(#[from] CalDateTimeError),
52
    #[error("Invalid CALSCALE: Only GREGORIAN supported")]
53
    InvalidCalscale,
54
    #[error("Invalid VERSION: MUST be 1.0 or 2.0")]
55
    InvalidVersion,
56
    #[error("Multiple main events are not allowed in a calendar object")]
57
    MultipleMainObjects,
58
    #[error("Differing UIDs inside a calendar object")]
59
    DifferingUIDs,
60
    #[error("Override without RECURRENCE-ID")]
61
    MissingRecurId,
62
}
63

64
/// An immutable interface for an Ical/Vcard component.
65
/// This is also implemented by verified components
UNCOV
66
pub trait Component: Clone {
×
67
    const NAMES: &[&str];
68

NEW
69
    fn get_comp_name(&self) -> &'static str {
×
NEW
70
        assert_eq!(
×
NEW
71
            Self::NAMES.len(),
×
NEW
72
            1,
×
NEW
73
            "Default implementation only applicable for fixed component name"
×
74
        );
NEW
75
        Self::NAMES[0]
×
76
    }
77

78
    type Unverified: ComponentMut;
79

80
    fn get_properties(&self) -> &Vec<ContentLine>;
81
    fn mutable(self) -> Self::Unverified;
82

83
    fn get_property<'c>(&'c self, name: &str) -> Option<&'c ContentLine> {
5✔
84
        self.get_properties().iter().find(|p| p.name == name)
17✔
85
    }
86

87
    fn get_named_properties<'c>(&'c self, name: &str) -> Vec<&'c ContentLine> {
9✔
88
        self.get_properties()
9✔
89
            .iter()
90
            .filter(|p| p.name == name)
30✔
91
            .collect()
92
    }
93
}
94

95
/// A mutable interface for an Ical/Vcard component.
96
///
97
/// It takes a `PropertyParser` and fills the component with. It's also able to create
98
/// sub-component used by event and alarms.
99
pub trait ComponentMut: Component + Default {
100
    type Verified: Component<Unverified = Self>;
101

102
    /// Add the givent sub component.
103
    fn add_sub_component<B: BufRead>(
104
        &mut self,
105
        value: &str,
106
        line_parser: &mut PropertyParser<B>,
107
    ) -> Result<(), ParserError>;
108

109
    fn get_properties_mut(&mut self) -> &mut Vec<ContentLine>;
110

UNCOV
111
    fn remove_property(&mut self, name: &str) {
×
112
        self.get_properties_mut().retain(|prop| prop.name != name);
×
113
    }
114

115
    /// Add the given property.
116
    fn add_content_line(&mut self, property: ContentLine) {
13✔
117
        self.get_properties_mut().push(property);
25✔
118
    }
119

120
    fn build(
121
        self,
122
        timezones: Option<&HashMap<String, Option<chrono_tz::Tz>>>,
123
    ) -> Result<Self::Verified, ParserError>;
124

125
    /// Parse the content from `line_parser` and fill the component with.
126
    fn parse<B: BufRead>(
13✔
127
        &mut self,
128
        line_parser: &mut PropertyParser<B>,
129
    ) -> Result<(), ParserError> {
130
        loop {
25✔
131
            let line = line_parser.next().ok_or(ParserError::NotComplete)??;
12✔
132

133
            match line.name.as_ref() {
24✔
134
                "END" => break,
13✔
135
                "BEGIN" => match line.value {
33✔
136
                    Some(v) => self.add_sub_component(v.as_str(), line_parser)?,
7✔
UNCOV
137
                    None => return Err(ParserError::NotComplete),
×
138
                },
139

140
                _ => self.add_content_line(line),
27✔
141
            };
142
        }
143
        Ok(())
14✔
144
    }
145

146
    fn from_parser<B: BufRead>(line_parser: &mut PropertyParser<B>) -> Result<Self, ParserError> {
4✔
147
        let mut out = Self::default();
4✔
148
        out.parse(line_parser)?;
8✔
149
        Ok(out)
4✔
150
    }
151
}
152

153
/// Reader returning `IcalCalendar` object from a `BufRead`.
154
pub struct ComponentParser<B: BufRead, T: Component> {
155
    line_parser: PropertyParser<B>,
156
    _t: PhantomData<T>,
157
}
158

159
impl<B: BufRead, T: Component> ComponentParser<B, T> {
160
    /// Return a new `IcalParser` from a `Reader`.
161
    pub fn new(reader: B) -> ComponentParser<B, T> {
6✔
162
        let line_reader = LineReader::new(reader);
7✔
163
        let line_parser = PropertyParser::new(line_reader);
7✔
164

165
        ComponentParser {
166
            line_parser,
167
            _t: Default::default(),
7✔
168
        }
169
    }
170

171
    /// Read the next line and check if it's a valid VCALENDAR start.
172
    fn check_header(&mut self) -> Result<Option<()>, ParserError> {
7✔
173
        let line = match self.line_parser.next() {
7✔
174
            Some(val) => val.map_err(ParserError::PropertyError)?,
5✔
175
            None => return Ok(None),
4✔
176
        };
177

178
        if line.name.to_uppercase() != "BEGIN"
11✔
179
            || line.value.is_none()
6✔
180
            || !T::NAMES.contains(&line.value.as_ref().unwrap().to_uppercase().as_str())
11✔
181
            || !line.params.is_empty()
5✔
182
        {
183
            return Err(ParserError::MissingHeader);
2✔
184
        }
185

186
        Ok(Some(()))
6✔
187
    }
188

189
    pub fn expect_one(mut self) -> Result<<T::Unverified as ComponentMut>::Verified, ParserError> {
2✔
190
        let item = self.next().ok_or(ParserError::EmptyInput)??;
4✔
191
        if self.next().is_some() {
4✔
NEW
192
            return Err(ParserError::TooManyComponents);
×
193
        }
194
        Ok(item)
2✔
195
    }
196
}
197

198
impl<B: BufRead, T: Component> Iterator for ComponentParser<B, T> {
199
    type Item = Result<<T::Unverified as ComponentMut>::Verified, ParserError>;
200

201
    fn next(&mut self) -> Option<Self::Item> {
7✔
202
        match self.check_header() {
7✔
203
            Ok(res) => res?,
7✔
204
            Err(err) => return Some(Err(err)),
2✔
205
        };
206

207
        let mut comp = T::Unverified::default();
7✔
208
        let result = match comp.parse(&mut self.line_parser) {
13✔
209
            Ok(_) => comp.build(None),
6✔
210
            Err(err) => Err(err),
2✔
211
        };
212

213
        #[cfg(feature = "test")]
214
        {
215
            // Run this for more test coverage
216
            if let Ok(comp) = result.as_ref() {
18✔
217
                let mutable = comp.clone().mutable();
6✔
218
                mutable.get_properties();
6✔
219
            }
220
        }
221

222
        Some(result)
6✔
223
    }
224
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc