use alloc::{
format,
string::{String, ToString},
vec::Vec,
};
use io_replica::placement::{ReplicaLinkId, ReplicaMeta, ReplicaSortKey};
use serde::{Deserialize, Serialize};
use crate::conventions::{PimdirDerivation, fnv1a64, time, unfold};
#[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)]
pub struct PimdirCalendarMeta {
pub v: u8,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub uid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub component: Option<String>,
#[serde(default)]
pub summary: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dtstart: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dtstart_tzid: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dtstart_value: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub dtend: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub due: Option<String>,
#[serde(default)]
pub recurring: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub until: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub size: Option<u64>,
}
pub fn derive(body: &[u8]) -> PimdirDerivation {
let components = parse(&unfold(body, false));
let zones: Vec<&Component> = components
.iter()
.flat_map(|component| component.children.iter())
.filter(|child| child.name == "VTIMEZONE")
.collect();
let master = master(&components);
let uid = master.and_then(|master| master.value("UID"));
let dtstart = master.and_then(|master| master.property("DTSTART"));
let due = master.and_then(|master| master.value("DUE"));
let component = master.map(|master| master.name.clone());
let rrule = master.and_then(|master| master.value("RRULE"));
let key = match (component.as_deref(), &due) {
(Some("VTODO"), Some(due)) => master
.and_then(|master| master.property("DUE"))
.and_then(|due_property| sort_key(due_property, &zones))
.or_else(|| sort_key_of(due, None, false, &zones)),
_ => dtstart.and_then(|dtstart| sort_key(dtstart, &zones)),
};
let link_id = match &uid {
Some(uid) => uid.clone(),
None => format!("hash:{}", fnv1a64(body)),
};
let meta = PimdirCalendarMeta {
v: 1,
uid,
component,
summary: master
.and_then(|master| master.value("SUMMARY"))
.unwrap_or_default(),
location: master.and_then(|master| master.value("LOCATION")),
dtstart: dtstart.map(|dtstart| dtstart.value.clone()),
dtstart_tzid: dtstart.and_then(|dtstart| dtstart.param("TZID")),
dtstart_value: dtstart.map(|dtstart| {
match dtstart.param("VALUE").as_deref() {
Some("DATE") => "date",
_ => "date-time",
}
.to_string()
}),
dtend: master.and_then(|master| master.value("DTEND")),
due,
recurring: rrule.is_some() || master.is_some_and(|master| master.value("RDATE").is_some()),
until: rrule.as_deref().and_then(until),
size: Some(body.len() as u64),
};
PimdirDerivation {
link_id: ReplicaLinkId(link_id),
meta: ReplicaMeta(serde_json::to_string(&meta).unwrap_or_default()),
sort_key: ReplicaSortKey(key.unwrap_or_default()),
}
}
fn master(components: &[Component]) -> Option<&Component> {
let candidates = components
.iter()
.flat_map(|component| component.children.iter())
.filter(|child| matches!(child.name.as_str(), "VEVENT" | "VTODO" | "VJOURNAL"));
let mut first = None;
for candidate in candidates {
if candidate.value("RECURRENCE-ID").is_none() {
return Some(candidate);
}
first.get_or_insert(candidate);
}
first
}
fn until(rrule: &str) -> Option<String> {
rrule.split(';').find_map(|part| {
let (name, value) = part.split_once('=')?;
name.eq_ignore_ascii_case("UNTIL")
.then(|| value.to_string())
})
}
fn sort_key(property: &Property, zones: &[&Component]) -> Option<String> {
sort_key_of(
&property.value,
property.param("TZID").as_deref(),
property.param("VALUE").as_deref() == Some("DATE"),
zones,
)
}
fn sort_key_of(
value: &str,
tzid: Option<&str>,
date_only: bool,
zones: &[&Component],
) -> Option<String> {
let bytes = value.as_bytes();
let year = time::digits(bytes, 0, 4)? as i32;
let month = time::digits(bytes, 4, 2)?;
let day = time::digits(bytes, 6, 2)?;
if date_only || bytes.len() == 8 {
return Some(time::rfc3339(time::unix(year, month, day, 0, 0, 0)));
}
if bytes.get(8) != Some(&b'T') {
return None;
}
let hour = time::digits(bytes, 9, 2)?;
let minute = time::digits(bytes, 11, 2)?;
let second = time::digits(bytes, 13, 2)?;
let local = time::unix(year, month, day, hour, minute, second);
if bytes.get(15) == Some(&b'Z') {
return Some(time::rfc3339(local));
}
let zone = tzid.and_then(|tzid| {
zones
.iter()
.find(|zone| zone.value("TZID").as_deref() == Some(tzid))
});
match zone {
Some(zone) => Some(time::rfc3339(resolve(zone, local, year))),
None => Some(time::rfc3339(local)),
}
}
fn resolve(zone: &Component, local: i64, year: i32) -> i64 {
let transitions = transitions(zone, year);
if transitions.is_empty() {
return local;
}
let mut valid: Option<i64> = None;
let mut earliest: Option<i64> = None;
for offset in offsets(&transitions) {
let instant = local - offset;
earliest = Some(earliest.map_or(instant, |current: i64| current.min(instant)));
if in_effect(&transitions, instant) == offset {
valid = Some(valid.map_or(instant, |current: i64| current.min(instant)));
}
}
valid.or(earliest).unwrap_or(local)
}
struct Transition {
at: i64,
to: i64,
from: i64,
}
fn transitions(zone: &Component, year: i32) -> Vec<Transition> {
let mut transitions = Vec::new();
for change in &zone.children {
if !matches!(change.name.as_str(), "STANDARD" | "DAYLIGHT") {
continue;
}
let (Some(from), Some(to)) = (
change.value("TZOFFSETFROM").as_deref().and_then(offset),
change.value("TZOFFSETTO").as_deref().and_then(offset),
) else {
continue;
};
let Some(start) = change.value("DTSTART") else {
continue;
};
let bytes = start.as_bytes();
let (Some(hour), Some(minute), Some(second)) = (
time::digits(bytes, 9, 2),
time::digits(bytes, 11, 2),
time::digits(bytes, 13, 2),
) else {
continue;
};
match change.value("RRULE").as_deref().and_then(yearly) {
Some((month, ordinal, weekday)) => {
for year in year - 1..=year + 1 {
let Some(day) = nth_weekday(year, month, ordinal, weekday) else {
continue;
};
transitions.push(Transition {
at: time::unix(year, month, day, hour, minute, second) - from,
to,
from,
});
}
}
None => {
let (Some(year), Some(month), Some(day)) = (
time::digits(bytes, 0, 4),
time::digits(bytes, 4, 2),
time::digits(bytes, 6, 2),
) else {
continue;
};
transitions.push(Transition {
at: time::unix(year as i32, month, day, hour, minute, second) - from,
to,
from,
});
}
}
}
transitions.sort_by_key(|transition| transition.at);
transitions
}
fn offsets(transitions: &[Transition]) -> Vec<i64> {
let mut offsets = Vec::new();
for offset in transitions
.first()
.map(|first| first.from)
.into_iter()
.chain(transitions.iter().map(|transition| transition.to))
{
if !offsets.contains(&offset) {
offsets.push(offset);
}
}
offsets
}
fn in_effect(transitions: &[Transition], instant: i64) -> i64 {
transitions
.iter()
.rfind(|transition| transition.at <= instant)
.map(|transition| transition.to)
.unwrap_or_else(|| transitions.first().map(|first| first.from).unwrap_or(0))
}
fn offset(raw: &str) -> Option<i64> {
let bytes = raw.trim().as_bytes();
if !matches!(bytes.first(), Some(b'+' | b'-')) {
return None;
}
let hours = time::digits(bytes, 1, 2)? as i64;
let minutes = time::digits(bytes, 3, 2)? as i64;
let seconds = time::digits(bytes, 5, 2).unwrap_or(0) as i64;
let offset = hours * 3_600 + minutes * 60 + seconds;
Some(if bytes[0] == b'-' { -offset } else { offset })
}
fn yearly(rrule: &str) -> Option<(u32, i32, u32)> {
let mut yearly = false;
let mut month = None;
let mut byday = None;
for part in rrule.split(';') {
let Some((name, value)) = part.split_once('=') else {
continue;
};
match name.to_uppercase().as_str() {
"FREQ" => yearly = value.eq_ignore_ascii_case("YEARLY"),
"BYMONTH" => month = value.parse::<u32>().ok(),
"BYDAY" => byday = Some(value.to_uppercase()),
_ => {}
}
}
if !yearly {
return None;
}
let byday = byday?;
let (ordinal, day) = byday.split_at(byday.len().checked_sub(2)?);
let weekday = ["SU", "MO", "TU", "WE", "TH", "FR", "SA"]
.iter()
.position(|name| *name == day)? as u32;
Some((month?, ordinal.parse().unwrap_or(1), weekday))
}
fn nth_weekday(year: i32, month: u32, ordinal: i32, weekday: u32) -> Option<u32> {
let length = time::days_in_month(year, month);
if length == 0 || ordinal == 0 {
return None;
}
let day = if ordinal > 0 {
let first = time::days_from_civil(year, month, 1);
let shift = (7 + weekday - time::weekday(first)) % 7;
1 + shift as i32 + (ordinal - 1) * 7
} else {
let last = time::days_from_civil(year, month, length);
let shift = (7 + time::weekday(last) - weekday) % 7;
length as i32 - shift as i32 + (ordinal + 1) * 7
};
(1..=length as i32).contains(&day).then_some(day as u32)
}
struct Component {
name: String,
properties: Vec<Property>,
children: Vec<Component>,
}
impl Component {
fn value(&self, name: &str) -> Option<String> {
self.property(name).map(|property| property.value.clone())
}
fn property(&self, name: &str) -> Option<&Property> {
self.properties
.iter()
.find(|property| property.name == name)
}
}
struct Property {
name: String,
params: Vec<(String, String)>,
value: String,
}
impl Property {
fn param(&self, name: &str) -> Option<String> {
self.params
.iter()
.find(|(param, _)| param == name)
.map(|(_, value)| value.clone())
}
}
fn parse(lines: &[String]) -> Vec<Component> {
let mut roots = Vec::new();
let mut stack: Vec<Component> = Vec::new();
for line in lines {
let Some((head, value)) = line.split_once(':') else {
continue;
};
let mut parts = head.split(';');
let name = parts.next().unwrap_or_default().trim().to_uppercase();
match name.as_str() {
"BEGIN" => stack.push(Component {
name: value.trim().to_uppercase(),
properties: Vec::new(),
children: Vec::new(),
}),
"END" => {
let Some(closed) = stack.pop() else { continue };
match stack.last_mut() {
Some(parent) => parent.children.push(closed),
None => roots.push(closed),
}
}
_ => {
let Some(component) = stack.last_mut() else {
continue;
};
component.properties.push(Property {
name,
params: parts
.filter_map(|param| param.split_once('='))
.map(|(param, value)| {
(param.trim().to_uppercase(), value.trim().to_string())
})
.collect(),
value: value.to_string(),
});
}
}
}
while let Some(unclosed) = stack.pop() {
match stack.last_mut() {
Some(parent) => parent.children.push(unclosed),
None => roots.push(unclosed),
}
}
roots
}