fix(webdav): achieve 100% litmus compliance (59/59 tests)

Complete RFC 4918 litmus test suite compliance (basic 16/16,
copymove 13/13, props 30/30) by fixing six categories of failures:

- PropPatchOp: parse PROPPATCH in document order (RFC 4918 §9.2)
  so that remove-then-set and set-then-remove yield different results
- Null namespace: resolve_name now correctly maps xmlns="" to empty-ns
  QualifiedName instead of falling through to DAV: namespace
- Unicode chars: handle quick-xml 0.39's Event::GeneralRef for character
  references (𐀀) — the parser emits these as GeneralRef, not Text
- MOVE preserves dead props: call rename_resource on MOVE, clearing
  stale destination data even when source has no registered properties
- Malformed PROPFIND: return 400 when body lacks a complete <propfind>
  element (test 2: bare <foo>, test 3: invalid xmlns:prefix="" binding)
- PROPPATCH document order: process ops via Vec<PropPatchOp> instead of
  separate (sets, removes) to honour interleaved remove/set sequences
This commit is contained in:
M.Schmidt
2026-06-30 08:58:13 +02:00
parent 43cf4a2b2b
commit 6ca6649e50
4 changed files with 210 additions and 115 deletions
+21 -16
View File
@@ -850,11 +850,10 @@ async fn handle_proppatch(
.await
.map_err(|e| AppError::bad_request(format!("Failed to read request body: {}", e)))?;
let (props_to_set, props_to_remove) =
crate::application::adapters::webdav_adapter::WebDavAdapter::parse_proppatch(
body_bytes.reader(),
)
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPPATCH: {}", e)))?;
let ops = crate::application::adapters::webdav_adapter::WebDavAdapter::parse_proppatch(
body_bytes.reader(),
)
.map_err(|e| AppError::bad_request(format!("Failed to parse PROPPATCH: {}", e)))?;
let effective_path = strip_username_prefix(path);
let calendar_id = effective_path.split('/').next().unwrap_or(effective_path);
@@ -870,12 +869,14 @@ async fn handle_proppatch(
is_public: None,
};
for prop in &props_to_set {
match prop.name.name.as_str() {
"displayname" => update.name = Some(prop.value.clone().unwrap_or_default()),
"calendar-description" => update.description = prop.value.clone(),
"calendar-color" => update.color = prop.value.clone(),
_ => {}
for op in &ops {
if let crate::application::adapters::webdav_adapter::PropPatchOp::Set(prop) = op {
match prop.name.name.as_str() {
"displayname" => update.name = Some(prop.value.clone().unwrap_or_default()),
"calendar-description" => update.description = prop.value.clone(),
"calendar-color" => update.color = prop.value.clone(),
_ => {}
}
}
}
@@ -887,11 +888,15 @@ async fn handle_proppatch(
}
let mut results = Vec::new();
for prop in &props_to_set {
results.push((&prop.name, true));
}
for prop in &props_to_remove {
results.push((prop, true));
for op in &ops {
match op {
crate::application::adapters::webdav_adapter::PropPatchOp::Set(prop) => {
results.push((&prop.name, true));
}
crate::application::adapters::webdav_adapter::PropPatchOp::Remove(name) => {
results.push((name, true));
}
}
}
let href = format!("/caldav/{}", path);