mirror of
https://github.com/lennart-k/rustical.git
synced 2025-12-13 14:42:30 +00:00
Support timezone ids from the Olson database
This commit is contained in:
76
Cargo.lock
generated
76
Cargo.lock
generated
@@ -723,6 +723,27 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd6dd8046d00723a59a2f8c5f295c515b9bb9a331ee4f8f3d4dd49e428acd3b6"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"chrono-tz-build",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "chrono-tz-build"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e94fea34d77a245229e7746bd2beb786cd2a896f306ff491fb8cecb3074b10a7"
|
||||
dependencies = [
|
||||
"parse-zoneinfo",
|
||||
"phf_codegen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cipher"
|
||||
version = "0.4.4"
|
||||
@@ -1971,6 +1992,15 @@ dependencies = [
|
||||
"windows-targets 0.52.6",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parse-zoneinfo"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
|
||||
dependencies = [
|
||||
"regex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "password-auth"
|
||||
version = "1.0.0"
|
||||
@@ -2028,6 +2058,44 @@ version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ade2d8b8f33c7333b51bcf0428d37e217e9f32192ae4772156f65063b8ce03dc"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_codegen"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8d39688d359e6b34654d328e262234662d16cc0f60ec8dcbe5e718709342a5a"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "48e4cc64c2ad9ebe670cb8fd69dd50ae301650392e81c05f9bfcb2d5bdbc24b0"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "90fcb95eef784c2ac79119d1dd819e162b5da872ce6f3c3abe1e8ca1c082f72b"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.5"
|
||||
@@ -2387,6 +2455,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"derive_more 1.0.0",
|
||||
"futures-util",
|
||||
"quick-xml",
|
||||
@@ -2469,6 +2538,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"ical",
|
||||
"lazy_static",
|
||||
"password-auth",
|
||||
@@ -2632,6 +2702,12 @@ dependencies = [
|
||||
"rand_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.9"
|
||||
|
||||
@@ -75,6 +75,7 @@ rustical_store = { path = "./crates/store/" }
|
||||
rustical_caldav = { path = "./crates/caldav/" }
|
||||
rustical_carddav = { path = "./crates/carddav/" }
|
||||
rustical_frontend = { path = "./crates/frontend/" }
|
||||
chrono-tz = "0.10.0"
|
||||
|
||||
[dependencies]
|
||||
rustical_store = { workspace = true }
|
||||
|
||||
@@ -25,3 +25,4 @@ roxmltree = { workspace = true }
|
||||
url = { workspace = true }
|
||||
rustical_dav = { workspace = true }
|
||||
rustical_store = { workspace = true }
|
||||
chrono = { workspace = true }
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
use actix_web::HttpRequest;
|
||||
use chrono::{DateTime, Utc};
|
||||
use rustical_dav::{
|
||||
methods::propfind::{PropElement, PropfindType},
|
||||
resource::Resource,
|
||||
xml::{multistatus::PropstatWrapper, MultistatusElement},
|
||||
};
|
||||
use rustical_store::{model::object::CalendarObject, timestamp::CalDateTime, CalendarStore};
|
||||
use rustical_store::{model::object::CalendarObject, CalendarStore};
|
||||
use serde::Deserialize;
|
||||
use tokio::sync::RwLock;
|
||||
|
||||
@@ -19,10 +20,18 @@ use crate::{
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
#[allow(dead_code)]
|
||||
struct TimeRangeElement {
|
||||
#[serde(rename = "@start")]
|
||||
start: Option<CalDateTime>,
|
||||
#[serde(rename = "@end")]
|
||||
end: Option<CalDateTime>,
|
||||
#[serde(
|
||||
rename = "@start",
|
||||
deserialize_with = "rustical_store::timestamp::deserialize_utc_datetime",
|
||||
default
|
||||
)]
|
||||
start: Option<DateTime<Utc>>,
|
||||
#[serde(
|
||||
rename = "@end",
|
||||
deserialize_with = "rustical_store::timestamp::deserialize_utc_datetime",
|
||||
default
|
||||
)]
|
||||
end: Option<DateTime<Utc>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
@@ -138,31 +147,18 @@ impl CompFilterElement {
|
||||
|
||||
if let Some(time_range) = &self.time_range {
|
||||
if let Some(start) = &time_range.start {
|
||||
if let CalDateTime::Utc(range_start) = &start {
|
||||
if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None)
|
||||
{
|
||||
if range_start > &first_occurence.utc() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// RFC 4791: 'Both attributes MUST be specified as "date with UTC time" value.'
|
||||
// TODO: Return Bad Request instead?
|
||||
return false;
|
||||
}
|
||||
if let Some(first_occurence) = cal_object.get_first_occurence().unwrap_or(None) {
|
||||
if start > &first_occurence.utc() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
if let Some(end) = &time_range.end {
|
||||
if let CalDateTime::Utc(range_end) = &end {
|
||||
if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) {
|
||||
if range_end < &last_occurence.utc() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// RFC 4791: 'Both attributes MUST be specified as "date with UTC time" value.'
|
||||
// TODO: Return Bad Request instead?
|
||||
return false;
|
||||
}
|
||||
if let Some(last_occurence) = cal_object.get_last_occurence().unwrap_or(None) {
|
||||
if end < &last_occurence.utc() {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,3 +27,4 @@ actix-web-httpauth = { workspace = true }
|
||||
tracing = { workspace = true }
|
||||
pbkdf2 = { workspace = true }
|
||||
rand_core = { workspace = true }
|
||||
chrono-tz = { workspace = true }
|
||||
|
||||
@@ -1,31 +1,30 @@
|
||||
use std::str::FromStr;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::Duration;
|
||||
use ical::{generator::IcalEvent, parser::Component, property::Property};
|
||||
|
||||
use crate::{
|
||||
timestamp::{parse_duration, CalDateTime},
|
||||
Error,
|
||||
};
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::Duration;
|
||||
use ical::{
|
||||
generator::IcalEvent,
|
||||
parser::{ical::component::IcalTimeZone, Component},
|
||||
property::Property,
|
||||
};
|
||||
use std::collections::HashMap;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EventObject {
|
||||
pub(crate) event: IcalEvent,
|
||||
pub(crate) timezones: HashMap<String, IcalTimeZone>,
|
||||
}
|
||||
|
||||
impl EventObject {
|
||||
pub fn get_first_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
// This is safe since we enforce the event's existance in the constructor
|
||||
let dtstart = if let Some(Property {
|
||||
value: Some(value), ..
|
||||
}) = self.event.get_property("DTSTART")
|
||||
{
|
||||
value
|
||||
if let Some(dtstart) = self.event.get_property("DTSTART") {
|
||||
CalDateTime::parse_prop(dtstart, &self.timezones)
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
Ok(Some(CalDateTime::from_str(dtstart)?))
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_last_occurence(&self) -> Result<Option<CalDateTime>, Error> {
|
||||
@@ -35,11 +34,8 @@ impl EventObject {
|
||||
return Err(anyhow!("event is recurring, we cannot handle that yet").into());
|
||||
}
|
||||
|
||||
if let Some(Property {
|
||||
value: Some(dtend), ..
|
||||
}) = self.event.get_property("DTEND")
|
||||
{
|
||||
return Ok(Some(CalDateTime::from_str(dtend)?));
|
||||
if let Some(dtend) = self.event.get_property("DTEND") {
|
||||
return CalDateTime::parse_prop(dtend, &self.timezones);
|
||||
};
|
||||
|
||||
let duration = if let Some(Property {
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use super::{event::EventObject, todo::TodoObject};
|
||||
use crate::{timestamp::CalDateTime, Error};
|
||||
use anyhow::Result;
|
||||
use ical::parser::{ical::component::IcalTimeZone, Component};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sha2::{Digest, Sha256};
|
||||
use std::io::BufReader;
|
||||
use std::{collections::HashMap, io::BufReader};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
// specified in https://datatracker.ietf.org/doc/html/rfc5545#section-3.6
|
||||
@@ -84,12 +85,24 @@ impl CalendarObject {
|
||||
));
|
||||
}
|
||||
|
||||
let timezones: HashMap<String, IcalTimeZone> = cal
|
||||
.timezones
|
||||
.clone()
|
||||
.into_iter()
|
||||
.filter_map(|timezone| {
|
||||
let timezone_prop = timezone.get_property("TZID")?.to_owned();
|
||||
let tzid = timezone_prop.value?;
|
||||
Some((tzid, timezone))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if let Some(event) = cal.events.first() {
|
||||
return Ok(CalendarObject {
|
||||
uid,
|
||||
ics,
|
||||
data: CalendarObjectComponent::Event(EventObject {
|
||||
event: event.clone(),
|
||||
timezones,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
use std::{ops::Add, str::FromStr};
|
||||
|
||||
use crate::Error;
|
||||
use anyhow::{anyhow, Result};
|
||||
use chrono::{DateTime, Duration, NaiveDate, NaiveDateTime, NaiveTime, Utc};
|
||||
use chrono_tz::Tz;
|
||||
use ical::{
|
||||
parser::{ical::component::IcalTimeZone, Component},
|
||||
property::Property,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Deserializer};
|
||||
use std::{collections::HashMap, ops::Add};
|
||||
|
||||
lazy_static! {
|
||||
static ref RE_DURATION: regex::Regex = regex::Regex::new(r"^(?<sign>[+-])?P((?P<W>\d+)W)?((?P<D>\d+)D)?(T((?P<H>\d+)H)?((?P<M>\d+)M)?((?P<S>\d+)S)?)?$").unwrap();
|
||||
@@ -21,19 +25,27 @@ pub enum CalDateTime {
|
||||
// Form 2, example: 19980119T070000Z
|
||||
Utc(DateTime<Utc>),
|
||||
// Form 3, example: TZID=America/New_York:19980119T020000
|
||||
// TODO: implement timezone parsing
|
||||
ExplicitTZ((String, NaiveDateTime)),
|
||||
// https://en.wikipedia.org/wiki/Tz_database
|
||||
OlsonTZ(DateTime<Tz>),
|
||||
Date(NaiveDate),
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for CalDateTime {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let input = String::deserialize(deserializer)?;
|
||||
Self::from_str(&input).map_err(|_| serde::de::Error::custom("Invalid datetime format"))
|
||||
}
|
||||
pub fn deserialize_utc_datetime<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
type Inner = Option<String>;
|
||||
Ok(if let Some(input) = Inner::deserialize(deserializer)? {
|
||||
dbg!(&input);
|
||||
Some(
|
||||
NaiveDateTime::parse_from_str(&input, UTC_DATE_TIME)
|
||||
.map_err(|err| serde::de::Error::custom(err.to_string()))?
|
||||
.and_utc(),
|
||||
)
|
||||
} else {
|
||||
dbg!("NONE");
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
impl Add<Duration> for CalDateTime {
|
||||
@@ -43,36 +55,95 @@ impl Add<Duration> for CalDateTime {
|
||||
match self {
|
||||
Self::Local(datetime) => Self::Local(datetime + duration),
|
||||
Self::Utc(datetime) => Self::Utc(datetime + duration),
|
||||
Self::ExplicitTZ((tz, datetime)) => Self::ExplicitTZ((tz, datetime + duration)),
|
||||
Self::OlsonTZ(datetime) => Self::OlsonTZ(datetime + duration),
|
||||
Self::Date(date) => Self::Local(date.and_time(NaiveTime::default()) + duration),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CalDateTime {
|
||||
type Err = Error;
|
||||
impl CalDateTime {
|
||||
pub fn parse_prop(
|
||||
prop: &Property,
|
||||
timezones: &HashMap<String, IcalTimeZone>,
|
||||
) -> Result<Option<Self>, Error> {
|
||||
let prop_value = if let Some(value) = &prop.value {
|
||||
value.to_owned()
|
||||
} else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
fn from_str(value: &str) -> Result<Self, Self::Err> {
|
||||
let timezone = if let Some(tzid) = &prop
|
||||
.params
|
||||
.clone()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter(|(name, _values)| name == "TZID")
|
||||
.map(|(_name, values)| values.first())
|
||||
.next()
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Some(timezone) = timezones.get(tzid.to_owned()) {
|
||||
// X-LIC-LOCATION is often used to refer to a standardised timezone from the Olson
|
||||
// database
|
||||
if let Some(olson_name) = timezone
|
||||
.get_property("X-LIC-LOCATION")
|
||||
.map(|prop| prop.value.to_owned())
|
||||
.unwrap_or_default()
|
||||
{
|
||||
if let Ok(tz) = olson_name.parse::<Tz>() {
|
||||
Some(tz)
|
||||
} else {
|
||||
// TODO: handle invalid timezone name
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// No name, we would have to parse it ourselves :(
|
||||
// TODO: implement
|
||||
None
|
||||
}
|
||||
} else {
|
||||
// ERROR: invalid timezone specified
|
||||
// For now just assume naive datetime?
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Self::parse(&prop_value, timezone).map(Some)
|
||||
}
|
||||
|
||||
pub fn parse(value: &str, timezone: Option<Tz>) -> Result<Self, Error> {
|
||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, LOCAL_DATE_TIME) {
|
||||
if let Some(timezone) = timezone {
|
||||
let result = datetime.and_local_timezone(timezone);
|
||||
if let Some(datetime) = result.earliest() {
|
||||
return Ok(CalDateTime::OlsonTZ(datetime));
|
||||
} else {
|
||||
// This time does not exist because there's a gap in local time
|
||||
return Err(Error::Other(anyhow!(
|
||||
"Timestamp doesn't exist because of gap in local time"
|
||||
)));
|
||||
}
|
||||
}
|
||||
return Ok(CalDateTime::Local(datetime));
|
||||
}
|
||||
|
||||
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
|
||||
return Ok(CalDateTime::Utc(datetime.and_utc()));
|
||||
}
|
||||
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
|
||||
return Ok(CalDateTime::Date(date));
|
||||
}
|
||||
|
||||
Err(Error::Other(anyhow!("Invalid datetime format")))
|
||||
}
|
||||
}
|
||||
|
||||
impl CalDateTime {
|
||||
pub fn utc(&self) -> DateTime<Utc> {
|
||||
match &self {
|
||||
CalDateTime::Local(local_datetime) => local_datetime.and_utc(),
|
||||
CalDateTime::Utc(utc_datetime) => utc_datetime.to_owned(),
|
||||
// TODO: respect timezone!
|
||||
CalDateTime::ExplicitTZ((_tzid, datetime)) => datetime.and_utc(),
|
||||
CalDateTime::OlsonTZ(datetime) => datetime.to_utc(),
|
||||
CalDateTime::Date(date) => date.and_time(NaiveTime::default()).and_utc(),
|
||||
}
|
||||
}
|
||||
@@ -84,13 +155,6 @@ impl From<CalDateTime> for DateTime<Utc> {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_cal_datetime() {
|
||||
CalDateTime::from_str("19980118T230000").unwrap();
|
||||
CalDateTime::from_str("19980118T230000Z").unwrap();
|
||||
CalDateTime::from_str("19980118").unwrap();
|
||||
}
|
||||
|
||||
pub fn parse_duration(string: &str) -> Result<Duration, Error> {
|
||||
let captures = RE_DURATION
|
||||
.captures(string)
|
||||
|
||||
Reference in New Issue
Block a user