netget/server/dav_common/
object.rs1use anyhow::{bail, ensure, Context, Result};
5
6pub const MAX_OBJECT_BYTES: usize = 512 * 1024;
7const MAX_LINES: usize = 20_000;
8const MAX_DEPTH: usize = 8;
9
10#[derive(Debug, Clone, PartialEq)]
11pub struct Property {
12 pub name: String,
13 pub params: Vec<(String, String)>,
14 pub value: String,
15}
16
17impl Property {
18 pub fn param(&self, name: &str) -> Option<&str> {
19 self.params
20 .iter()
21 .find(|(k, _)| k.eq_ignore_ascii_case(name))
22 .map(|(_, v)| v.as_str())
23 }
24}
25
26#[derive(Debug, Clone, PartialEq)]
27pub struct Component {
28 pub name: String,
29 pub props: Vec<Property>,
30 pub children: Vec<Component>,
31}
32
33impl Component {
34 pub fn prop(&self, name: &str) -> Option<&Property> {
35 self.props
36 .iter()
37 .find(|p| p.name.eq_ignore_ascii_case(name))
38 }
39 pub fn props_named<'a>(&'a self, name: &'a str) -> impl Iterator<Item = &'a Property> + 'a {
40 self.props
41 .iter()
42 .filter(move |p| p.name.eq_ignore_ascii_case(name))
43 }
44}
45
46fn lines(text: &str) -> Result<Vec<String>> {
48 ensure!(
49 text.len() <= MAX_OBJECT_BYTES,
50 "object over {} KiB",
51 MAX_OBJECT_BYTES / 1024
52 );
53 let mut out: Vec<String> = Vec::new();
54 for raw in text.split('\n') {
55 let raw = raw.strip_suffix('\r').unwrap_or(raw);
56 if let Some(cont) = raw.strip_prefix([' ', '\t']) {
57 let last = out
58 .last_mut()
59 .context("object starts with a continuation line")?;
60 last.push_str(cont);
61 } else if !raw.is_empty() {
62 out.push(raw.to_owned());
63 }
64 ensure!(
65 out.len() <= MAX_LINES,
66 "object has more than {MAX_LINES} lines"
67 );
68 }
69 Ok(out)
70}
71
72fn name_ok(s: &str) -> bool {
73 !s.is_empty() && s.len() <= 128 && s.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-')
74}
75
76fn parse_line(line: &str) -> Result<Property> {
78 let mut i = 0;
79 let b = line.as_bytes();
80 while i < b.len() && b[i] != b';' && b[i] != b':' {
81 i += 1;
82 }
83 let name = &line[..i];
84 let bare = name.rsplit('.').next().unwrap_or(name);
86 ensure!(
87 name_ok(bare) && name.split('.').all(name_ok),
88 "invalid property name {name:?}"
89 );
90 let mut params = Vec::new();
91 while i < b.len() && b[i] == b';' {
92 i += 1;
93 let start = i;
94 while i < b.len() && b[i] != b'=' && b[i] != b';' && b[i] != b':' {
95 i += 1;
96 }
97 let pname = line[start..i].to_owned();
98 ensure!(name_ok(&pname), "invalid parameter name in {name}");
99 let mut value = String::new();
100 if i < b.len() && b[i] == b'=' {
101 i += 1;
102 loop {
103 if i < b.len() && b[i] == b'"' {
104 let end = line[i + 1..]
105 .find('"')
106 .context("unterminated quoted parameter")?
107 + i
108 + 1;
109 value.push_str(&line[i + 1..end]);
110 i = end + 1;
111 } else {
112 let start = i;
113 while i < b.len() && b[i] != b';' && b[i] != b':' && b[i] != b',' {
114 i += 1;
115 }
116 value.push_str(&line[start..i]);
117 }
118 if i < b.len() && b[i] == b',' {
119 value.push(',');
120 i += 1;
121 } else {
122 break;
123 }
124 }
125 }
126 params.push((pname, value));
127 }
128 ensure!(i < b.len() && b[i] == b':', "property {name} has no value");
129 Ok(Property {
130 name: bare.to_ascii_uppercase(),
131 params,
132 value: line[i + 1..].to_owned(),
133 })
134}
135
136pub fn parse(text: &str, root: &str) -> Result<Component> {
138 let lines = lines(text)?;
139 let mut stack: Vec<Component> = Vec::new();
140 let mut done: Option<Component> = None;
141 for line in &lines {
142 ensure!(done.is_none(), "content after END:{root}");
143 let p = parse_line(line)?;
144 match p.name.as_str() {
145 "BEGIN" => {
146 ensure!(stack.len() < MAX_DEPTH, "components nest too deeply");
147 ensure!(name_ok(&p.value), "invalid component name");
148 if stack.is_empty() {
149 ensure!(p.value.eq_ignore_ascii_case(root), "expected BEGIN:{root}");
150 }
151 stack.push(Component {
152 name: p.value.to_ascii_uppercase(),
153 props: vec![],
154 children: vec![],
155 });
156 }
157 "END" => {
158 let c = stack.pop().context("END without BEGIN")?;
159 ensure!(
160 c.name.eq_ignore_ascii_case(&p.value),
161 "END:{} closes {}",
162 p.value,
163 c.name
164 );
165 match stack.last_mut() {
166 Some(parent) => parent.children.push(c),
167 None => done = Some(c),
168 }
169 }
170 _ => stack
171 .last_mut()
172 .context("property outside a component")?
173 .props
174 .push(p),
175 }
176 }
177 ensure!(stack.is_empty(), "unterminated component");
178 done.with_context(|| format!("no {root} component"))
179}
180
181pub fn check_calendar(text: &str) -> Result<(String, String, Component)> {
184 let cal = parse(text, "VCALENDAR")?;
185 ensure!(
186 cal.prop("VERSION").is_some_and(|v| v.value == "2.0"),
187 "VCALENDAR needs VERSION:2.0"
188 );
189 ensure!(cal.prop("PRODID").is_some(), "VCALENDAR needs PRODID");
190 let items: Vec<&Component> = cal
191 .children
192 .iter()
193 .filter(|c| c.name != "VTIMEZONE")
194 .collect();
195 ensure!(!items.is_empty(), "no calendar component");
196 let kind = items[0].name.clone();
197 ensure!(
198 matches!(kind.as_str(), "VEVENT" | "VTODO" | "VJOURNAL" | "VFREEBUSY"),
199 "unsupported component {kind}"
200 );
201 ensure!(
202 items.iter().all(|c| c.name == kind),
203 "a calendar object holds one component type"
204 );
205 let uid = items[0]
206 .prop("UID")
207 .map(|u| u.value.clone())
208 .context("component without UID")?;
209 ensure!(
210 !uid.is_empty() && uid.len() <= 255,
211 "UID must be 1..255 characters"
212 );
213 ensure!(
214 items
215 .iter()
216 .all(|c| c.prop("UID").is_some_and(|u| u.value == uid)),
217 "all components share the UID"
218 );
219 Ok((kind, uid, cal))
220}
221
222pub fn check_vcard(text: &str) -> Result<(String, Component)> {
224 let card = parse(text, "VCARD")?;
225 ensure!(
226 card.prop("VERSION")
227 .is_some_and(|v| v.value == "3.0" || v.value == "4.0"),
228 "VCARD needs VERSION 3.0 or 4.0"
229 );
230 ensure!(card.prop("FN").is_some(), "VCARD needs FN");
231 let uid = card
232 .prop("UID")
233 .map(|u| u.value.clone())
234 .context("VCARD without UID")?;
235 ensure!(
236 !uid.is_empty() && uid.len() <= 255,
237 "UID must be 1..255 characters"
238 );
239 Ok((uid, card))
240}
241
242pub fn timestamp(value: &str) -> Result<i64> {
245 use chrono::{NaiveDate, NaiveDateTime, TimeZone, Utc};
246 let v = value.trim_end_matches('Z');
247 let t = if v.len() == 8 {
248 NaiveDate::parse_from_str(v, "%Y%m%d")?
249 .and_hms_opt(0, 0, 0)
250 .context("date")?
251 } else {
252 NaiveDateTime::parse_from_str(v, "%Y%m%dT%H%M%S")?
253 };
254 Ok(Utc.from_utc_datetime(&t).timestamp())
255}
256
257pub fn duration(value: &str) -> Result<i64> {
259 let (neg, v) = match value.strip_prefix('-') {
260 Some(r) => (true, r),
261 None => (false, value.strip_prefix('+').unwrap_or(value)),
262 };
263 let v = v.strip_prefix('P').context("duration starts with P")?;
264 let mut total = 0i64;
265 let mut num = String::new();
266 let mut time = false;
267 for c in v.chars() {
268 match c {
269 'T' => time = true,
270 '0'..='9' => num.push(c),
271 _ => {
272 let n: i64 = num.parse().context("duration number")?;
273 num.clear();
274 total += n * match (c, time) {
275 ('W', _) => 604_800,
276 ('D', _) => 86_400,
277 ('H', true) => 3600,
278 ('M', true) => 60,
279 ('S', true) => 1,
280 _ => bail!("invalid duration unit {c}"),
281 };
282 }
283 }
284 }
285 ensure!(num.is_empty(), "trailing number in duration");
286 Ok(if neg { -total } else { total })
287}
288
289pub fn overlaps(c: &Component, start: Option<i64>, end: Option<i64>) -> bool {
294 let dt = |n: &str| c.prop(n).and_then(|p| timestamp(&p.value).ok());
295 let begin = match c.name.as_str() {
296 "VTODO" => dt("DTSTART").or_else(|| dt("DUE")),
297 _ => dt("DTSTART"),
298 };
299 let Some(b) = begin else {
300 return true;
301 };
302 let recurring = c.prop("RRULE").is_some() || c.prop("RDATE").is_some();
303 let finish = if recurring {
304 i64::MAX
305 } else if let Some(e) = dt("DTEND").or_else(|| dt("DUE")) {
306 e
307 } else if let Some(d) = c.prop("DURATION").and_then(|p| duration(&p.value).ok()) {
308 b + d
309 } else if c.prop("DTSTART").is_some_and(|p| p.value.len() == 8) {
310 b + 86_400
311 } else {
312 b
313 };
314 let after_start = start.is_none_or(|s| finish > s || (finish == b && b >= s));
315 let before_end = end.is_none_or(|e| b < e);
316 after_start && before_end
317}
318
319pub fn text_match(value: &str, needle: &str, match_type: &str, negate: bool) -> bool {
322 let (v, n) = (value.to_lowercase(), needle.to_lowercase());
323 let hit = match match_type {
324 "equals" => v == n,
325 "starts-with" => v.starts_with(&n),
326 "ends-with" => v.ends_with(&n),
327 _ => v.contains(&n),
328 };
329 hit != negate
330}