fix: resolve CalDAV PROPFIND returning empty property values (#153)

Root cause: XML namespace prefixes (e.g. "D", "C") were not being
resolved to their actual namespace URIs (e.g. "DAV:",
"urn:ietf:params:xml:ns:caldav") during PROPFIND parsing. This caused
all property match arms to fall through to the catch-all, producing
empty XML elements.

Changes:
- Add namespace-aware XML parsing (collect_ns_decls + resolve_name) to
  WebDavAdapter, used by all DAV protocol parsers (WebDAV, CalDAV,
  CardDAV)
- Add /.well-known/caldav -> /caldav/ redirect (RFC 6764)
- Add root /caldav/ PROPFIND response with current-user-principal and
  calendar-home-set discovery properties
- Add /caldav/principals/{username}/ PROPFIND handler (was 500 error)
- Add /caldav/{username}/ user calendar home handler (calendar-home-set
  target)
- Respect Depth header at root: depth 0 returns only root entry, depth
  1+ includes calendar children
- Fix pre-existing TRANSCODE_POOL_THREADS test compilation error
- Add 8 new tests covering namespace resolution, discovery properties,
  and principal responses

https://claude.ai/code/session_01T49VBJSimgo28APxbucHzq
This commit is contained in:
Claude
2026-03-02 20:40:41 +00:00
parent 991a98b1fc
commit b9de342223
7 changed files with 879 additions and 81 deletions
+7 -6
View File
@@ -53,10 +53,12 @@ impl CardDavAdapter {
let mut sync_token = String::new();
let mut in_href = false;
let mut in_sync_token = false;
let mut ns_map = std::collections::HashMap::<String, String>::new();
loop {
match xml_reader.read_event_into(&mut buffer) {
Ok(Event::Start(ref e)) => {
WebDavAdapter::collect_ns_decls(e, &mut ns_map);
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
@@ -78,9 +80,8 @@ impl CardDavAdapter {
in_sync_token = true
}
_ if in_prop => {
let namespace = WebDavAdapter::extract_namespace(name_str);
let prop_name = WebDavAdapter::extract_local_name(name_str);
props.push(QualifiedName::new(namespace, prop_name));
let qname = WebDavAdapter::resolve_name(name_str, &ns_map);
props.push(qname);
}
_ => {}
}
@@ -107,11 +108,11 @@ impl CardDavAdapter {
}
}
Ok(Event::Empty(ref e)) if in_prop => {
WebDavAdapter::collect_ns_decls(e, &mut ns_map);
let name = e.name();
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
let namespace = WebDavAdapter::extract_namespace(name_str);
let prop_name = WebDavAdapter::extract_local_name(name_str);
props.push(QualifiedName::new(namespace, prop_name));
let qname = WebDavAdapter::resolve_name(name_str, &ns_map);
props.push(qname);
}
Ok(Event::Eof) => break,
Err(e) => return Err(WebDavError::XmlError(e)),