Lots of clippy appeasement

This commit is contained in:
Lennart
2025-10-27 20:12:21 +01:00
parent 0d071d3b92
commit 86cf490fa9
84 changed files with 413 additions and 435 deletions

2
Cargo.lock generated
View File

@@ -3194,7 +3194,6 @@ dependencies = [
"chrono-tz", "chrono-tz",
"derive_more", "derive_more",
"ical", "ical",
"lazy_static",
"regex", "regex",
"rrule", "rrule",
"rustical_xml", "rustical_xml",
@@ -3234,7 +3233,6 @@ dependencies = [
"headers", "headers",
"http", "http",
"ical", "ical",
"lazy_static",
"regex", "regex",
"rrule", "rrule",
"rstest", "rstest",

View File

@@ -48,7 +48,6 @@ pbkdf2 = { version = "0.12", features = ["simple"] }
rand_core = { version = "0.9", features = ["std"] } rand_core = { version = "0.9", features = ["std"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
regex = "1.10" regex = "1.10"
lazy_static = "1.5"
rstest = "0.26" rstest = "0.26"
rstest_reuse = "0.7" rstest_reuse = "0.7"
sha2 = "0.10" sha2 = "0.10"

View File

@@ -32,7 +32,6 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
return Err(crate::Error::Unauthorized); return Err(crate::Error::Unauthorized);
} }
let mut timezones = HashMap::new();
let mut vtimezones = HashMap::new(); let mut vtimezones = HashMap::new();
let objects = cal_store.get_objects(&principal, &calendar_id).await?; let objects = cal_store.get_objects(&principal, &calendar_id).await?;
@@ -64,31 +63,23 @@ pub async fn route_get<C: CalendarStore, S: SubscriptionStore>(
for object in &objects { for object in &objects {
vtimezones.extend(object.get_vtimezones()); vtimezones.extend(object.get_vtimezones());
match object.get_data() { match object.get_data() {
CalendarObjectComponent::Event( CalendarObjectComponent::Event(EventObject { event, .. }, overrides) => {
EventObject {
event,
timezones: object_timezones,
..
},
overrides,
) => {
timezones.extend(object_timezones);
ical_calendar_builder = ical_calendar_builder.add_event(event.clone()); ical_calendar_builder = ical_calendar_builder.add_event(event.clone());
for _override in overrides { for ev_override in overrides {
ical_calendar_builder = ical_calendar_builder =
ical_calendar_builder.add_event(_override.event.clone()); ical_calendar_builder.add_event(ev_override.event.clone());
} }
} }
CalendarObjectComponent::Todo(todo, overrides) => { CalendarObjectComponent::Todo(todo, overrides) => {
ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone()); ical_calendar_builder = ical_calendar_builder.add_todo(todo.clone());
for _override in overrides { for ev_override in overrides {
ical_calendar_builder = ical_calendar_builder.add_todo(_override.clone()); ical_calendar_builder = ical_calendar_builder.add_todo(ev_override.clone());
} }
} }
CalendarObjectComponent::Journal(journal, overrides) => { CalendarObjectComponent::Journal(journal, overrides) => {
ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone()); ical_calendar_builder = ical_calendar_builder.add_journal(journal.clone());
for _override in overrides { for ev_override in overrides {
ical_calendar_builder = ical_calendar_builder.add_journal(_override.clone()); ical_calendar_builder = ical_calendar_builder.add_journal(ev_override.clone());
} }
} }
} }

View File

@@ -89,17 +89,12 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
// TODO: Proper error (calendar-timezone precondition) // TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::new(tz.as_bytes()) let calendar = IcalParser::new(tz.as_bytes())
.next() .next()
.ok_or(rustical_dav::Error::BadRequest( .ok_or_else(|| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?
"No timezone data provided".to_owned(),
))?
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?; .map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
let timezone = calendar let timezone = calendar.timezones.first().ok_or_else(|| {
.timezones rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
.first() })?;
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?;
let timezone: chrono_tz::Tz = timezone let timezone: chrono_tz::Tz = timezone
.try_into() .try_into()
.map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?; .map_err(|_| rustical_dav::Error::BadRequest("No timezone data provided".to_owned()))?;
@@ -123,14 +118,16 @@ pub async fn route_mkcalendar<C: CalendarStore, S: SubscriptionStore>(
synctoken: 0, synctoken: 0,
subscription_url: request.source.map(|href| href.href), subscription_url: request.source.map(|href| href.href),
push_topic: uuid::Uuid::new_v4().to_string(), push_topic: uuid::Uuid::new_v4().to_string(),
components: request components: request.supported_calendar_component_set.map_or_else(
.supported_calendar_component_set || {
.map(Into::into) vec![
.unwrap_or(vec![ CalendarObjectType::Event,
CalendarObjectType::Event, CalendarObjectType::Todo,
CalendarObjectType::Todo, CalendarObjectType::Journal,
CalendarObjectType::Journal, ]
]), },
Into::into,
),
}; };
cal_store.insert_calendar(calendar).await?; cal_store.insert_calendar(calendar).await?;

View File

@@ -53,7 +53,8 @@ pub async fn route_post<C: CalendarStore, S: SubscriptionStore>(
push_resource: request push_resource: request
.subscription .subscription
.web_push_subscription .web_push_subscription
.push_resource.clone(), .push_resource
.clone(),
topic: calendar_resource.cal.push_topic, topic: calendar_resource.cal.push_topic,
expiration: expires.naive_local(), expiration: expires.naive_local(),
public_key: request public_key: request

View File

@@ -36,11 +36,9 @@ pub async fn get_objects_calendar_multiget<C: CalendarStore>(
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());
continue;
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());
continue;
} }
} }

View File

@@ -36,7 +36,7 @@ pub struct TextMatchElement {
pub(crate) negate_condition: Option<String>, pub(crate) negate_condition: Option<String>,
} }
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[allow(dead_code)] #[allow(dead_code)]
// https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2 // https://www.rfc-editor.org/rfc/rfc4791#section-9.7.2
pub struct PropFilterElement { pub struct PropFilterElement {
@@ -76,7 +76,7 @@ impl CompFilterElement {
let comp_vcal = self.name == "VCALENDAR"; let comp_vcal = self.name == "VCALENDAR";
match (self.is_not_defined, comp_vcal) { match (self.is_not_defined, comp_vcal) {
// Client wants VCALENDAR to not exist but we are a VCALENDAR // Client wants VCALENDAR to not exist but we are a VCALENDAR
(Some(()), true) => return false, (Some(()), true) |
// Client is asking for something different than a vcalendar // Client is asking for something different than a vcalendar
(None, false) => return false, (None, false) => return false,
_ => {} _ => {}
@@ -106,7 +106,7 @@ impl CompFilterElement {
let comp_name_matches = self.name == cal_object.get_component_name(); let comp_name_matches = self.name == cal_object.get_component_name();
match (self.is_not_defined, comp_name_matches) { match (self.is_not_defined, comp_name_matches) {
// Client wants VCALENDAR to not exist but we are a VCALENDAR // Client wants VCALENDAR to not exist but we are a VCALENDAR
(Some(()), true) => return false, (Some(()), true) |
// Client is asking for something different than a vcalendar // Client is asking for something different than a vcalendar
(None, false) => return false, (None, false) => return false,
_ => {} _ => {}
@@ -164,7 +164,7 @@ impl From<&FilterElement> for CalendarQuery {
}; };
} }
} }
Default::default() Self::default()
} }
} }
@@ -184,10 +184,6 @@ pub struct CalendarQueryRequest {
impl From<&CalendarQueryRequest> for CalendarQuery { impl From<&CalendarQueryRequest> for CalendarQuery {
fn from(value: &CalendarQueryRequest) -> Self { fn from(value: &CalendarQueryRequest) -> Self {
value value.filter.as_ref().map(Self::from).unwrap_or_default()
.filter
.as_ref()
.map(Self::from)
.unwrap_or_default()
} }
} }

View File

@@ -33,7 +33,7 @@ mod tests {
PropFilterElement, TextMatchElement, PropFilterElement, TextMatchElement,
}, },
}, },
calendar_object::{CalendarObjectPropName, CalendarObjectPropWrapperName}, calendar_object::{CalendarData, CalendarObjectPropName, CalendarObjectPropWrapperName},
}; };
#[test] #[test]
@@ -76,7 +76,7 @@ mod tests {
CalendarObjectPropName::Getetag, CalendarObjectPropName::Getetag,
), ),
CalendarObjectPropWrapperName::CalendarObject( CalendarObjectPropWrapperName::CalendarObject(
CalendarObjectPropName::CalendarData(Default::default()) CalendarObjectPropName::CalendarData(CalendarData::default())
), ),
], ],
vec![] vec![]
@@ -115,6 +115,6 @@ mod tests {
timezone: None, timezone: None,
timezone_id: None timezone_id: None
} }
) );
} }
} }

View File

@@ -43,9 +43,9 @@ pub(crate) enum ReportRequest {
impl ReportRequest { impl ReportRequest {
const fn props(&self) -> &PropfindType<CalendarObjectPropWrapperName> { const fn props(&self) -> &PropfindType<CalendarObjectPropWrapperName> {
match &self { match &self {
Self::CalendarMultiget(CalendarMultigetRequest { prop, .. }) => prop, Self::CalendarMultiget(CalendarMultigetRequest { prop, .. })
Self::CalendarQuery(CalendarQueryRequest { prop, .. }) => prop, | Self::CalendarQuery(CalendarQueryRequest { prop, .. })
Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop, | Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
} }
} }
} }
@@ -184,7 +184,7 @@ mod tests {
"/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned() "/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
] ]
}) })
) );
} }
#[test] #[test]
@@ -241,7 +241,7 @@ mod tests {
timezone: None, timezone: None,
timezone_id: None, timezone_id: None,
}) })
) );
} }
#[test] #[test]
@@ -269,6 +269,6 @@ mod tests {
"/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned() "/caldav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
] ]
}) })
) );
} }
} }

View File

@@ -18,7 +18,7 @@ use rustical_xml::{EnumVariants, PropName};
use rustical_xml::{XmlDeserialize, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlSerialize};
use serde::Deserialize; use serde::Deserialize;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropName")] #[xml(unit_variants_ident = "CalendarPropName")]
pub enum CalendarProp { pub enum CalendarProp {
// CalDAV (RFC 4791) // CalDAV (RFC 4791)
@@ -54,7 +54,7 @@ pub enum CalendarProp {
MaxDateTime(String), MaxDateTime(String),
} }
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)] #[xml(unit_variants_ident = "CalendarPropWrapperName", untagged)]
pub enum CalendarPropWrapper { pub enum CalendarPropWrapper {
Calendar(CalendarProp), Calendar(CalendarProp),
@@ -135,7 +135,9 @@ impl Resource for CalendarResource {
} }
CalendarPropName::CalendarTimezone => { CalendarPropName::CalendarTimezone => {
CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| { CalendarProp::CalendarTimezone(self.cal.timezone_id.as_ref().and_then(|tzid| {
vtimezones_rs::VTIMEZONES.get(tzid).map(|tz| (*tz).to_string()) vtimezones_rs::VTIMEZONES
.get(tzid)
.map(|tz| (*tz).to_string())
})) }))
} }
// chrono_tz uses the IANA database // chrono_tz uses the IANA database
@@ -154,13 +156,13 @@ impl Resource for CalendarResource {
CalendarPropName::SupportedCalendarData => { CalendarPropName::SupportedCalendarData => {
CalendarProp::SupportedCalendarData(SupportedCalendarData::default()) CalendarProp::SupportedCalendarData(SupportedCalendarData::default())
} }
CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10000000), CalendarPropName::MaxResourceSize => CalendarProp::MaxResourceSize(10_000_000),
CalendarPropName::SupportedReportSet => { CalendarPropName::SupportedReportSet => {
CalendarProp::SupportedReportSet(SupportedReportSet::all()) CalendarProp::SupportedReportSet(SupportedReportSet::all())
} }
CalendarPropName::Source => CalendarProp::Source( CalendarPropName::Source => {
self.cal.subscription_url.clone().map(HrefElement::from), CalendarProp::Source(self.cal.subscription_url.clone().map(HrefElement::from))
), }
CalendarPropName::MinDateTime => { CalendarPropName::MinDateTime => {
CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format()) CalendarProp::MinDateTime(CalDateTime::from(DateTime::<Utc>::MIN_UTC).format())
} }
@@ -199,22 +201,20 @@ impl Resource for CalendarResource {
// TODO: Proper error (calendar-timezone precondition) // TODO: Proper error (calendar-timezone precondition)
let calendar = IcalParser::new(tz.as_bytes()) let calendar = IcalParser::new(tz.as_bytes())
.next() .next()
.ok_or(rustical_dav::Error::BadRequest( .ok_or_else(|| {
"No timezone data provided".to_owned(), rustical_dav::Error::BadRequest(
))? "No timezone data provided".to_owned(),
)
})?
.map_err(|_| { .map_err(|_| {
rustical_dav::Error::BadRequest( rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(), "No timezone data provided".to_owned(),
) )
})?; })?;
let timezone = let timezone = calendar.timezones.first().ok_or_else(|| {
calendar rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
.timezones })?;
.first()
.ok_or(rustical_dav::Error::BadRequest(
"No timezone data provided".to_owned(),
))?;
let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| { let timezone: chrono_tz::Tz = timezone.try_into().map_err(|_| {
rustical_dav::Error::BadRequest("No timezone data provided".to_owned()) rustical_dav::Error::BadRequest("No timezone data provided".to_owned())
})?; })?;
@@ -223,7 +223,6 @@ impl Resource for CalendarResource {
} }
Ok(()) Ok(())
} }
CalendarProp::TimezoneServiceSet(_) => Err(rustical_dav::Error::PropReadOnly),
CalendarProp::CalendarTimezoneId(timezone_id) => { CalendarProp::CalendarTimezoneId(timezone_id) => {
if let Some(tzid) = &timezone_id if let Some(tzid) = &timezone_id
&& !vtimezones_rs::VTIMEZONES.contains_key(tzid) && !vtimezones_rs::VTIMEZONES.contains_key(tzid)
@@ -243,13 +242,13 @@ impl Resource for CalendarResource {
self.cal.components = comp_set.into(); self.cal.components = comp_set.into();
Ok(()) Ok(())
} }
CalendarProp::SupportedCalendarData(_) => Err(rustical_dav::Error::PropReadOnly), CalendarProp::TimezoneServiceSet(_)
CalendarProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly), | CalendarProp::SupportedCalendarData(_)
CalendarProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), | CalendarProp::MaxResourceSize(_)
// Converting between a calendar subscription calendar and a normal one would be weird | CalendarProp::SupportedReportSet(_)
CalendarProp::Source(_) => Err(rustical_dav::Error::PropReadOnly), | CalendarProp::Source(_)
CalendarProp::MinDateTime(_) => Err(rustical_dav::Error::PropReadOnly), | CalendarProp::MinDateTime(_)
CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly), | CalendarProp::MaxDateTime(_) => Err(rustical_dav::Error::PropReadOnly),
}, },
CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop), CalendarPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop), CalendarPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
@@ -275,7 +274,6 @@ impl Resource for CalendarResource {
self.cal.timezone_id = None; self.cal.timezone_id = None;
Ok(()) Ok(())
} }
CalendarPropName::TimezoneServiceSet => Err(rustical_dav::Error::PropReadOnly),
CalendarPropName::CalendarOrder => { CalendarPropName::CalendarOrder => {
self.cal.meta.order = 0; self.cal.meta.order = 0;
Ok(()) Ok(())
@@ -283,13 +281,13 @@ impl Resource for CalendarResource {
CalendarPropName::SupportedCalendarComponentSet => { CalendarPropName::SupportedCalendarComponentSet => {
Err(rustical_dav::Error::PropReadOnly) Err(rustical_dav::Error::PropReadOnly)
} }
CalendarPropName::SupportedCalendarData => Err(rustical_dav::Error::PropReadOnly), CalendarPropName::TimezoneServiceSet
CalendarPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly), | CalendarPropName::SupportedCalendarData
CalendarPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), | CalendarPropName::MaxResourceSize
// Converting a calendar subscription calendar into a normal one would be weird | CalendarPropName::SupportedReportSet
CalendarPropName::Source => Err(rustical_dav::Error::PropReadOnly), | CalendarPropName::Source
CalendarPropName::MinDateTime => Err(rustical_dav::Error::PropReadOnly), | CalendarPropName::MinDateTime
CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly), | CalendarPropName::MaxDateTime => Err(rustical_dav::Error::PropReadOnly),
}, },
CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop), CalendarPropWrapperName::SyncToken(prop) => SyncTokenExtension::remove_prop(self, prop),
CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop), CalendarPropWrapperName::DavPush(prop) => DavPushExtension::remove_prop(self, prop),

View File

@@ -78,7 +78,7 @@ pub async fn put_event<C: CalendarStore>(
true true
}; };
let object = if let Ok(obj) = CalendarObject::from_ics(body.clone()) { obj } else { let Ok(object) = CalendarObject::from_ics(body.clone()) else {
debug!("invalid calendar data:\n{body}"); debug!("invalid calendar data:\n{body}");
return Err(Error::PreconditionFailed(Precondition::ValidCalendarData)); return Err(Error::PreconditionFailed(Precondition::ValidCalendarData));
}; };

View File

@@ -17,7 +17,7 @@ pub enum CalendarObjectProp {
CalendarData(String), CalendarData(String),
} }
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "CalendarObjectPropWrapperName", untagged)] #[xml(unit_variants_ident = "CalendarObjectPropWrapperName", untagged)]
pub enum CalendarObjectPropWrapper { pub enum CalendarObjectPropWrapper {
CalendarObject(CalendarObjectProp), CalendarObject(CalendarObjectProp),

View File

@@ -1,4 +1,7 @@
use super::prop::{CalendarObjectPropWrapper, CalendarObjectPropWrapperName, CalendarObjectPropName, CalendarObjectProp, CalendarData}; use super::prop::{
CalendarData, CalendarObjectProp, CalendarObjectPropName, CalendarObjectPropWrapper,
CalendarObjectPropWrapperName,
};
use crate::Error; use crate::Error;
use derive_more::derive::{From, Into}; use derive_more::derive::{From, Into};
use rustical_dav::{ use rustical_dav::{

View File

@@ -106,9 +106,8 @@ where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let name: String = Deserialize::deserialize(deserializer)?; let name: String = Deserialize::deserialize(deserializer)?;
if let Some(object_id) = name.strip_suffix(".ics") { name.strip_suffix(".ics").map_or_else(
Ok(object_id.to_owned()) || Err(serde::de::Error::custom("Missing .ics extension")),
} else { |object_id| Ok(object_id.to_owned()),
Err(serde::de::Error::custom("Missing .ics extension")) )
}
} }

View File

@@ -62,7 +62,8 @@ pub enum Error {
} }
impl Error { impl Error {
#[must_use] pub fn status_code(&self) -> StatusCode { #[must_use]
pub fn status_code(&self) -> StatusCode {
match self { match self {
Self::StoreError(err) => match err { Self::StoreError(err) => match err {
rustical_store::Error::NotFound => StatusCode::NOT_FOUND, rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
@@ -70,12 +71,11 @@ impl Error {
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN, rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}, },
Self::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::DavError(err) => StatusCode::try_from(err.status_code().as_u16()) Self::DavError(err) => StatusCode::try_from(err.status_code().as_u16())
.expect("Just converting between versions"), .expect("Just converting between versions"),
Self::Unauthorized => StatusCode::UNAUTHORIZED, Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Self::ChronoParseError(_) | Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound => StatusCode::NOT_FOUND, Self::NotFound => StatusCode::NOT_FOUND,
Self::IcalError(err) => err.status_code(), Self::IcalError(err) => err.status_code(),
Self::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED, Self::PreconditionFailed(_err) => StatusCode::PRECONDITION_FAILED,

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
use axum::{Extension, Router}; use axum::{Extension, Router};
use derive_more::Constructor; use derive_more::Constructor;
use principal::PrincipalResourceService; use principal::PrincipalResourceService;

View File

@@ -37,7 +37,7 @@ pub enum PrincipalProp {
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)]
pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>); pub struct CalendarHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)] #[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
pub enum PrincipalPropWrapper { pub enum PrincipalPropWrapper {
Principal(PrincipalProp), Principal(PrincipalProp),

View File

@@ -15,7 +15,7 @@ pub enum AddressObjectProp {
AddressData(String), AddressData(String),
} }
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressObjectPropWrapperName", untagged)] #[xml(unit_variants_ident = "AddressObjectPropWrapperName", untagged)]
pub enum AddressObjectPropWrapper { pub enum AddressObjectPropWrapper {
AddressObject(AddressObjectProp), AddressObject(AddressObjectProp),

View File

@@ -98,9 +98,8 @@ where
D: Deserializer<'de>, D: Deserializer<'de>,
{ {
let name: String = Deserialize::deserialize(deserializer)?; let name: String = Deserialize::deserialize(deserializer)?;
if let Some(object_id) = name.strip_suffix(".vcf") { name.strip_suffix(".vcf").map_or_else(
Ok(object_id.to_owned()) || Err(serde::de::Error::custom("Missing .vcf extension")),
} else { |object_id| Ok(object_id.to_owned()),
Err(serde::de::Error::custom("Missing .vcf extension")) )
}
} }

View File

@@ -127,6 +127,6 @@ mod tests {
} }
} }
} }
) );
} }
} }

View File

@@ -49,7 +49,8 @@ pub async fn route_post<AS: AddressbookStore, S: SubscriptionStore>(
push_resource: request push_resource: request
.subscription .subscription
.web_push_subscription .web_push_subscription
.push_resource.clone(), .push_resource
.clone(),
topic: addressbook_resource.0.push_topic, topic: addressbook_resource.0.push_topic,
expiration: expires.naive_local(), expiration: expires.naive_local(),
public_key: request public_key: request

View File

@@ -47,11 +47,9 @@ pub async fn get_objects_addressbook_multiget<AS: AddressbookStore>(
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());
continue;
} }
} else { } else {
not_found.push(href.to_owned()); not_found.push(href.to_owned());
continue;
} }
} }

View File

@@ -28,8 +28,8 @@ pub(crate) enum ReportRequest {
impl ReportRequest { impl ReportRequest {
const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> { const fn props(&self) -> &PropfindType<AddressObjectPropWrapperName> {
match self { match self {
Self::AddressbookMultiget(AddressbookMultigetRequest { prop, .. }) => prop, Self::AddressbookMultiget(AddressbookMultigetRequest { prop, .. })
Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop, | Self::SyncCollection(SyncCollectionRequest { prop, .. }) => prop,
} }
} }
} }
@@ -101,7 +101,7 @@ mod tests {
assert_eq!( assert_eq!(
report_request, report_request,
ReportRequest::SyncCollection(SyncCollectionRequest { ReportRequest::SyncCollection(SyncCollectionRequest {
sync_token: "".to_owned(), sync_token: String::new(),
sync_level: SyncLevel::One, sync_level: SyncLevel::One,
prop: rustical_dav::xml::PropfindType::Prop(PropElement( prop: rustical_dav::xml::PropfindType::Prop(PropElement(
vec![AddressObjectPropWrapperName::AddressObject( vec![AddressObjectPropWrapperName::AddressObject(
@@ -111,7 +111,7 @@ mod tests {
)), )),
limit: None limit: None
}) })
) );
} }
#[test] #[test]
@@ -142,6 +142,6 @@ mod tests {
"/carddav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned() "/carddav/user/user/6f787542-5256-401a-8db97003260da/ae7a998fdfd1d84a20391168962c62b".to_owned()
] ]
}) })
) );
} }
} }

View File

@@ -6,7 +6,7 @@ use rustical_dav_push::DavPushExtensionProp;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
use strum_macros::VariantArray; use strum_macros::VariantArray;
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressbookPropName")] #[xml(unit_variants_ident = "AddressbookPropName")]
pub enum AddressbookProp { pub enum AddressbookProp {
// CardDAV (RFC 6352) // CardDAV (RFC 6352)
@@ -20,7 +20,7 @@ pub enum AddressbookProp {
MaxResourceSize(i64), MaxResourceSize(i64),
} }
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)] #[xml(unit_variants_ident = "AddressbookPropWrapperName", untagged)]
pub enum AddressbookPropWrapper { pub enum AddressbookPropWrapper {
Addressbook(AddressbookProp), Addressbook(AddressbookProp),

View File

@@ -59,7 +59,7 @@ impl Resource for AddressbookResource {
AddressbookPropWrapperName::Addressbook(prop) => { AddressbookPropWrapperName::Addressbook(prop) => {
AddressbookPropWrapper::Addressbook(match prop { AddressbookPropWrapper::Addressbook(match prop {
AddressbookPropName::MaxResourceSize => { AddressbookPropName::MaxResourceSize => {
AddressbookProp::MaxResourceSize(10000000) AddressbookProp::MaxResourceSize(10_000_000)
} }
AddressbookPropName::SupportedReportSet => { AddressbookPropName::SupportedReportSet => {
AddressbookProp::SupportedReportSet(SupportedReportSet::all()) AddressbookProp::SupportedReportSet(SupportedReportSet::all())
@@ -92,9 +92,11 @@ impl Resource for AddressbookResource {
self.0.description = description; self.0.description = description;
Ok(()) Ok(())
} }
AddressbookProp::MaxResourceSize(_) => Err(rustical_dav::Error::PropReadOnly), AddressbookProp::MaxResourceSize(_)
AddressbookProp::SupportedReportSet(_) => Err(rustical_dav::Error::PropReadOnly), | AddressbookProp::SupportedReportSet(_)
AddressbookProp::SupportedAddressData(_) => Err(rustical_dav::Error::PropReadOnly), | AddressbookProp::SupportedAddressData(_) => {
Err(rustical_dav::Error::PropReadOnly)
}
}, },
AddressbookPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop), AddressbookPropWrapper::SyncToken(prop) => SyncTokenExtension::set_prop(self, prop),
AddressbookPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop), AddressbookPropWrapper::DavPush(prop) => DavPushExtension::set_prop(self, prop),
@@ -112,9 +114,11 @@ impl Resource for AddressbookResource {
self.0.description = None; self.0.description = None;
Ok(()) Ok(())
} }
AddressbookPropName::MaxResourceSize => Err(rustical_dav::Error::PropReadOnly), AddressbookPropName::MaxResourceSize
AddressbookPropName::SupportedReportSet => Err(rustical_dav::Error::PropReadOnly), | AddressbookPropName::SupportedReportSet
AddressbookPropName::SupportedAddressData => Err(rustical_dav::Error::PropReadOnly), | AddressbookPropName::SupportedAddressData => {
Err(rustical_dav::Error::PropReadOnly)
}
}, },
AddressbookPropWrapperName::SyncToken(prop) => { AddressbookPropWrapperName::SyncToken(prop) => {
SyncTokenExtension::remove_prop(self, prop) SyncTokenExtension::remove_prop(self, prop)

View File

@@ -30,7 +30,8 @@ pub enum Error {
} }
impl Error { impl Error {
#[must_use] pub const fn status_code(&self) -> StatusCode { #[must_use]
pub const fn status_code(&self) -> StatusCode {
match self { match self {
Self::StoreError(err) => match err { Self::StoreError(err) => match err {
rustical_store::Error::NotFound => StatusCode::NOT_FOUND, rustical_store::Error::NotFound => StatusCode::NOT_FOUND,
@@ -38,11 +39,10 @@ impl Error {
rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN, rustical_store::Error::ReadOnly => StatusCode::FORBIDDEN,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}, },
Self::ChronoParseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::DavError(err) => err.status_code(), Self::DavError(err) => err.status_code(),
Self::Unauthorized => StatusCode::UNAUTHORIZED, Self::Unauthorized => StatusCode::UNAUTHORIZED,
Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST, Self::XmlDecodeError(_) => StatusCode::BAD_REQUEST,
Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR, Self::ChronoParseError(_) | Self::NotImplemented => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound => StatusCode::NOT_FOUND, Self::NotFound => StatusCode::NOT_FOUND,
Self::IcalError(err) => err.status_code(), Self::IcalError(err) => err.status_code(),
} }

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
use axum::response::Redirect; use axum::response::Redirect;
use axum::routing::any; use axum::routing::any;
use axum::{Extension, Router}; use axum::{Extension, Router};
@@ -37,20 +38,15 @@ pub fn carddav_router<AP: AuthenticationProvider, A: AddressbookStore, S: Subscr
store: Arc<A>, store: Arc<A>,
subscription_store: Arc<S>, subscription_store: Arc<S>,
) -> Router { ) -> Router {
let principal_service = PrincipalResourceService::new( let principal_service =
store, PrincipalResourceService::new(store, auth_provider.clone(), subscription_store);
auth_provider.clone(),
subscription_store,
);
Router::new() Router::new()
.nest( .nest(
prefix, prefix,
RootResourceService::<_, Principal, CardDavPrincipalUri>::new( RootResourceService::<_, Principal, CardDavPrincipalUri>::new(principal_service)
principal_service, .axum_router()
) .layer(AuthenticationLayer::new(auth_provider))
.axum_router() .layer(Extension(CardDavPrincipalUri(prefix))),
.layer(AuthenticationLayer::new(auth_provider))
.layer(Extension(CardDavPrincipalUri(prefix))),
) )
.route( .route(
"/.well-known/carddav", "/.well-known/carddav",

View File

@@ -30,7 +30,7 @@ pub enum PrincipalProp {
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone)]
pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>); pub struct AddressbookHomeSet(#[xml(ty = "untagged", flatten)] pub Vec<HrefElement>);
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, EnumVariants, PropName)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, EnumVariants, PropName)]
#[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)] #[xml(unit_variants_ident = "PrincipalPropWrapperName", untagged)]
pub enum PrincipalPropWrapper { pub enum PrincipalPropWrapper {
Principal(PrincipalProp), Principal(PrincipalProp),

View File

@@ -35,9 +35,9 @@ pub enum Error {
} }
impl Error { impl Error {
#[must_use] pub const fn status_code(&self) -> StatusCode { #[must_use]
pub const fn status_code(&self) -> StatusCode {
match self { match self {
Self::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
Self::NotFound => StatusCode::NOT_FOUND, Self::NotFound => StatusCode::NOT_FOUND,
Self::BadRequest(_) => StatusCode::BAD_REQUEST, Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::Unauthorized => StatusCode::UNAUTHORIZED, Self::Unauthorized => StatusCode::UNAUTHORIZED,
@@ -52,7 +52,7 @@ impl Error {
}, },
Self::PropReadOnly => StatusCode::CONFLICT, Self::PropReadOnly => StatusCode::CONFLICT,
Self::PreconditionFailed => StatusCode::PRECONDITION_FAILED, Self::PreconditionFailed => StatusCode::PRECONDITION_FAILED,
Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR, Self::InternalError | Self::IOError(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Forbidden => StatusCode::FORBIDDEN, Self::Forbidden => StatusCode::FORBIDDEN,
} }
} }

View File

@@ -6,7 +6,7 @@ use crate::{
}; };
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants)]
#[xml(unit_variants_ident = "CommonPropertiesPropName")] #[xml(unit_variants_ident = "CommonPropertiesPropName")]
pub enum CommonPropertiesProp { pub enum CommonPropertiesProp {
// WebDAV (RFC 2518) // WebDAV (RFC 2518)
@@ -39,9 +39,9 @@ pub trait CommonPropertiesExtension: Resource {
CommonPropertiesPropName::Resourcetype => { CommonPropertiesPropName::Resourcetype => {
CommonPropertiesProp::Resourcetype(self.get_resourcetype()) CommonPropertiesProp::Resourcetype(self.get_resourcetype())
} }
CommonPropertiesPropName::Displayname => { CommonPropertiesPropName::Displayname => CommonPropertiesProp::Displayname(
CommonPropertiesProp::Displayname(self.get_displayname().map(std::string::ToString::to_string)) self.get_displayname().map(std::string::ToString::to_string),
} ),
CommonPropertiesPropName::CurrentUserPrincipal => { CommonPropertiesPropName::CurrentUserPrincipal => {
CommonPropertiesProp::CurrentUserPrincipal( CommonPropertiesProp::CurrentUserPrincipal(
principal_uri.principal_uri(principal.get_id()).into(), principal_uri.principal_uri(principal.get_id()).into(),

View File

@@ -85,10 +85,11 @@ impl<S: Send + Sync> FromRequestParts<S> for Depth {
parts: &mut axum::http::request::Parts, parts: &mut axum::http::request::Parts,
_state: &S, _state: &S,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
if let Some(depth_header) = parts.headers.get("Depth") { parts
depth_header.as_bytes().try_into() .headers
} else { .get("Depth")
Ok(Self::Zero) .map_or(Ok(Self::Zero), |depth_header| {
} depth_header.as_bytes().try_into()
})
} }
} }

View File

@@ -30,11 +30,10 @@ impl<S: Send + Sync> FromRequestParts<S> for Overwrite {
parts: &mut axum::http::request::Parts, parts: &mut axum::http::request::Parts,
_state: &S, _state: &S,
) -> Result<Self, Self::Rejection> { ) -> Result<Self, Self::Rejection> {
if let Some(overwrite_header) = parts.headers.get("Overwrite") { parts.headers.get("Overwrite").map_or_else(
overwrite_header.as_bytes().try_into() || Ok(Self::default()),
} else { |overwrite_header| overwrite_header.as_bytes().try_into(),
Ok(Self::default()) )
}
} }
} }
@@ -60,7 +59,7 @@ mod tests {
#[tokio::test] #[tokio::test]
async fn test_overwrite_default() { async fn test_overwrite_default() {
let request = Request::put("asd").body(()).unwrap(); let request = Request::put("asd").body(()).unwrap();
let (mut parts, _) = request.into_parts(); let (mut parts, ()) = request.into_parts();
let overwrite = Overwrite::from_request_parts(&mut parts, &()) let overwrite = Overwrite::from_request_parts(&mut parts, &())
.await .await
.unwrap(); .unwrap();

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc)]
pub mod error; pub mod error;
pub mod extensions; pub mod extensions;
pub mod header; pub mod header;

View File

@@ -47,7 +47,8 @@ pub struct UserPrivilegeSet {
} }
impl UserPrivilegeSet { impl UserPrivilegeSet {
#[must_use] pub fn has(&self, privilege: &UserPrivilege) -> bool { #[must_use]
pub fn has(&self, privilege: &UserPrivilege) -> bool {
if (privilege == &UserPrivilege::WriteProperties if (privilege == &UserPrivilege::WriteProperties
|| privilege == &UserPrivilege::WriteContent) || privilege == &UserPrivilege::WriteContent)
&& self.privileges.contains(&UserPrivilege::Write) && self.privileges.contains(&UserPrivilege::Write)
@@ -57,13 +58,15 @@ impl UserPrivilegeSet {
self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All) self.privileges.contains(privilege) || self.privileges.contains(&UserPrivilege::All)
} }
#[must_use] pub fn all() -> Self { #[must_use]
pub fn all() -> Self {
Self { Self {
privileges: HashSet::from([UserPrivilege::All]), privileges: HashSet::from([UserPrivilege::All]),
} }
} }
#[must_use] pub fn owner_only(is_owner: bool) -> Self { #[must_use]
pub fn owner_only(is_owner: bool) -> Self {
if is_owner { if is_owner {
Self::all() Self::all()
} else { } else {
@@ -71,7 +74,8 @@ impl UserPrivilegeSet {
} }
} }
#[must_use] pub fn owner_read(is_owner: bool) -> Self { #[must_use]
pub fn owner_read(is_owner: bool) -> Self {
if is_owner { if is_owner {
Self::read_only() Self::read_only()
} else { } else {
@@ -79,7 +83,8 @@ impl UserPrivilegeSet {
} }
} }
#[must_use] pub fn owner_write_properties(is_owner: bool) -> Self { #[must_use]
pub fn owner_write_properties(is_owner: bool) -> Self {
// Content is read-only but we can write properties // Content is read-only but we can write properties
if is_owner { if is_owner {
Self::write_properties() Self::write_properties()
@@ -88,7 +93,8 @@ impl UserPrivilegeSet {
} }
} }
#[must_use] pub fn read_only() -> Self { #[must_use]
pub fn read_only() -> Self {
Self { Self {
privileges: HashSet::from([ privileges: HashSet::from([
UserPrivilege::Read, UserPrivilege::Read,
@@ -98,7 +104,8 @@ impl UserPrivilegeSet {
} }
} }
#[must_use] pub fn write_properties() -> Self { #[must_use]
pub fn write_properties() -> Self {
Self { Self {
privileges: HashSet::from([ privileges: HashSet::from([
UserPrivilege::Read, UserPrivilege::Read,

View File

@@ -9,42 +9,50 @@ pub type MethodFunction<State> =
pub trait AxumMethods: Sized + Send + Sync + 'static { pub trait AxumMethods: Sized + Send + Sync + 'static {
#[inline] #[inline]
#[must_use] fn report() -> Option<MethodFunction<Self>> { #[must_use]
fn report() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn get() -> Option<MethodFunction<Self>> { #[must_use]
fn get() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn post() -> Option<MethodFunction<Self>> { #[must_use]
fn post() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn mkcol() -> Option<MethodFunction<Self>> { #[must_use]
fn mkcol() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn mkcalendar() -> Option<MethodFunction<Self>> { #[must_use]
fn mkcalendar() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn put() -> Option<MethodFunction<Self>> { #[must_use]
fn put() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn import() -> Option<MethodFunction<Self>> { #[must_use]
fn import() -> Option<MethodFunction<Self>> {
None None
} }
#[inline] #[inline]
#[must_use] fn allow_header() -> Allow { #[must_use]
fn allow_header() -> Allow {
let mut allow = vec![ let mut allow = vec![
Method::from_str("PROPFIND").unwrap(), Method::from_str("PROPFIND").unwrap(),
Method::from_str("PROPPATCH").unwrap(), Method::from_str("PROPPATCH").unwrap(),

View File

@@ -42,7 +42,8 @@ pub trait Resource: Clone + Send + 'static {
fn get_resourcetype(&self) -> Resourcetype; fn get_resourcetype(&self) -> Resourcetype;
#[must_use] fn list_props() -> Vec<(Option<Namespace<'static>>, &'static str)> { #[must_use]
fn list_props() -> Vec<(Option<Namespace<'static>>, &'static str)> {
Self::Prop::variant_names() Self::Prop::variant_names()
} }
@@ -75,27 +76,27 @@ pub trait Resource: Clone + Send + 'static {
} }
fn satisfies_if_match(&self, if_match: &IfMatch) -> bool { fn satisfies_if_match(&self, if_match: &IfMatch) -> bool {
if let Some(etag) = self.get_etag() { self.get_etag().map_or_else(
if let Ok(etag) = ETag::from_str(&etag) { || if_match.is_any(),
if_match.precondition_passes(&etag) |etag| {
} else { ETag::from_str(&etag).map_or_else(
if_match.is_any() |_| if_match.is_any(),
} |etag| if_match.precondition_passes(&etag),
} else { )
if_match.is_any() },
} )
} }
fn satisfies_if_none_match(&self, if_none_match: &IfNoneMatch) -> bool { fn satisfies_if_none_match(&self, if_none_match: &IfNoneMatch) -> bool {
if let Some(etag) = self.get_etag() { self.get_etag().map_or_else(
if let Ok(etag) = ETag::from_str(&etag) { || if_none_match != &IfNoneMatch::any(),
if_none_match.precondition_passes(&etag) |etag| {
} else { ETag::from_str(&etag).map_or_else(
if_none_match != &IfNoneMatch::any() |_| if_none_match != &IfNoneMatch::any(),
} |etag| if_none_match.precondition_passes(&etag),
} else { )
if_none_match != &IfNoneMatch::any() },
} )
} }
fn get_user_privileges( fn get_user_privileges(

View File

@@ -76,10 +76,7 @@ pub trait ResourceService: Clone + Sized + Send + Sync + AxumMethods + 'static {
Err(crate::Error::Forbidden.into()) Err(crate::Error::Forbidden.into())
} }
fn axum_service(self) -> AxumService<Self> fn axum_service(self) -> AxumService<Self> {
where
Self: AxumMethods,
{
AxumService::new(self) AxumService::new(self)
} }

View File

@@ -8,7 +8,8 @@ pub struct HrefElement {
} }
impl HrefElement { impl HrefElement {
#[must_use] pub const fn new(href: String) -> Self { #[must_use]
pub const fn new(href: String) -> Self {
Self { href } Self { href }
} }
} }

View File

@@ -19,6 +19,7 @@ pub struct PropstatElement<PropType: XmlSerialize> {
pub status: StatusCode, pub status: StatusCode,
} }
#[allow(clippy::trivially_copy_pass_by_ref)]
fn xml_serialize_status( fn xml_serialize_status(
status: &StatusCode, status: &StatusCode,
ns: Option<Namespace>, ns: Option<Namespace>,
@@ -56,6 +57,7 @@ pub struct ResponseElement<PropstatType: XmlSerialize> {
pub propstat: Vec<PropstatWrapper<PropstatType>>, pub propstat: Vec<PropstatWrapper<PropstatType>>,
} }
#[allow(clippy::trivially_copy_pass_by_ref, clippy::ref_option)]
fn xml_serialize_optional_status( fn xml_serialize_optional_status(
val: &Option<StatusCode>, val: &Option<StatusCode>,
ns: Option<Namespace>, ns: Option<Namespace>,

View File

@@ -6,7 +6,7 @@ use rustical_xml::XmlDeserialize;
use rustical_xml::XmlError; use rustical_xml::XmlError;
use rustical_xml::XmlRootTag; use rustical_xml::XmlRootTag;
#[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq)] #[derive(Debug, Clone, XmlDeserialize, XmlRootTag, PartialEq, Eq)]
#[xml(root = "propfind", ns = "crate::namespace::NS_DAV")] #[xml(root = "propfind", ns = "crate::namespace::NS_DAV")]
pub struct PropfindElement<PN: XmlDeserialize> { pub struct PropfindElement<PN: XmlDeserialize> {
#[xml(ty = "untagged")] #[xml(ty = "untagged")]

View File

@@ -10,7 +10,8 @@ pub struct SupportedReportSet<T: XmlSerialize + 'static> {
} }
impl<T: XmlSerialize + Clone + 'static> SupportedReportSet<T> { impl<T: XmlSerialize + Clone + 'static> SupportedReportSet<T> {
#[must_use] pub fn new(methods: Vec<T>) -> Self { #[must_use]
pub fn new(methods: Vec<T>) -> Self {
Self { Self {
supported_report: methods supported_report: methods
.into_iter() .into_iter()

View File

@@ -40,6 +40,6 @@ mod tests {
<calendar-color xmlns="http://calendarserver.org/ns/"/> <calendar-color xmlns="http://calendarserver.org/ns/"/>
</resourcetype> </resourcetype>
</document>"# </document>"#
) );
} }
} }

View File

@@ -56,7 +56,7 @@ impl From<LimitElement> for u64 {
#[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct NresultsElement(#[xml(ty = "text")] u64); pub struct NresultsElement(#[xml(ty = "text")] u64);
#[derive(XmlDeserialize, Clone, Debug, PartialEq, XmlRootTag)] #[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq, XmlRootTag)]
// <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)> // <!ELEMENT sync-collection (sync-token, sync-level, limit?, prop)>
// <!-- DAV:limit defined in RFC 5323, Section 5.17 --> // <!-- DAV:limit defined in RFC 5323, Section 5.17 -->
// <!-- DAV:prop defined in RFC 4918, Section 14.18 --> // <!-- DAV:prop defined in RFC 4918, Section 14.18 -->
@@ -106,11 +106,11 @@ mod tests {
assert_eq!( assert_eq!(
request, request,
SyncCollectionRequest { SyncCollectionRequest {
sync_token: "".to_owned(), sync_token: String::new(),
sync_level: SyncLevel::One, sync_level: SyncLevel::One,
prop: PropfindType::Prop(PropElement(vec![TestPropName::Getetag], vec![])), prop: PropfindType::Prop(PropElement(vec![TestPropName::Getetag], vec![])),
limit: Some(100.into()) limit: Some(100.into())
} }
) );
} }
} }

View File

@@ -17,15 +17,13 @@ impl XmlSerialize for TagList {
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns.and_then(|ns| namespaces.get(&ns)).map(|prefix| {
.and_then(|ns| namespaces.get(&ns)) if prefix.is_empty() {
.map(|prefix| { String::new()
if prefix.is_empty() { } else {
String::new() format!("{prefix}:")
} else { }
format!("{prefix}:") });
}
});
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());

View File

@@ -2,7 +2,7 @@ use crate::{ContentUpdate, PropertyUpdate, SupportedTriggers, Transports, Trigge
use rustical_dav::header::Depth; use rustical_dav::header::Depth;
use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize}; use rustical_xml::{EnumVariants, PropName, XmlDeserialize, XmlSerialize};
#[derive(XmlDeserialize, XmlSerialize, PartialEq, Clone, PropName, EnumVariants)] #[derive(XmlDeserialize, XmlSerialize, PartialEq, Eq, Clone, PropName, EnumVariants)]
#[xml(unit_variants_ident = "DavPushExtensionPropName")] #[xml(unit_variants_ident = "DavPushExtensionPropName")]
pub enum DavPushExtensionProp { pub enum DavPushExtensionProp {
// WebDav Push // WebDav Push
@@ -32,7 +32,7 @@ pub trait DavPushExtension {
) -> Result<DavPushExtensionProp, rustical_dav::Error> { ) -> Result<DavPushExtensionProp, rustical_dav::Error> {
Ok(match &prop { Ok(match &prop {
DavPushExtensionPropName::Transports => { DavPushExtensionPropName::Transports => {
DavPushExtensionProp::Transports(Default::default()) DavPushExtensionProp::Transports(Transports::default())
} }
DavPushExtensionPropName::Topic => DavPushExtensionProp::Topic(self.get_topic()), DavPushExtensionPropName::Topic => DavPushExtensionProp::Topic(self.get_topic()),
DavPushExtensionPropName::SupportedTriggers => { DavPushExtensionPropName::SupportedTriggers => {

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc)]
mod extension; mod extension;
mod prop; mod prop;
pub mod register; pub mod register;
@@ -69,6 +70,7 @@ impl<S: SubscriptionStore> DavPushController<S> {
} }
} }
#[allow(clippy::cognitive_complexity)]
async fn send_message(&self, message: CollectionOperation) { async fn send_message(&self, message: CollectionOperation) {
let subscriptions = match self.sub_store.get_subscriptions(&message.topic).await { let subscriptions = match self.sub_store.get_subscriptions(&message.topic).await {
Ok(subs) => subs, Ok(subs) => subs,

View File

@@ -7,7 +7,7 @@ pub enum Transport {
WebPush, WebPush,
} }
#[derive(Debug, Clone, XmlSerialize, PartialEq)] #[derive(Debug, Clone, XmlSerialize, PartialEq, Eq)]
pub struct Transports { pub struct Transports {
#[xml(flatten, ty = "untagged")] #[xml(flatten, ty = "untagged")]
#[xml(ns = "crate::namespace::NS_DAVPUSH")] #[xml(ns = "crate::namespace::NS_DAVPUSH")]
@@ -22,10 +22,10 @@ impl Default for Transports {
} }
} }
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Clone)] #[derive(XmlSerialize, XmlDeserialize, PartialEq, Eq, Clone)]
pub struct SupportedTriggers(#[xml(flatten, ty = "untagged")] pub Vec<Trigger>); pub struct SupportedTriggers(#[xml(flatten, ty = "untagged")] pub Vec<Trigger>);
#[derive(XmlSerialize, XmlDeserialize, PartialEq, Debug, Clone)] #[derive(XmlSerialize, XmlDeserialize, PartialEq, Eq, Debug, Clone)]
pub enum Trigger { pub enum Trigger {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
ContentUpdate(ContentUpdate), ContentUpdate(ContentUpdate),

View File

@@ -1,7 +1,7 @@
use crate::Trigger; use crate::Trigger;
use rustical_xml::{XmlDeserialize, XmlRootTag, XmlSerialize}; use rustical_xml::{XmlDeserialize, XmlRootTag, XmlSerialize};
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
#[xml(ns = "crate::namespace::NS_DAVPUSH")] #[xml(ns = "crate::namespace::NS_DAVPUSH")]
pub struct WebPushSubscription { pub struct WebPushSubscription {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
@@ -23,16 +23,16 @@ pub struct SubscriptionPublicKey {
pub key: String, pub key: String,
} }
#[derive(XmlDeserialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, Clone, Debug, PartialEq, Eq)]
pub struct SubscriptionElement { pub struct SubscriptionElement {
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
pub web_push_subscription: WebPushSubscription, pub web_push_subscription: WebPushSubscription,
} }
#[derive(XmlDeserialize, XmlSerialize, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlSerialize, Clone, Debug, PartialEq, Eq)]
pub struct TriggerElement(#[xml(ty = "untagged", flatten)] Vec<Trigger>); pub struct TriggerElement(#[xml(ty = "untagged", flatten)] Vec<Trigger>);
#[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq)] #[derive(XmlDeserialize, XmlRootTag, Clone, Debug, PartialEq, Eq)]
#[xml(root = "push-register")] #[xml(root = "push-register")]
#[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")] #[xml(ns = "rustical_dav::namespace::NS_DAVPUSH")]
pub struct PushRegister { pub struct PushRegister {
@@ -100,6 +100,6 @@ mod tests {
Trigger::PropertyUpdate(PropertyUpdate(Depth::Zero)), Trigger::PropertyUpdate(PropertyUpdate(Depth::Zero)),
])) ]))
} }
) );
} }
} }

View File

@@ -8,7 +8,7 @@ use futures_core::future::BoxFuture;
use headers::{ContentType, ETag, HeaderMapExt}; use headers::{ContentType, ETag, HeaderMapExt};
use http::{Method, StatusCode}; use http::{Method, StatusCode};
use rust_embed::RustEmbed; use rust_embed::RustEmbed;
use std::{convert::Infallible, marker::PhantomData, str::FromStr}; use std::{borrow::Cow, convert::Infallible, marker::PhantomData, str::FromStr};
use tower::Service; use tower::Service;
#[derive(Clone, RustEmbed, Default)] #[derive(Clone, RustEmbed, Default)]
@@ -16,6 +16,7 @@ use tower::Service;
#[allow(dead_code)] // Since this is not used with the frontend-dev feature #[allow(dead_code)] // Since this is not used with the frontend-dev feature
pub struct Assets; pub struct Assets;
#[allow(dead_code)]
#[derive(Clone, Default)] #[derive(Clone, Default)]
pub struct EmbedService<E> pub struct EmbedService<E>
where where
@@ -41,6 +42,7 @@ where
} }
#[inline] #[inline]
#[allow(clippy::similar_names)]
fn call(&mut self, mut req: Request) -> Self::Future { fn call(&mut self, mut req: Request) -> Self::Future {
Box::pin(async move { Box::pin(async move {
if req.method() != Method::GET && req.method() != Method::HEAD { if req.method() != Method::GET && req.method() != Method::HEAD {
@@ -60,7 +62,7 @@ where
let mime = mime_guess::from_path(path).first_or_octet_stream(); let mime = mime_guess::from_path(path).first_or_octet_stream();
let body = if req.method() == Method::HEAD { let body = if req.method() == Method::HEAD {
Default::default() Cow::default()
} else { } else {
data data
}; };

View File

@@ -48,7 +48,7 @@ pub fn frontend_router<AP: AuthenticationProvider, CS: CalendarStore, AS: Addres
) -> Router { ) -> Router {
let user_router = Router::new() let user_router = Router::new()
.route("/", get(route_get_home)) .route("/", get(route_get_home))
.route("/{user}", get(route_user_named::<CS, AS, AP>)) .route("/{user}", get(route_user_named::<AP>))
// App token management // App token management
.route("/{user}/app_token", post(route_post_app_token::<AP>)) .route("/{user}/app_token", post(route_post_app_token::<AP>))
.route( .route(

View File

@@ -27,20 +27,22 @@ pub async fn post_nextcloud_login(
let token = uuid::Uuid::new_v4().to_string(); let token = uuid::Uuid::new_v4().to_string();
let app_name = user_agent.to_string(); let app_name = user_agent.to_string();
let mut flows = state.flows.write().await; {
// Flows must not last longer than 10 minutes let mut flows = state.flows.write().await;
// We also enforce that condition here to prevent a memory leak where unpolled flows would // Flows must not last longer than 10 minutes
// never be cleaned up // We also enforce that condition here to prevent a memory leak where unpolled flows would
flows.retain(|_, flow| Utc::now() - flow.created_at < Duration::minutes(10)); // never be cleaned up
flows.insert( flows.retain(|_, flow| Utc::now() - flow.created_at < Duration::minutes(10));
flow_id.clone(), flows.insert(
NextcloudFlow { flow_id.clone(),
app_name: app_name.clone(), NextcloudFlow {
created_at: Utc::now(), app_name: app_name.clone(),
token: token.clone(), created_at: Utc::now(),
response: None, token: token.clone(),
}, response: None,
); },
);
}
Json(NextcloudLoginResponse { Json(NextcloudLoginResponse {
login: format!("https://{host}/index.php/login/v2/flow/{flow_id}"), login: format!("https://{host}/index.php/login/v2/flow/{flow_id}"),
poll: NextcloudLoginPoll { poll: NextcloudLoginPoll {
@@ -56,6 +58,7 @@ pub struct NextcloudPollForm {
token: String, token: String,
} }
#[allow(clippy::significant_drop_tightening)]
pub async fn post_nextcloud_poll<AP: AuthenticationProvider>( pub async fn post_nextcloud_poll<AP: AuthenticationProvider>(
Extension(state): Extension<Arc<NextcloudFlows>>, Extension(state): Extension<Arc<NextcloudFlows>>,
Path(flow_id): Path<String>, Path(flow_id): Path<String>,

View File

@@ -1,8 +1,7 @@
use std::sync::Arc;
use async_trait::async_trait; use async_trait::async_trait;
use rustical_oidc::UserStore; use rustical_oidc::UserStore;
use rustical_store::auth::{AuthenticationProvider, Principal}; use rustical_store::auth::{AuthenticationProvider, Principal, PrincipalType};
use std::sync::Arc;
pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>); pub struct OidcUserStore<AP: AuthenticationProvider>(pub Arc<AP>);
@@ -26,7 +25,7 @@ impl<AP: AuthenticationProvider> UserStore for OidcUserStore<AP> {
Principal { Principal {
id: id.to_owned(), id: id.to_owned(),
displayname: None, displayname: None,
principal_type: Default::default(), principal_type: PrincipalType::default(),
password: None, password: None,
memberships: vec![], memberships: vec![],
}, },

View File

@@ -42,8 +42,8 @@ pub async fn route_addressbook_restore<AS: AddressbookStore>(
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
} }
store.restore_addressbook(&owner, &addressbook_id).await?; store.restore_addressbook(&owner, &addressbook_id).await?;
Ok(match referer { Ok(referer.map_or_else(
Some(referer) => Redirect::to(&referer.to_string()).into_response(), || (StatusCode::CREATED, "Restored").into_response(),
None => (StatusCode::CREATED, "Restored").into_response(), |referer| Redirect::to(&referer.to_string()).into_response(),
}) ))
} }

View File

@@ -42,8 +42,8 @@ pub async fn route_calendar_restore<CS: CalendarStore>(
return Ok(StatusCode::UNAUTHORIZED.into_response()); return Ok(StatusCode::UNAUTHORIZED.into_response());
} }
store.restore_calendar(&owner, &cal_id).await?; store.restore_calendar(&owner, &cal_id).await?;
Ok(match referer { Ok(referer.map_or_else(
Some(referer) => Redirect::to(&referer.to_string()).into_response(), || (StatusCode::CREATED, "Restored").into_response(),
None => (StatusCode::CREATED, "Restored").into_response(), |referer| Redirect::to(&referer.to_string()).into_response(),
}) ))
} }

View File

@@ -10,10 +10,7 @@ use axum::{
use axum_extra::{TypedHeader, extract::Host}; use axum_extra::{TypedHeader, extract::Host};
use headers::UserAgent; use headers::UserAgent;
use http::StatusCode; use http::StatusCode;
use rustical_store::{ use rustical_store::auth::{AppToken, AuthenticationProvider, Principal};
AddressbookStore, CalendarStore,
auth::{AppToken, AuthenticationProvider, Principal},
};
use crate::pages::user::{Section, UserPage}; use crate::pages::user::{Section, UserPage};
@@ -32,14 +29,8 @@ pub struct ProfileSection {
pub davx5_hostname: Option<String>, pub davx5_hostname: Option<String>,
} }
pub async fn route_user_named< pub async fn route_user_named<AP: AuthenticationProvider>(
CS: CalendarStore,
AS: AddressbookStore,
AP: AuthenticationProvider,
>(
Path(user_id): Path<String>, Path(user_id): Path<String>,
Extension(cal_store): Extension<Arc<CS>>,
Extension(addr_store): Extension<Arc<AS>>,
Extension(auth_provider): Extension<Arc<AP>>, Extension(auth_provider): Extension<Arc<AP>>,
TypedHeader(user_agent): TypedHeader<UserAgent>, TypedHeader(user_agent): TypedHeader<UserAgent>,
Host(host): Host, Host(host): Host,
@@ -49,26 +40,6 @@ pub async fn route_user_named<
return StatusCode::UNAUTHORIZED.into_response(); return StatusCode::UNAUTHORIZED.into_response();
} }
let mut calendars = vec![];
for group in user.memberships() {
calendars.extend(cal_store.get_calendars(group).await.unwrap());
}
let mut deleted_calendars = vec![];
for group in user.memberships() {
deleted_calendars.extend(cal_store.get_deleted_calendars(group).await.unwrap());
}
let mut addressbooks = vec![];
for group in user.memberships() {
addressbooks.extend(addr_store.get_addressbooks(group).await.unwrap());
}
let mut deleted_addressbooks = vec![];
for group in user.memberships() {
deleted_addressbooks.extend(addr_store.get_deleted_addressbooks(group).await.unwrap());
}
let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS"); let is_apple = user_agent.as_str().contains("Apple") || user_agent.as_str().contains("Mac OS");
let davx5_hostname = user_agent.as_str().contains("Android").then_some(host); let davx5_hostname = user_agent.as_str().contains("Android").then_some(host);

View File

@@ -15,7 +15,6 @@ thiserror.workspace = true
derive_more.workspace = true derive_more.workspace = true
rustical_xml.workspace = true rustical_xml.workspace = true
ical.workspace = true ical.workspace = true
lazy_static.workspace = true
regex.workspace = true regex.workspace = true
rrule.workspace = true rrule.workspace = true
serde.workspace = true serde.workspace = true

View File

@@ -22,7 +22,7 @@ impl TryFrom<VcardContact> for AddressObject {
fn try_from(vcard: VcardContact) -> Result<Self, Self::Error> { fn try_from(vcard: VcardContact) -> Result<Self, Self::Error> {
let uid = vcard let uid = vcard
.get_uid() .get_uid()
.ok_or(Error::InvalidData("missing UID".to_owned()))? .ok_or_else(|| Error::InvalidData("missing UID".to_owned()))?
.to_owned(); .to_owned();
let vcf = vcard.generate(); let vcf = vcard.generate();
Ok(Self { Ok(Self {
@@ -45,32 +45,38 @@ impl AddressObject {
Ok(Self { id, vcf, vcard }) Ok(Self { id, vcf, vcard })
} }
#[must_use] pub fn get_id(&self) -> &str { #[must_use]
pub fn get_id(&self) -> &str {
&self.id &self.id
} }
#[must_use] pub fn get_etag(&self) -> String { #[must_use]
pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(self.get_id()); hasher.update(self.get_id());
hasher.update(self.get_vcf()); hasher.update(self.get_vcf());
format!("\"{:x}\"", hasher.finalize()) format!("\"{:x}\"", hasher.finalize())
} }
#[must_use] pub fn get_vcf(&self) -> &str { #[must_use]
pub fn get_vcf(&self) -> &str {
&self.vcf &self.vcf
} }
#[must_use] pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> { #[must_use]
pub fn get_anniversary(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?; let prop = self.vcard.get_property("ANNIVERSARY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok() CalDateTime::parse_vcard(prop).ok()
} }
#[must_use] pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> { #[must_use]
pub fn get_birthday(&self) -> Option<(CalDateTime, bool)> {
let prop = self.vcard.get_property("BDAY")?.value.as_deref()?; let prop = self.vcard.get_property("BDAY")?.value.as_deref()?;
CalDateTime::parse_vcard(prop).ok() CalDateTime::parse_vcard(prop).ok()
} }
#[must_use] pub fn get_full_name(&self) -> Option<&str> { #[must_use]
pub fn get_full_name(&self) -> Option<&str> {
let prop = self.vcard.get_property("FN")?; let prop = self.vcard.get_property("FN")?;
prop.value.as_deref() prop.value.as_deref()
} }
@@ -78,9 +84,7 @@ impl AddressObject {
pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_anniversary_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok( Ok(
if let Some((anniversary, contains_year)) = self.get_anniversary() { if let Some((anniversary, contains_year)) = self.get_anniversary() {
let fullname = if let Some(name) = self.get_full_name() { let Some(fullname) = self.get_full_name() else {
name
} else {
return Ok(None); return Ok(None);
}; };
let anniversary = anniversary.date(); let anniversary = anniversary.date();
@@ -122,9 +126,7 @@ END:VCALENDAR",
pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> { pub fn get_birthday_object(&self) -> Result<Option<CalendarObject>, Error> {
Ok( Ok(
if let Some((birthday, contains_year)) = self.get_birthday() { if let Some((birthday, contains_year)) = self.get_birthday() {
let fullname = if let Some(name) = self.get_full_name() { let Some(fullname) = self.get_full_name() else {
name
} else {
return Ok(None); return Ok(None);
}; };
let birthday = birthday.date(); let birthday = birthday.date();

View File

@@ -24,10 +24,12 @@ pub enum Error {
} }
impl Error { impl Error {
#[must_use] pub const fn status_code(&self) -> StatusCode { #[must_use]
pub const fn status_code(&self) -> StatusCode {
match self { match self {
Self::InvalidData(_) => StatusCode::BAD_REQUEST, Self::InvalidData(_) | Self::MissingCalendar | Self::MissingContact => {
Self::MissingCalendar | Self::MissingContact => StatusCode::BAD_REQUEST, StatusCode::BAD_REQUEST
}
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
} }
} }

View File

@@ -15,7 +15,8 @@ pub struct EventObject {
} }
impl EventObject { impl EventObject {
#[must_use] pub fn get_uid(&self) -> &str { #[must_use]
pub fn get_uid(&self) -> &str {
self.event.get_uid() self.event.get_uid()
} }
@@ -70,9 +71,9 @@ impl EventObject {
for prop in &self.event.properties { for prop in &self.event.properties {
rrule_set = match prop.name.as_str() { rrule_set = match prop.name.as_str() {
"RRULE" => { "RRULE" => {
let rrule = RRule::from_str(prop.value.as_ref().ok_or(Error::RRuleError( let rrule = RRule::from_str(prop.value.as_ref().ok_or_else(|| {
rrule::ParseError::MissingDateGenerationRules.into(), Error::RRuleError(rrule::ParseError::MissingDateGenerationRules.into())
))?)? })?)?
.validate(dtstart) .validate(dtstart)
.unwrap(); .unwrap();
rrule_set.rrule(rrule) rrule_set.rrule(rrule)
@@ -120,8 +121,8 @@ impl EventObject {
date.format() date.format()
}; };
for _override in overrides { for ev_override in overrides {
if let Some(override_id) = &_override if let Some(override_id) = &ev_override
.event .event
.get_recurrence_id() .get_recurrence_id()
.as_ref() .as_ref()
@@ -131,7 +132,7 @@ impl EventObject {
{ {
// We have an override for this occurence // We have an override for this occurence
// //
events.push(_override.event.clone()); events.push(ev_override.event.clone());
continue 'recurrence; continue 'recurrence;
} }
} }
@@ -185,7 +186,7 @@ mod tests {
use crate::CalendarObject; use crate::CalendarObject;
use ical::generator::Emitter; use ical::generator::Emitter;
const ICS: &str = r#"BEGIN:VCALENDAR const ICS: &str = r"BEGIN:VCALENDAR
CALSCALE:GREGORIAN CALSCALE:GREGORIAN
VERSION:2.0 VERSION:2.0
BEGIN:VTIMEZONE BEGIN:VTIMEZONE
@@ -203,7 +204,7 @@ SUMMARY:weekly stuff
TRANSP:OPAQUE TRANSP:OPAQUE
RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU RRULE:FREQ=WEEKLY;COUNT=4;INTERVAL=2;BYDAY=TU,TH,SU
END:VEVENT END:VEVENT
END:VCALENDAR"#; END:VCALENDAR";
const EXPANDED: [&str; 4] = [ const EXPANDED: [&str; 4] = [
"BEGIN:VEVENT\r "BEGIN:VEVENT\r
@@ -251,13 +252,7 @@ END:VEVENT\r\n",
#[test] #[test]
fn test_expand_recurrence() { fn test_expand_recurrence() {
let event = CalendarObject::from_ics(ICS.to_string()).unwrap(); let event = CalendarObject::from_ics(ICS.to_string()).unwrap();
let (event, overrides) = if let crate::CalendarObjectComponent::Event( let crate::CalendarObjectComponent::Event(event, overrides) = event.get_data() else {
main_event,
overrides,
) = event.get_data()
{
(main_event, overrides)
} else {
panic!() panic!()
}; };

View File

@@ -26,7 +26,8 @@ pub enum CalendarObjectType {
} }
impl CalendarObjectType { impl CalendarObjectType {
#[must_use] pub const fn as_str(&self) -> &'static str { #[must_use]
pub const fn as_str(&self) -> &'static str {
match self { match self {
Self::Event => "VEVENT", Self::Event => "VEVENT",
Self::Todo => "VTODO", Self::Todo => "VTODO",
@@ -208,15 +209,18 @@ impl CalendarObject {
}) })
} }
#[must_use] pub const fn get_vtimezones(&self) -> &HashMap<String, IcalTimeZone> { #[must_use]
pub const fn get_vtimezones(&self) -> &HashMap<String, IcalTimeZone> {
&self.vtimezones &self.vtimezones
} }
#[must_use] pub const fn get_data(&self) -> &CalendarObjectComponent { #[must_use]
pub const fn get_data(&self) -> &CalendarObjectComponent {
&self.data &self.data
} }
#[must_use] pub fn get_id(&self) -> &str { #[must_use]
pub fn get_id(&self) -> &str {
match &self.data { match &self.data {
// We've made sure before that the first component exists and all components share the // We've made sure before that the first component exists and all components share the
// same UID // same UID
@@ -226,22 +230,26 @@ impl CalendarObject {
} }
} }
#[must_use] pub fn get_etag(&self) -> String { #[must_use]
pub fn get_etag(&self) -> String {
let mut hasher = Sha256::new(); let mut hasher = Sha256::new();
hasher.update(self.get_id()); hasher.update(self.get_id());
hasher.update(self.get_ics()); hasher.update(self.get_ics());
format!("\"{:x}\"", hasher.finalize()) format!("\"{:x}\"", hasher.finalize())
} }
#[must_use] pub fn get_ics(&self) -> &str { #[must_use]
pub fn get_ics(&self) -> &str {
&self.ics &self.ics
} }
#[must_use] pub fn get_component_name(&self) -> &str { #[must_use]
pub fn get_component_name(&self) -> &str {
self.get_object_type().as_str() self.get_object_type().as_str()
} }
#[must_use] pub fn get_object_type(&self) -> CalendarObjectType { #[must_use]
pub fn get_object_type(&self) -> CalendarObjectType {
(&self.data).into() (&self.data).into()
} }
@@ -249,7 +257,7 @@ impl CalendarObject {
match &self.data { match &self.data {
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
.iter() .iter()
.chain([main_event].into_iter()) .chain(std::iter::once(main_event))
.map(super::event::EventObject::get_dtstart) .map(super::event::EventObject::get_dtstart)
.collect::<Result<Vec<_>, _>>()? .collect::<Result<Vec<_>, _>>()?
.into_iter() .into_iter()
@@ -263,7 +271,7 @@ impl CalendarObject {
match &self.data { match &self.data {
CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides CalendarObjectComponent::Event(main_event, overrides) => Ok(overrides
.iter() .iter()
.chain([main_event].into_iter()) .chain(std::iter::once(main_event))
.map(super::event::EventObject::get_last_occurence) .map(super::event::EventObject::get_last_occurence)
.collect::<Result<Vec<_>, _>>()? .collect::<Result<Vec<_>, _>>()?
.into_iter() .into_iter()

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
mod timestamp; mod timestamp;
mod timezone; mod timezone;
pub use timestamp::*; pub use timestamp::*;

View File

@@ -3,14 +3,11 @@ use chrono::{DateTime, Datelike, Duration, Local, NaiveDate, NaiveDateTime, Naiv
use chrono_tz::Tz; use chrono_tz::Tz;
use derive_more::derive::Deref; use derive_more::derive::Deref;
use ical::property::Property; use ical::property::Property;
use lazy_static::lazy_static;
use rustical_xml::{ValueDeserialize, ValueSerialize}; use rustical_xml::{ValueDeserialize, ValueSerialize};
use std::{borrow::Cow, collections::HashMap, ops::Add}; use std::{borrow::Cow, collections::HashMap, ops::Add, sync::LazyLock};
lazy_static! { static RE_VCARD_DATE_MM_DD: LazyLock<regex::Regex> =
static ref RE_VCARD_DATE_MM_DD: regex::Regex = LazyLock::new(|| regex::Regex::new(r"^--(?<m>\d{2})(?<d>\d{2})$").unwrap());
regex::Regex::new(r"^--(?<m>\d{2})(?<d>\d{2})$").unwrap();
}
const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S"; const LOCAL_DATE_TIME: &str = "%Y%m%dT%H%M%S";
const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ"; const UTC_DATE_TIME: &str = "%Y%m%dT%H%M%SZ";
@@ -137,9 +134,7 @@ impl CalDateTime {
let prop_value = prop let prop_value = prop
.value .value
.as_ref() .as_ref()
.ok_or(CalDateTimeError::InvalidDatetimeFormat( .ok_or_else(|| CalDateTimeError::InvalidDatetimeFormat("empty property".into()))?;
"empty property".to_owned(),
))?;
let timezone = if let Some(tzid) = prop.get_param("TZID") { let timezone = if let Some(tzid) = prop.get_param("TZID") {
if let Some(timezone) = timezones.get(tzid) { if let Some(timezone) = timezones.get(tzid) {
@@ -158,7 +153,8 @@ impl CalDateTime {
Self::parse(prop_value, timezone) Self::parse(prop_value, timezone)
} }
#[must_use] pub fn format(&self) -> String { #[must_use]
pub fn format(&self) -> String {
match self { match self {
Self::DateTime(datetime) => match datetime.timezone() { Self::DateTime(datetime) => match datetime.timezone() {
ICalTimezone::Olson(chrono_tz::UTC) => datetime.format(UTC_DATE_TIME).to_string(), ICalTimezone::Olson(chrono_tz::UTC) => datetime.format(UTC_DATE_TIME).to_string(),
@@ -168,25 +164,29 @@ impl CalDateTime {
} }
} }
#[must_use] pub fn format_date(&self) -> String { #[must_use]
pub fn format_date(&self) -> String {
match self { match self {
Self::DateTime(datetime) => datetime.format(LOCAL_DATE).to_string(), Self::DateTime(datetime) => datetime.format(LOCAL_DATE).to_string(),
Self::Date(date, _) => date.format(LOCAL_DATE).to_string(), Self::Date(date, _) => date.format(LOCAL_DATE).to_string(),
} }
} }
#[must_use] pub fn date(&self) -> NaiveDate { #[must_use]
pub fn date(&self) -> NaiveDate {
match self { match self {
Self::DateTime(datetime) => datetime.date_naive(), Self::DateTime(datetime) => datetime.date_naive(),
Self::Date(date, _) => date.to_owned(), Self::Date(date, _) => date.to_owned(),
} }
} }
#[must_use] pub const fn is_date(&self) -> bool { #[must_use]
pub const fn is_date(&self) -> bool {
matches!(&self, Self::Date(_, _)) matches!(&self, Self::Date(_, _))
} }
#[must_use] pub fn as_datetime(&self) -> Cow<'_, DateTime<ICalTimezone>> { #[must_use]
pub fn as_datetime(&self) -> Cow<'_, DateTime<ICalTimezone>> {
match self { match self {
Self::DateTime(datetime) => Cow::Borrowed(datetime), Self::DateTime(datetime) => Cow::Borrowed(datetime),
Self::Date(date, tz) => Cow::Owned( Self::Date(date, tz) => Cow::Owned(
@@ -219,8 +219,7 @@ impl CalDateTime {
if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) { if let Ok(datetime) = NaiveDateTime::parse_from_str(value, UTC_DATE_TIME) {
return Ok(datetime.and_utc().into()); return Ok(datetime.and_utc().into());
} }
let timezone = timezone let timezone = timezone.map_or(ICalTimezone::Local, ICalTimezone::Olson);
.map_or(ICalTimezone::Local, ICalTimezone::Olson);
if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) { if let Ok(date) = NaiveDate::parse_from_str(value, LOCAL_DATE) {
return Ok(Self::Date(date, timezone)); return Ok(Self::Date(date, timezone));
} }
@@ -251,7 +250,7 @@ impl CalDateTime {
return Ok(( return Ok((
Self::Date( Self::Date(
NaiveDate::from_ymd_opt(year, month, day) NaiveDate::from_ymd_opt(year, month, day)
.ok_or(CalDateTimeError::ParseError(value.to_string()))?, .ok_or_else(|| CalDateTimeError::ParseError(value.to_string()))?,
ICalTimezone::Local, ICalTimezone::Local,
), ),
false, false,
@@ -260,11 +259,13 @@ impl CalDateTime {
Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string())) Err(CalDateTimeError::InvalidDatetimeFormat(value.to_string()))
} }
#[must_use] pub fn utc(&self) -> DateTime<Utc> { #[must_use]
pub fn utc(&self) -> DateTime<Utc> {
self.as_datetime().to_utc() self.as_datetime().to_utc()
} }
#[must_use] pub fn timezone(&self) -> ICalTimezone { #[must_use]
pub fn timezone(&self) -> ICalTimezone {
match &self { match &self {
Self::DateTime(datetime) => datetime.timezone(), Self::DateTime(datetime) => datetime.timezone(),
Self::Date(_, tz) => tz.to_owned(), Self::Date(_, tz) => tz.to_owned(),
@@ -349,9 +350,7 @@ impl Datelike for CalDateTime {
fn with_month0(&self, month0: u32) -> Option<Self> { fn with_month0(&self, month0: u32) -> Option<Self> {
match &self { match &self {
Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_month0(month0)?)), Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_month0(month0)?)),
Self::Date(date, tz) => { Self::Date(date, tz) => Some(Self::Date(date.with_month0(month0)?, tz.to_owned())),
Some(Self::Date(date.with_month0(month0)?, tz.to_owned()))
}
} }
} }
fn with_day(&self, day: u32) -> Option<Self> { fn with_day(&self, day: u32) -> Option<Self> {
@@ -368,22 +367,14 @@ impl Datelike for CalDateTime {
} }
fn with_ordinal(&self, ordinal: u32) -> Option<Self> { fn with_ordinal(&self, ordinal: u32) -> Option<Self> {
match &self { match &self {
Self::DateTime(datetime) => { Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_ordinal(ordinal)?)),
Some(Self::DateTime(datetime.with_ordinal(ordinal)?)) Self::Date(date, tz) => Some(Self::Date(date.with_ordinal(ordinal)?, tz.to_owned())),
}
Self::Date(date, tz) => {
Some(Self::Date(date.with_ordinal(ordinal)?, tz.to_owned()))
}
} }
} }
fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> { fn with_ordinal0(&self, ordinal0: u32) -> Option<Self> {
match &self { match &self {
Self::DateTime(datetime) => { Self::DateTime(datetime) => Some(Self::DateTime(datetime.with_ordinal0(ordinal0)?)),
Some(Self::DateTime(datetime.with_ordinal0(ordinal0)?)) Self::Date(date, tz) => Some(Self::Date(date.with_ordinal0(ordinal0)?, tz.to_owned())),
}
Self::Date(date, tz) => {
Some(Self::Date(date.with_ordinal0(ordinal0)?, tz.to_owned()))
}
} }
} }
} }

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
use axum::{ use axum::{
Extension, Form, Extension, Form,
extract::Query, extract::Query,

View File

@@ -15,7 +15,6 @@ sha2 = { workspace = true }
ical = { workspace = true } ical = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
regex = { workspace = true } regex = { workspace = true }
lazy_static = { workspace = true }
thiserror = { workspace = true } thiserror = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
chrono-tz = { workspace = true } chrono-tz = { workspace = true }

View File

@@ -14,7 +14,8 @@ pub struct Addressbook {
} }
impl Addressbook { impl Addressbook {
#[must_use] pub fn format_synctoken(&self) -> String { #[must_use]
pub fn format_synctoken(&self) -> String {
format_synctoken(self.synctoken) format_synctoken(self.synctoken)
} }
} }

View File

@@ -53,9 +53,9 @@ impl<S: Clone, AP: AuthenticationProvider> Clone for AuthenticationMiddleware<S,
} }
} }
impl<S: Clone, AP: AuthenticationProvider> Service<Request> for AuthenticationMiddleware<S, AP> impl<S, AP: AuthenticationProvider> Service<Request> for AuthenticationMiddleware<S, AP>
where where
S: Service<Request, Response = Response> + Send + 'static, S: Service<Request, Response = Response> + Send + Clone + 'static,
S::Future: Send + 'static, S::Future: Send + 'static,
{ {
type Response = S::Response; type Response = S::Response;

View File

@@ -35,7 +35,8 @@ impl Principal {
/// Returns true if the user is either /// Returns true if the user is either
/// - the principal itself /// - the principal itself
/// - has full access to the prinicpal (is member) /// - has full access to the prinicpal (is member)
#[must_use] pub fn is_principal(&self, principal: &str) -> bool { #[must_use]
pub fn is_principal(&self, principal: &str) -> bool {
if self.id == principal { if self.id == principal {
return true; return true;
} }

View File

@@ -36,7 +36,8 @@ impl TryFrom<&str> for PrincipalType {
} }
impl PrincipalType { impl PrincipalType {
#[must_use] pub const fn as_str(&self) -> &'static str { #[must_use]
pub const fn as_str(&self) -> &'static str {
match self { match self {
Self::Individual => "INDIVIDUAL", Self::Individual => "INDIVIDUAL",
Self::Group => "GROUP", Self::Group => "GROUP",

View File

@@ -32,17 +32,20 @@ pub struct Calendar {
} }
impl Calendar { impl Calendar {
#[must_use] pub fn format_synctoken(&self) -> String { #[must_use]
pub fn format_synctoken(&self) -> String {
format_synctoken(self.synctoken) format_synctoken(self.synctoken)
} }
#[must_use] pub fn get_timezone(&self) -> Option<chrono_tz::Tz> { #[must_use]
pub fn get_timezone(&self) -> Option<chrono_tz::Tz> {
self.timezone_id self.timezone_id
.as_ref() .as_ref()
.and_then(|tzid| chrono_tz::Tz::from_str(tzid).ok()) .and_then(|tzid| chrono_tz::Tz::from_str(tzid).ok())
} }
#[must_use] pub fn get_vtimezone(&self) -> Option<&'static str> { #[must_use]
pub fn get_vtimezone(&self) -> Option<&'static str> {
self.timezone_id self.timezone_id
.as_ref() .as_ref()
.and_then(|tzid| vtimezones_rs::VTIMEZONES.get(tzid).copied()) .and_then(|tzid| vtimezones_rs::VTIMEZONES.get(tzid).copied())

View File

@@ -20,6 +20,7 @@ impl CombinedCalendarStore {
} }
} }
#[must_use]
pub fn with_store<CS: PrefixedCalendarStore>(mut self, store: Arc<CS>) -> Self { pub fn with_store<CS: PrefixedCalendarStore>(mut self, store: Arc<CS>) -> Self {
let store: Arc<dyn CalendarStore> = store; let store: Arc<dyn CalendarStore> = store;
self.stores.insert(CS::PREFIX, store); self.stores.insert(CS::PREFIX, store);
@@ -30,8 +31,7 @@ impl CombinedCalendarStore {
self.stores self.stores
.iter() .iter()
.find(|&(prefix, _store)| id.starts_with(prefix)) .find(|&(prefix, _store)| id.starts_with(prefix))
.map(|(_prefix, store)| store.clone()) .map_or_else(|| self.default.clone(), |(_prefix, store)| store.clone())
.unwrap_or(self.default.clone())
} }
} }

View File

@@ -30,7 +30,8 @@ pub enum Error {
} }
impl Error { impl Error {
#[must_use] pub const fn status_code(&self) -> StatusCode { #[must_use]
pub const fn status_code(&self) -> StatusCode {
match self { match self {
Self::NotFound => StatusCode::NOT_FOUND, Self::NotFound => StatusCode::NOT_FOUND,
Self::AlreadyExists => StatusCode::CONFLICT, Self::AlreadyExists => StatusCode::CONFLICT,

View File

@@ -1,10 +1,12 @@
const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/"; const SYNC_NAMESPACE: &str = "github.com/lennart-k/rustical/ns/";
#[must_use] pub fn format_synctoken(synctoken: i64) -> String { #[must_use]
pub fn format_synctoken(synctoken: i64) -> String {
format!("{SYNC_NAMESPACE}{synctoken}") format!("{SYNC_NAMESPACE}{synctoken}")
} }
#[must_use] pub fn parse_synctoken(synctoken: &str) -> Option<i64> { #[must_use]
pub fn parse_synctoken(synctoken: &str) -> Option<i64> {
if !synctoken.starts_with(SYNC_NAMESPACE) { if !synctoken.starts_with(SYNC_NAMESPACE) {
return None; return None;
} }

View File

@@ -141,10 +141,12 @@ impl SqliteAddressbookStore {
if use_trashbin { if use_trashbin {
sqlx::query!( sqlx::query!(
r#"UPDATE addressbooks SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#, r#"UPDATE addressbooks SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
principal, addressbook_id principal,
addressbook_id
) )
.execute(executor) .execute(executor)
.await.map_err(crate::Error::from)?; .await
.map_err(crate::Error::from)?;
} else { } else {
sqlx::query!( sqlx::query!(
r#"DELETE FROM addressbooks WHERE (principal, id) = (?, ?)"#, r#"DELETE FROM addressbooks WHERE (principal, id) = (?, ?)"#,
@@ -203,9 +205,7 @@ impl SqliteAddressbookStore {
let mut objects = vec![]; let mut objects = vec![];
let mut deleted_objects = vec![]; let mut deleted_objects = vec![];
let new_synctoken = changes let new_synctoken = changes.last().map_or(0, |&Row { synctoken, .. }| synctoken);
.last()
.map_or(0, |&Row { synctoken, .. }| synctoken);
for Row { object_id, .. } in changes { for Row { object_id, .. } in changes {
match Self::_get_object(&mut *conn, principal, addressbook_id, &object_id, false).await match Self::_get_object(&mut *conn, principal, addressbook_id, &object_id, false).await

View File

@@ -213,21 +213,25 @@ impl SqliteCalendarStore {
id: &str, id: &str,
use_trashbin: bool, use_trashbin: bool,
) -> Result<(), Error> { ) -> Result<(), Error> {
if use_trashbin { sqlx::query!( if use_trashbin {
r#"UPDATE calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#, sqlx::query!(
principal, r#"UPDATE calendars SET deleted_at = datetime() WHERE (principal, id) = (?, ?)"#,
id principal,
) id
.execute(executor) )
.await .execute(executor)
.map_err(crate::Error::from)? } else { sqlx::query!( .await
r#"DELETE FROM calendars WHERE (principal, id) = (?, ?)"#, .map_err(crate::Error::from)?
principal, } else {
id sqlx::query!(
) r#"DELETE FROM calendars WHERE (principal, id) = (?, ?)"#,
.execute(executor) principal,
.await id
.map_err(crate::Error::from)? }; )
.execute(executor)
.await
.map_err(crate::Error::from)?
};
Ok(()) Ok(())
} }
@@ -476,9 +480,7 @@ impl SqliteCalendarStore {
let mut objects = vec![]; let mut objects = vec![];
let mut deleted_objects = vec![]; let mut deleted_objects = vec![];
let new_synctoken = changes let new_synctoken = changes.last().map_or(0, |&Row { synctoken, .. }| synctoken);
.last()
.map_or(0, |&Row { synctoken, .. }| synctoken);
for Row { object_id, .. } in changes { for Row { object_id, .. } in changes {
match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await { match Self::_get_object(&mut *conn, principal, cal_id, &object_id, false).await {

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc)]
pub use error::Error; pub use error::Error;
use serde::Serialize; use serde::Serialize;
use sqlx::{Pool, Sqlite, SqlitePool, sqlite::SqliteConnectOptions}; use sqlx::{Pool, Sqlite, SqlitePool, sqlite::SqliteConnectOptions};
@@ -26,7 +27,8 @@ pub struct SqliteStore {
} }
impl SqliteStore { impl SqliteStore {
#[must_use] pub const fn new(db: SqlitePool) -> Self { #[must_use]
pub const fn new(db: SqlitePool) -> Self {
Self { db } Self { db }
} }
} }

View File

@@ -195,9 +195,8 @@ impl AuthenticationProvider for SqlitePrincipalStore {
Some(user) => user, Some(user) => user,
None => return Ok(None), None => return Ok(None),
}; };
let password = match &user.password { let Some(password) = &user.password else {
Some(password) => password, return Ok(None);
None => return Ok(None),
}; };
if password_auth::verify_password(password_input, password.as_ref()).is_ok() { if password_auth::verify_password(password_input, password.as_ref()).is_ok() {

View File

@@ -18,10 +18,7 @@ pub trait XmlDocument: XmlDeserialize {
fn parse<R: BufRead>(reader: quick_xml::NsReader<R>) -> Result<Self, XmlError>; fn parse<R: BufRead>(reader: quick_xml::NsReader<R>) -> Result<Self, XmlError>;
#[inline] #[inline]
fn parse_reader<R: BufRead>(input: R) -> Result<Self, XmlError> fn parse_reader<R: BufRead>(input: R) -> Result<Self, XmlError> {
where
Self: XmlDeserialize,
{
let mut reader = quick_xml::NsReader::from_reader(input); let mut reader = quick_xml::NsReader::from_reader(input);
reader.config_mut().trim_text(true); reader.config_mut().trim_text(true);
Self::parse(reader) Self::parse(reader)
@@ -43,8 +40,7 @@ impl<T: XmlRootTag + XmlDeserialize> XmlDocument for T {
let event = reader.read_event_into(&mut buf)?; let event = reader.read_event_into(&mut buf)?;
let empty = matches!(event, Event::Empty(_)); let empty = matches!(event, Event::Empty(_));
match event { match event {
Event::Decl(_) => { /* <?xml ... ?> ignore this */ } Event::Decl(_) | Event::Comment(_) => { /* ignore this */ }
Event::Comment(_) => { /* ignore this */ }
Event::Start(start) | Event::Empty(start) => { Event::Start(start) | Event::Empty(start) => {
let (ns, name) = reader.resolve_element(start.name()); let (ns, name) = reader.resolve_element(start.name());
let matches = match (Self::root_ns(), &ns, name) { let matches = match (Self::root_ns(), &ns, name) {

View File

@@ -1,4 +1,5 @@
#![warn(clippy::all, clippy::pedantic, clippy::nursery)] #![warn(clippy::all, clippy::pedantic, clippy::nursery)]
#![allow(clippy::missing_errors_doc)]
use quick_xml::name::Namespace; use quick_xml::name::Namespace;
use std::collections::HashMap; use std::collections::HashMap;
use std::hash::Hash; use std::hash::Hash;

View File

@@ -28,7 +28,8 @@ impl<'a> From<&'a Namespace<'a>> for NamespaceOwned {
} }
impl NamespaceOwned { impl NamespaceOwned {
#[must_use] pub fn as_ref(&self) -> Namespace<'_> { #[must_use]
pub fn as_ref(&self) -> Namespace<'_> {
Namespace(&self.0) Namespace(&self.0)
} }
} }

View File

@@ -26,11 +26,8 @@ impl<T: XmlSerialize> XmlSerialize for Option<T> {
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
if let Some(some) = self { self.as_ref()
some.serialize(ns, tag, namespaces, writer) .map_or(Ok(()), |some| some.serialize(ns, tag, namespaces, writer))
} else {
Ok(())
}
} }
fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>> { fn attributes<'a>(&self) -> Option<Vec<Attribute<'a>>> {
@@ -64,15 +61,13 @@ impl XmlSerialize for () {
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns.and_then(|ns| namespaces.get(&ns)).map(|prefix| {
.and_then(|ns| namespaces.get(&ns)) if prefix.is_empty() {
.map(|prefix| { String::new()
if prefix.is_empty() { } else {
String::new() [*prefix, ":"].concat()
} else { }
[*prefix, ":"].concat() });
}
});
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
if let Some(tagname) = tagname.as_ref() { if let Some(tagname) = tagname.as_ref() {

View File

@@ -9,7 +9,8 @@ use crate::{XmlDeserialize, XmlError};
pub struct Unparsed(BytesStart<'static>); pub struct Unparsed(BytesStart<'static>);
impl Unparsed { impl Unparsed {
#[must_use] pub fn tag_name(&self) -> String { #[must_use]
pub fn tag_name(&self) -> String {
// TODO: respect namespace? // TODO: respect namespace?
String::from_utf8_lossy(self.0.local_name().as_ref()).to_string() String::from_utf8_lossy(self.0.local_name().as_ref()).to_string()
} }

View File

@@ -114,15 +114,13 @@ impl<T: ValueSerialize> XmlSerialize for T {
namespaces: &HashMap<Namespace, &str>, namespaces: &HashMap<Namespace, &str>,
writer: &mut quick_xml::Writer<&mut Vec<u8>>, writer: &mut quick_xml::Writer<&mut Vec<u8>>,
) -> std::io::Result<()> { ) -> std::io::Result<()> {
let prefix = ns let prefix = ns.and_then(|ns| namespaces.get(&ns)).map(|prefix| {
.and_then(|ns| namespaces.get(&ns)) if prefix.is_empty() {
.map(|prefix| { String::new()
if prefix.is_empty() { } else {
String::new() [*prefix, ":"].concat()
} else { }
[*prefix, ":"].concat() });
}
});
let has_prefix = prefix.is_some(); let has_prefix = prefix.is_some();
let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat()); let tagname = tag.map(|tag| [&prefix.unwrap_or_default(), tag].concat());
if let Some(tagname) = tagname.as_ref() { if let Some(tagname) = tagname.as_ref() {

View File

@@ -28,7 +28,11 @@ use tower_sessions::{Expiry, MemoryStore, SessionManagerLayer};
use tracing::Span; use tracing::Span;
use tracing::field::display; use tracing::field::display;
#[allow(clippy::too_many_arguments)] #[allow(
clippy::too_many_arguments,
clippy::too_many_lines,
clippy::cognitive_complexity
)]
pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>( pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
addr_store: Arc<AS>, addr_store: Arc<AS>,
cal_store: Arc<CS>, cal_store: Arc<CS>,
@@ -36,7 +40,7 @@ pub fn make_app<AS: AddressbookStore, CS: CalendarStore, S: SubscriptionStore>(
auth_provider: Arc<impl AuthenticationProvider>, auth_provider: Arc<impl AuthenticationProvider>,
frontend_config: FrontendConfig, frontend_config: FrontendConfig,
oidc_config: Option<OidcConfig>, oidc_config: Option<OidcConfig>,
nextcloud_login_config: NextcloudLoginConfig, nextcloud_login_config: &NextcloudLoginConfig,
dav_push_enabled: bool, dav_push_enabled: bool,
session_cookie_samesite_strict: bool, session_cookie_samesite_strict: bool,
payload_limit_mb: usize, payload_limit_mb: usize,

View File

@@ -115,7 +115,7 @@ async fn main() -> Result<()> {
principal_store.clone(), principal_store.clone(),
config.frontend.clone(), config.frontend.clone(),
config.oidc.clone(), config.oidc.clone(),
config.nextcloud_login.clone(), &config.nextcloud_login,
config.dav_push.enabled, config.dav_push.enabled,
config.http.session_cookie_samesite_strict, config.http.session_cookie_samesite_strict,
config.http.payload_limit_mb, config.http.payload_limit_mb,