fix(calendar): the ICS address parser survives a multibyte prefix
bare_address sliced trimmed[..7] after a len() >= 7 check — byte count against a byte slice is fine, but the guard and the slice disagree once the text is lossy-decoded elsewhere; an ATTENDEE line whose value opens with a multibyte run panicked the reminder source on a feed the operator does not control. The slice is get(..7) now, and a test feeds multibyte prefixes through both the ATTENDEE and ORGANIZER paths.
This commit is contained in:
1 parent
00adf092ff
commit
dad99c7664
1 file changed
+40
-5
@@ -540,12 +540,17 @@ fn parse_event<'a>(block: &[ContentLine], provider: &'a str) -> Result<Parsed<'a
|
||||
|
||||
let text = |name: &str| first(name).map(|line| unescape(&line.value));
|
||||
// `mailto:` is the scheme ICS addresses ride; strip it wherever it
|
||||
// appears, case-insensitively.
|
||||
let bare_address = |value: &str| match value.trim() {
|
||||
trimmed if trimmed.len() >= 7 && trimmed[..7].eq_ignore_ascii_case("mailto:") => {
|
||||
trimmed[7..].trim().to_owned()
|
||||
// appears, case-insensitively. `get` over indexing: the prefix can
|
||||
// end inside a multi-byte character, and a slice there would panic
|
||||
// on a remote feed's value.
|
||||
let bare_address = |value: &str| {
|
||||
let trimmed = value.trim();
|
||||
match trimmed.get(..7) {
|
||||
Some(prefix) if prefix.eq_ignore_ascii_case("mailto:") => {
|
||||
trimmed[7..].trim().to_owned()
|
||||
}
|
||||
_ => trimmed.to_owned(),
|
||||
}
|
||||
trimmed => trimmed.to_owned(),
|
||||
};
|
||||
|
||||
let mut raw = Map::new();
|
||||
@@ -984,6 +989,36 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_multibyte_address_prefix_parses_without_panicking() {
|
||||
// The first seven bytes of `üüüüüüüü:x` end inside a
|
||||
// character, so a plain seven-byte slice would panic on a
|
||||
// remote feed's deterministic value.
|
||||
let raw = "BEGIN:VEVENT\nUID:a@x\nDTSTART:20261020T140000Z\n\
|
||||
ORGANIZER:üüüüüüüü:x\n\
|
||||
ATTENDEE:üüüüüüüü:x\n\
|
||||
ATTENDEE:üüüx:alice@example.net\n\
|
||||
ATTENDEE:mailto:alice@example.net\n\
|
||||
ATTENDEE:MAILTO:Bob@Example.net\n\
|
||||
END:VEVENT\n";
|
||||
let events = expand(raw, &window("2026-10-20T00:00:00Z", 24)).expect("expands");
|
||||
assert_eq!(
|
||||
events[0].organizer.as_deref(),
|
||||
Some("üüüüüüüü:x"),
|
||||
"no mailto: prefix reads verbatim"
|
||||
);
|
||||
assert_eq!(
|
||||
events[0].attendees,
|
||||
vec![
|
||||
"üüüüüüüü:x".to_owned(),
|
||||
"üüüx:alice@example.net".to_owned(),
|
||||
"alice@example.net".to_owned(),
|
||||
"Bob@Example.net".to_owned(),
|
||||
],
|
||||
"the mailto: strip is case-insensitive; anything else is kept whole"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_weekly_byday_rule_expands_across_the_window() {
|
||||
let raw = "BEGIN:VEVENT\nUID:standup@x\nDTSTART:20261005T090000Z\nDTEND:20261005T093000Z\n\
|
||||
|
||||
Reference in new issue
Block a user