//! Photos and birthday events derived from contacts. use chrono::{DateTime, Utc}; use pimdav::calcard::icalendar::ICalendar; use pimdav::contact; use pimdav::expand::expand; use pimdav::zone::Zone; fn card(lines: &str) -> String { format!( "BEGIN:VCARD\r\nVERSION:3.0\r\nUID:c1\r\nFN:Anna Berg\r\n{}END:VCARD\r\n", lines.replace('\n', "\r\n") ) } fn utc(s: &str) -> DateTime { format!("{s}T00:00:00Z").parse().unwrap() } /// The start dates of an event's instances in a range. fn days(ics: &str, from: &str, to: &str) -> Vec { let cal = ICalendar::parse(ics).unwrap(); expand(&cal, utc(from)..utc(to), Zone::Utc) .instances .iter() .map(|i| i.start.format("%Y-%m-%d").to_string()) .collect() } #[test] fn photo_forms() { let v3 = card("PHOTO;ENCODING=b;TYPE=JPEG:/9j/4AAQ\n"); assert_eq!( contact::photo(&v3), Some(vec![0xff, 0xd8, 0xff, 0xe0, 0x00, 0x10]) ); let v4 = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c\r\nFN:X\r\nPHOTO:data:image/png;base64,iVBORw0K\r\nEND:VCARD\r\n"; assert_eq!(contact::photo(v4).unwrap()[..4], [0x89, b'P', b'N', b'G']); assert_eq!( contact::photo(&card("PHOTO;VALUE=uri:http://example.com/a.jpg\n")), None ); assert_eq!(contact::photo(&card("")), None); } #[test] fn birthday_forms() { let full = contact::dates(&card("BDAY:1980-03-15\n"), "k"); assert_eq!(full.len(), 1); let (uid, ics) = &full[0]; assert!(ics.contains("SUMMARY:๐ŸŽ‚ Anna Berg (1980)\r\n")); assert!(ics.contains("TRANSP:TRANSPARENT\r\n")); assert_eq!( days(ics, "2025-01-01", "2027-01-01"), ["2025-03-15", "2026-03-15"] ); // Stable per key and kind, different for another key. assert_eq!(contact::dates(&card("BDAY:19800315\n"), "k")[0].0, *uid); assert_ne!(contact::dates(&card("BDAY:19800315\n"), "other")[0].0, *uid); for yearless in [ "BDAY:--0315\n", "BDAY:--03-15\n", "BDAY;X-APPLE-OMIT-YEAR=1604:1604-03-15\n", ] { let (_, ics) = &contact::dates(&card(yearless), "k")[0]; assert!(ics.contains("SUMMARY:๐ŸŽ‚ Anna Berg\r\n"), "{yearless}"); assert_eq!( days(ics, "2025-01-01", "2026-01-01"), ["2025-03-15"], "{yearless}" ); } assert!(contact::dates(&card("BDAY;VALUE=text:circa 1800\n"), "k").is_empty()); assert!(contact::dates(&card("BDAY:1981-02-29\n"), "k").is_empty()); } #[test] fn leap_day_birthday() { let (_, ics) = &contact::dates(&card("BDAY:1980-02-29\n"), "k")[0]; assert_eq!( days(ics, "2027-01-01", "2029-01-01"), ["2027-02-28", "2028-02-29"] ); } #[test] fn anniversaries() { let v4 = "BEGIN:VCARD\r\nVERSION:4.0\r\nUID:c\r\nFN:Anna\r\nBDAY:--0315\r\nANNIVERSARY:20010601\r\nEND:VCARD\r\n"; let both = contact::dates(v4, "k"); assert_eq!(both.len(), 2); assert!(both[1].1.contains("SUMMARY:๐Ÿ’ Anna (2001)\r\n")); assert_ne!(both[0].0, both[1].0); let evolution = contact::dates(&card("X-ANNIVERSARY:2001-06-01\n"), "k"); assert!(evolution[0].1.contains("๐Ÿ’")); let apple = card( "item1.X-ABDATE:2001-06-01\nitem1.X-ABLabel:_$!!$_\nitem2.X-ABDATE:2010-01-01\nitem2.X-ABLabel:Other\n", ); let dates = contact::dates(&apple, "k"); assert_eq!(dates.len(), 1); assert!(dates[0].1.contains("๐Ÿ’ Anna Berg (2001)")); } #[test] fn non_ascii_dates_are_ignored() { // Byte offsets on these would split a character. for bday in ["BDAY:1980ๅนด3\n", "BDAY:--0ๅนด\n", "BDAY:รฉ1234567\n"] { assert!(contact::dates(&card(bday), "k").is_empty(), "{bday}"); assert_eq!(pimdav::view::card(&card(bday)).birthday, None, "{bday}"); } // A line without a value that ends in a multi-byte character. let odd = card("X-NOTE;รฉ\nBDAY:1980-03-15\n"); let v4 = pimdav::render::AddressData { props: None, version: Some(pimdav::calcard::vcard::VCardVersion::V4_0), }; assert!(pimdav::render::address_data(&odd, &v4).contains("BEGIN:VCARD")); } #[test] fn many_apple_dates_stay_linear() { let mut lines = String::new(); for n in 0..20_000 { lines.push_str(&format!("g{n}.X-ABDATE:2000-01-01\n")); } lines.push_str("g7.X-ABLABEL:_$!!$_\n"); let started = std::time::Instant::now(); let dates = contact::dates(&card(&lines), "k"); assert!( started.elapsed().as_secs_f64() < 2.0, "{:?}", started.elapsed() ); assert_eq!(dates.len(), 1); }