Compare commits

..

6 Commits

Author SHA1 Message Date
Lennart K
11a61cf8b1 version 0.12.1 2026-01-20 13:20:04 +01:00
Lennart Kämmle
227d4bc61a Merge pull request #171 from wrvsrx/fix-anniversayr-typo
Fix a typo about anniversary
2026-01-20 13:17:44 +01:00
wrvsrx
d9afc85222 Fix a typo about anniversary 2026-01-20 19:45:50 +08:00
Lennart
c9fe5706a9 clippy appeasement 2026-01-19 17:03:14 +01:00
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
9 changed files with 66 additions and 38 deletions

24
Cargo.lock generated
View File

@@ -3317,7 +3317,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical" name = "rustical"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"argon2", "argon2",
@@ -3364,7 +3364,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_caldav" name = "rustical_caldav"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"async-std", "async-std",
"async-trait", "async-trait",
@@ -3406,7 +3406,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_carddav" name = "rustical_carddav"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3440,7 +3440,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav" name = "rustical_dav"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3466,7 +3466,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_dav_push" name = "rustical_dav_push"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3491,7 +3491,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_frontend" name = "rustical_frontend"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"askama", "askama",
"askama_web", "askama_web",
@@ -3527,7 +3527,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_ical" name = "rustical_ical"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
@@ -3546,7 +3546,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_oidc" name = "rustical_oidc"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"axum", "axum",
@@ -3562,7 +3562,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store" name = "rustical_store"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"async-trait", "async-trait",
@@ -3595,7 +3595,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_store_sqlite" name = "rustical_store_sqlite"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"chrono", "chrono",
@@ -3620,7 +3620,7 @@ dependencies = [
[[package]] [[package]]
name = "rustical_xml" name = "rustical_xml"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"quick-xml", "quick-xml",
"thiserror 2.0.18", "thiserror 2.0.18",
@@ -5442,7 +5442,7 @@ checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
[[package]] [[package]]
name = "xml_derive" name = "xml_derive"
version = "0.12.0" version = "0.12.1"
dependencies = [ dependencies = [
"darling 0.23.0", "darling 0.23.0",
"heck", "heck",

View File

@@ -2,7 +2,7 @@
members = ["crates/*"] members = ["crates/*"]
[workspace.package] [workspace.package]
version = "0.12.0" version = "0.12.1"
rust-version = "1.92" rust-version = "1.92"
edition = "2024" edition = "2024"
description = "A CalDAV server" description = "A CalDAV server"

View File

@@ -1,6 +1,9 @@
use std::str::FromStr;
use crate::Error; use crate::Error;
use crate::calendar::CalendarResourceService; use crate::calendar::CalendarResourceService;
use crate::calendar::prop::SupportedCalendarComponentSet; use crate::calendar::prop::SupportedCalendarComponentSet;
use crate::error::Precondition;
use axum::extract::{Path, State}; use axum::extract::{Path, State};
use axum::response::{IntoResponse, Response}; use axum::response::{IntoResponse, Response};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
@@ -84,21 +87,33 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
} }
let timezone_id = if let Some(tzid) = request.calendar_timezone_id { 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) Some(tzid)
} else if let Some(tz) = request.calendar_timezone { } else if let Some(tz) = request.calendar_timezone {
// TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::from_slice(tz.as_bytes()) let calendar = IcalParser::from_slice(tz.as_bytes())
.next() .next()
.ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))? .ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
.map_err(|_| rustical_dav::Error::BadRequest("Error parsing timezone".to_owned()))?; "No timezone data provided",
)))?
.map_err(|_| {
Error::PreconditionFailed(Precondition::CalendarTimezone("Error parsing timezone"))
})?;
let timezone = calendar.vtimezones.values().next().ok_or_else(|| { let timezone = calendar
rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) .vtimezones
})?; .values()
.next()
.ok_or(Error::PreconditionFailed(Precondition::CalendarTimezone(
"No timezone data provided",
)))?;
let timezone: Option<chrono_tz::Tz> = timezone.into(); let timezone: Option<chrono_tz::Tz> = timezone.into();
let timezone = timezone.ok_or_else(|| { let timezone = timezone.ok_or(Error::PreconditionFailed(
rustical_dav::Error::BadRequest("Cannot translate VTIMEZONE into IANA TZID".to_owned()) Precondition::CalendarTimezone("No timezone data provided"),
})?; ))?;
Some(timezone.name().to_owned()) Some(timezone.name().to_owned())
} else { } else {

View File

@@ -12,6 +12,9 @@ pub enum Precondition {
#[error("valid-calendar-data")] #[error("valid-calendar-data")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")] #[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
ValidCalendarData, ValidCalendarData,
#[error("calendar-timezone")]
#[xml(ns = "rustical_dav::namespace::NS_CALDAV")]
CalendarTimezone(&'static str),
} }
impl IntoResponse for Precondition { 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 route_proppatch(&path, uri.path(), &body, &principal, &resource_service).await
} }
#[allow(clippy::too_many_lines)]
pub async fn route_proppatch<R: ResourceService>( pub async fn route_proppatch<R: ResourceService>(
path_components: &R::PathComponents, path_components: &R::PathComponents,
path: &str, path: &str,
@@ -116,12 +117,14 @@ pub async fn route_proppatch<R: ResourceService>(
} }
} }
SetPropertyPropWrapper::Invalid(invalid) => { SetPropertyPropWrapper::Invalid(invalid) => {
let propname = invalid.tag_name(); let Unparsed(propns, propname) = invalid;
if let Some(full_propname) = <R::Resource as Resource>::list_props() if let Some(full_propname) = <R::Resource as Resource>::list_props()
.into_iter() .into_iter()
.find_map(|(ns, tag)| { .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())) Some((ns.map(NamespaceOwned::from), tag.to_owned()))
} else { } else {
None None
@@ -133,7 +136,7 @@ pub async fn route_proppatch<R: ResourceService>(
// - internal properties // - internal properties
props_conflict.push(full_propname); props_conflict.push(full_propname);
} else { } 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::NotFound => StatusCode::NOT_FOUND,
Self::AlreadyExists => StatusCode::CONFLICT, Self::AlreadyExists => StatusCode::CONFLICT,
Self::ReadOnly => StatusCode::FORBIDDEN, Self::ReadOnly => StatusCode::FORBIDDEN,
// TODO: Can also be Bad Request, depending on when this is raised
Self::IcalError(_err) => StatusCode::INTERNAL_SERVER_ERROR, Self::IcalError(_err) => StatusCode::INTERNAL_SERVER_ERROR,
Self::InvalidPrincipalType(_) => StatusCode::BAD_REQUEST, Self::InvalidPrincipalType(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,

View File

@@ -338,7 +338,7 @@ impl CalendarStore for SqliteAddressbookStore {
out_objects.push((format!("{object_id}-birthday"), birthday)); out_objects.push((format!("{object_id}-birthday"), birthday));
} }
if let Some(anniversary) = object.get_anniversary_object()? { if let Some(anniversary) = object.get_anniversary_object()? {
out_objects.push((format!("{object_id}-anniversayr"), anniversary)); out_objects.push((format!("{object_id}-anniversary"), anniversary));
} }
} }
@@ -382,7 +382,7 @@ impl CalendarStore for SqliteAddressbookStore {
objects.push((format!("{object_id}-birthday"), birthday)); objects.push((format!("{object_id}-birthday"), birthday));
} }
if let Some(anniversary) = object.get_anniversary_object()? { if let Some(anniversary) = object.get_anniversary_object()? {
objects.push((format!("{object_id}-anniversayr"), anniversary)); objects.push((format!("{object_id}-anniversary"), anniversary));
} }
} }
Ok(objects) Ok(objects)

View File

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

View File

@@ -1,18 +1,21 @@
use std::io::BufRead; 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)] #[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Unparsed(String); pub struct Unparsed(pub Option<NamespaceOwned>, pub String);
impl Unparsed { impl Unparsed {
#[must_use] #[must_use]
pub fn tag_name(&self) -> String { pub const fn ns(&self) -> Option<&NamespaceOwned> {
// TODO: respect namespace? self.0.as_ref()
self.0.clone() }
#[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![]; let mut buf = vec![];
reader.read_to_end_into(start.name(), &mut buf)?; reader.read_to_end_into(start.name(), &mut buf)?;
} }
let tag_name = String::from_utf8_lossy(start.local_name().as_ref()).to_string(); let (ns, tag_name) = reader.resolver().resolve_element(start.name());
Ok(Self(tag_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))
} }
} }