Compare commits

..

2 Commits

Author SHA1 Message Date
Lennart
1b6214d426 MKCALENDAR: Handling of invalid timezones 2026-01-19 16:36:25 +01:00
Lennart
be34cc3091 xml: Implement namespace for Unparsed 2026-01-19 16:22:21 +01:00
6 changed files with 52 additions and 23 deletions

View File

@@ -1,10 +1,14 @@
use std::str::FromStr;
use crate::Error;
use crate::calendar::CalendarResourceService;
use crate::calendar::prop::SupportedCalendarComponentSet;
use crate::error::Precondition;
use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode};
use ical::IcalParser;
use ical::types::get_proprietary_tzid;
use rustical_dav::xml::HrefElement;
use rustical_ical::CalendarObjectType;
use rustical_store::auth::Principal;
@@ -84,21 +88,33 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
}
let timezone_id = if let Some(tzid) = request.calendar_timezone_id {
if chrono_tz::Tz::from_str(&tzid).is_err() {
return Err(Error::PreconditionFailed(Precondition::CalendarTimezone(
"Invalid timezone ID in calendar-timezone-id",
)));
}
Some(tzid)
} else if let Some(tz) = request.calendar_timezone {
// TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::from_slice(tz.as_bytes())
.next()
.ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?
.map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?;
.ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
"No timezone data provided",
)))?
.map_err(|_| {
Error::PreconditionFailed(Precondition::CalendarTimezone("Error parsing timezone"))
})?;
let timezone = calendar.vtimezones.values().next().ok_or_else(|| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?;
let timezone = calendar
.vtimezones
.values()
.next()
.ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
"No timezone data provided",
)))?;
let timezone: Option<chrono_tz::Tz> = timezone.into();
let timezone = timezone.ok_or_else(|| {
rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned())
})?;
let timezone = timezone.ok_or(Error::PreconditionFailed(
Precondition::CalendarTimezone("No timezone data provided"),
))?;
Some(timezone.name().to_owned())
} else {

View File

@@ -12,6 +12,9 @@ pub enum Precondition {
#[error("valid-calendar-data")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
ValidCalendarData,
#[error("calendar-timezone")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarTimezone(&'static str),
}
impl IntoResponse for Precondition {

View File

@@ -71,6 +71,7 @@ pub async fn axum_route_proppatch<R: ResourceService>(
route_proppatch(&path, uri.path(), &body, &principal, &resource_service).await
}
#[allow(clippy::too_many_lines)]
pub async fn route_proppatch<R: ResourceService>(
path_components: &R::PathComponents,
path: &str,
@@ -116,12 +117,14 @@ pub async fn route_proppatch<R: ResourceService>(
}
}
SetPropertyPropWrapper::Invalid(invalid) => {
let propname = invalid.tag_name();
let Unparsed(propns, propname) = invalid;
if let Some(full_propname) = <R::Resource as Resource>::list_props()
.into_iter()
.find_map(|(ns, tag)| {
if tag == propname.as_str() {
if (ns, tag)
== (propns.as_ref().map(NamespaceOwned::as_ref), &propname)
{
Some((ns.map(NamespaceOwned::from), tag.to_owned()))
} else {
None
@@ -133,7 +136,7 @@ pub async fn route_proppatch<R: ResourceService>(
// - internal properties
props_conflict.push(full_propname);
} else {
props_not_found.push((None, propname));
props_not_found.push((propns, propname));
}
}
}

View File

@@ -36,7 +36,6 @@ impl Error {
Self::NotFound => StatusCode::NOT_FOUND,
Self::AlreadyExists => StatusCode::CONFLICT,
Self::ReadOnly => StatusCode::FORBIDDEN,
// TODO: Can also be Bad Request, depending on when this is raised
Self::IcalError(_err) => StatusCode::INTERNAL_SERVER_ERROR,
Self::InvalidPrincipalType(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,

View File

@@ -1,6 +1,6 @@
use quick_xml::name::Namespace;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[derive(Debug, Clone, Default, PartialEq, Eq, Hash)]
pub struct NamespaceOwned(pub Vec<u8>);
impl<'a> From<Namespace<'a>> for NamespaceOwned {

View File

@@ -1,18 +1,21 @@
use std::io::BufRead;
use quick_xml::events::BytesStart;
use quick_xml::{events::BytesStart, name::ResolveResult};
use crate::{XmlDeserialize, XmlError};
use crate::{NamespaceOwned, XmlDeserialize, XmlError};
// TODO: actually implement
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Unparsed(String);
pub struct Unparsed(pub Option<NamespaceOwned>, pub String);
impl Unparsed {
#[must_use]
pub fn tag_name(&self) -> String {
// TODO: respect namespace?
self.0.clone()
pub const fn ns(&self) -> Option<&NamespaceOwned> {
self.0.as_ref()
}
#[must_use]
pub const fn tag_name(&self) -> &str {
self.1.as_str()
}
}
@@ -27,7 +30,12 @@ impl XmlDeserialize for Unparsed {
let mut buf = vec![];
reader.read_to_end_into(start.name(), &mut buf)?;
}
let tag_name = String::from_utf8_lossy(start.local_name().as_ref()).to_string();
Ok(Self(tag_name))
let (ns, tag_name) = reader.resolver().resolve_element(start.name());
let ns: Option<NamespaceOwned> = match ns {
ResolveResult::Bound(ns) => Some(ns.into()),
ResolveResult::Unbound | ResolveResult::Unknown(_) => None,
};
let tag_name = String::from_utf8_lossy(tag_name.as_ref()).to_string();
Ok(Self(ns, tag_name))
}
}