//! XML request parsing, response building, and PUT body validation. use pimdav::object::{self, CalendarObject, Invalid}; use pimdav::xml::{self, CALDAV, DAV, Name, Propfind}; use xmltree::Element; #[test] fn propfind_bodies() { assert_eq!(xml::propfind(b""), Ok(Propfind::AllProp(vec![]))); let body = br#" "#; assert_eq!( xml::propfind(body), Ok(Propfind::Prop(vec![ Name::new(DAV, "getetag"), Name::new("urn:x", "odd") ])) ); let body = br#""#; assert_eq!( xml::propfind(body), Ok(Propfind::AllProp(vec![Name::new(DAV, "getetag")])) ); let body = br#""#; assert_eq!(xml::propfind(body), Ok(Propfind::PropName)); assert!(xml::propfind(b"").is_err()); assert!(xml::propfind(b" Work#ff0000 "#; let u = xml::update(body).unwrap(); assert_eq!(u.set.len(), 2); assert_eq!(xml::text(&u.set[0]), "Work"); assert_eq!(u.remove, vec![Name::new(DAV, "displayname")]); let body = br#" "#; let u = xml::update(body).unwrap(); assert!(Name::of(&u.set[0]).is(CALDAV, "supported-calendar-component-set")); assert!(xml::update(b"").unwrap().set.is_empty()); assert!(xml::update(b"").is_err()); } #[test] fn multistatus_round_trips() { let mut r = xml::Response::new("/a b/"); r.push( 200, xml::with_text(xml::el(DAV, "displayname"), "x < y & \"z\""), ); r.push(404, Name::new("urn:x", "odd").element()); r.push( 200, xml::with_attr(xml::el(CALDAV, "comp"), "name", "VEVENT"), ); let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]); let root = Element::parse(out.as_bytes()).unwrap(); assert!(Name::of(&root).is(DAV, "multistatus")); let response = xml::child(&root, DAV, "response").unwrap(); assert_eq!( xml::text(xml::child(response, DAV, "href").unwrap()), "/a b/" ); let stats: Vec<_> = xml::elements(response) .filter(|e| e.name == "propstat") .collect(); assert_eq!(stats.len(), 2); let ok = xml::child(stats[0], DAV, "prop").unwrap(); assert_eq!( xml::text(xml::child(ok, DAV, "displayname").unwrap()), "x < y & \"z\"" ); assert_eq!( xml::child(ok, CALDAV, "comp").unwrap().attributes["name"], "VEVENT" ); let missing = xml::child(stats[1], DAV, "prop").unwrap(); assert!(xml::child(missing, "urn:x", "odd").is_some()); assert_eq!( xml::text(xml::child(stats[1], DAV, "status").unwrap()), "HTTP/1.1 404 Not Found" ); } #[test] fn deep_bodies_and_control_characters() { let deep = format!( "{}{}", "".repeat(20_000), "".repeat(20_000) ); assert!(xml::propfind(deep.as_bytes()).is_err()); assert!(xml::update(deep.replace("propfind", "propertyupdate").as_bytes()).is_err()); let mut r = xml::Response::new("/a/"); r.push(200, xml::with_text(xml::el(DAV, "displayname"), "a\u{1}b")); let out = xml::multistatus(&Name::new(DAV, "multistatus"), &[r]); let root = Element::parse(out.as_bytes()).unwrap(); let response = xml::child(&root, DAV, "response").unwrap(); let prop = xml::child(xml::child(response, DAV, "propstat").unwrap(), DAV, "prop").unwrap(); assert_eq!( xml::text(xml::child(prop, DAV, "displayname").unwrap()), "a\u{fffd}b" ); } fn ics(body: &str) -> Vec { format!("BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//t//EN\r\n{body}END:VCALENDAR\r\n") .into_bytes() } const EVENT: &str = "BEGIN:VEVENT\r\nUID:a\r\nDTSTAMP:20260101T000000Z\r\nDTSTART:20260101T100000Z\r\nEND:VEVENT\r\n"; #[test] fn calendar_objects() { let all = ["VEVENT", "VTODO"]; assert_eq!( object::calendar(&ics(EVENT), &all), Ok(CalendarObject { uid: "a".into(), component: "VEVENT" }) ); let override_ = EVENT.replace("DTSTART", "RECURRENCE-ID:20260102T100000Z\r\nDTSTART"); assert!(object::calendar(&ics(&format!("{EVENT}{override_}")), &all).is_ok()); let todo = "BEGIN:VTODO\r\nUID:t\r\nDTSTAMP:20260101T000000Z\r\nEND:VTODO\r\n"; assert!(object::calendar(&ics(todo), &all).is_ok()); let cases = [ ( ics(&format!("METHOD:REQUEST\r\n{EVENT}")), Invalid::CalendarResource, ), ( ics(&format!("{EVENT}{}", EVENT.replace("UID:a", "UID:b"))), Invalid::CalendarResource, ), ( ics(&format!("{EVENT}{}", EVENT.replace("VEVENT", "VTODO"))), Invalid::CalendarResource, ), ( ics(&EVENT.replace("UID:a\r\n", "")), Invalid::CalendarResource, ), (ics(""), Invalid::CalendarResource), ( ics(&EVENT.replace("VEVENT", "VJOURNAL")), Invalid::CalendarComponent, ), ( ics(&EVENT.replace("DTSTART:20260101T100000Z\r\n", "")), Invalid::CalendarData, ), ( ics("BEGIN:VTODO\r\nUID:t\r\nDURATION:PT1H\r\nEND:VTODO\r\n"), Invalid::CalendarData, ), (b"not a calendar".to_vec(), Invalid::CalendarData), (vec![0xff, 0xfe], Invalid::CalendarData), ([ics(EVENT), ics(EVENT)].concat(), Invalid::CalendarResource), ( ics(&EVENT.replace( "END:VEVENT", &format!( "{}{}END:VEVENT", "BEGIN:VALARM\r\n".repeat(5), "END:VALARM\r\n".repeat(5) ), )), Invalid::CalendarData, ), ]; for (body, want) in cases { assert_eq!( object::calendar(&body, &all), Err(want), "{}", String::from_utf8_lossy(&body) ); } } #[test] fn vcards() { let card = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c1\r\nFN:Bob\r\nEND:VCARD\r\n"; assert_eq!(object::vcard(card.as_bytes()), Ok(Some("c1".into()))); let no_uid = card.replace("UID:c1\r\n", ""); assert_eq!(object::vcard(no_uid.as_bytes()), Ok(None)); assert_eq!(object::vcard(&ics(EVENT)), Err(Invalid::AddressData)); assert_eq!( object::vcard(format!("{card}{card}").as_bytes()), Err(Invalid::AddressData) ); let cut = card.replace("END:VCARD\r\n", ""); assert_eq!(object::vcard(cut.as_bytes()), Err(Invalid::AddressData)); } #[test] fn missing_dtstamp_is_inserted() { let now = chrono::DateTime::from_timestamp(1_790_000_000, 0).unwrap(); let sent = "BEGIN:VCALENDAR\r\nBEGIN:VEVENT\r\nUID:a\r\nDESCRIPTION:long\r\n line\r\n\ BEGIN:VALARM\r\nDTSTAMP:20260101T000000Z\r\nEND:VALARM\r\nEND:VEVENT\r\n\ BEGIN:VTODO\r\nDTSTAMP:20260101T000000Z\r\nUID:b\r\nEND:VTODO\r\nEND:VCALENDAR\r\n"; let got = object::with_dtstamp(sent.as_bytes(), now).unwrap(); // The VALARM's DTSTAMP does not count for the VEVENT around it. let want = sent.replacen( "BEGIN:VEVENT\r\n", "BEGIN:VEVENT\r\nDTSTAMP:20260921T141320Z\r\n", 1, ); assert_eq!(String::from_utf8(got).unwrap(), want); assert_eq!(object::with_dtstamp(want.as_bytes(), now), None); let lf = "BEGIN:VCALENDAR\nBEGIN:VJOURNAL\nUID:c\nEND:VJOURNAL\nEND:VCALENDAR\n"; let got = object::with_dtstamp(lf.as_bytes(), now).unwrap(); assert_eq!( String::from_utf8(got).unwrap(), lf.replace("VJOURNAL\nUID", "VJOURNAL\nDTSTAMP:20260921T141320Z\nUID") ); } #[test] fn too_many_rules_are_refused() { let rules = |n: usize| "RRULE:FREQ=DAILY\r\n".repeat(n); let event = |n| EVENT.replace("END:VEVENT", &format!("{}END:VEVENT", rules(n))); assert!(object::calendar(&ics(&event(4)), &["VEVENT"]).is_ok()); assert_eq!( object::calendar(&ics(&event(5)), &["VEVENT"]), Err(Invalid::CalendarResource) ); let zone = |n| { format!( "BEGIN:VTIMEZONE\r\nTZID:X\r\nBEGIN:STANDARD\r\nDTSTART:19701025T030000\r\nTZOFFSETFROM:+0200\r\nTZOFFSETTO:+0100\r\n{}END:STANDARD\r\nEND:VTIMEZONE\r\n", rules(n) ) }; assert!(object::calendar(&ics(&format!("{}{EVENT}", zone(50))), &["VEVENT"]).is_ok()); assert_eq!( object::calendar(&ics(&format!("{}{EVENT}", zone(51))), &["VEVENT"]), Err(Invalid::CalendarResource) ); } #[test] fn a_rule_with_a_huge_count_is_refused() { let with = |rule: &str| { let event = EVENT.replace("END:VEVENT", &format!("{rule}\r\nEND:VEVENT")); object::calendar(&ics(&event), &["VEVENT"]) }; assert!(with("RRULE:FREQ=DAILY;COUNT=100000").is_ok()); assert_eq!( with("RRULE:FREQ=DAILY;COUNT=100001"), Err(Invalid::CalendarResource) ); assert!(with("RRULE:FREQ=MINUTELY;COUNT=10000").is_ok()); assert_eq!( with("EXRULE:FREQ=SECONDLY;COUNT=10001"), Err(Invalid::CalendarResource) ); } #[test] fn one_master_per_object_and_no_blank_uid() { let master2 = EVENT.replace("T100000Z\r\nEND", "T110000Z\r\nEND"); assert_eq!( object::calendar(&ics(&format!("{EVENT}{master2}")), &["VEVENT"]), Err(Invalid::CalendarResource) ); let blank = EVENT.replace("UID:a", "UID: "); assert_eq!( object::calendar(&ics(&blank), &["VEVENT"]), Err(Invalid::CalendarResource) ); let card = b"BEGIN:VCARD\r\nVERSION:3.0\r\nFN:A\r\nUID: \r\nEND:VCARD\r\n"; assert_eq!(object::vcard(card), Ok(None)); } #[test] fn a_cut_or_nested_object_is_refused() { let full = ics(EVENT); let cut = &full[..full.len() - "END:VCALENDAR\r\n".len()]; assert_eq!( object::calendar(cut, &["VEVENT"]), Err(Invalid::CalendarData) ); let inner = EVENT.replace("UID:a", "UID:b"); let nested = EVENT.replace("END:VEVENT", &format!("{inner}END:VEVENT")); assert_eq!( object::calendar(&ics(&nested), &["VEVENT"]), Err(Invalid::CalendarResource) ); }