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:
@@ -124,6 +124,41 @@ pub enum LockType {
|
||||
pub struct WebDavAdapter;
|
||||
|
||||
impl WebDavAdapter {
|
||||
/// Collect namespace prefix → URI mappings from element attributes.
|
||||
/// E.g. `xmlns:D="DAV:"` maps prefix `"D"` to `"DAV:"`.
|
||||
pub fn collect_ns_decls(
|
||||
e: &BytesStart,
|
||||
ns_map: &mut std::collections::HashMap<String, String>,
|
||||
) {
|
||||
for attr in e.attributes().flatten() {
|
||||
let key = std::str::from_utf8(attr.key.as_ref()).unwrap_or("");
|
||||
if let Some(prefix) = key.strip_prefix("xmlns:") {
|
||||
let uri = attr.unescape_value().unwrap_or_default().to_string();
|
||||
ns_map.insert(prefix.to_string(), uri);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Resolve a prefixed element name (e.g. `D:resourcetype`) to a
|
||||
/// `QualifiedName` using the accumulated namespace declarations.
|
||||
pub fn resolve_name(
|
||||
name_str: &str,
|
||||
ns_map: &std::collections::HashMap<String, String>,
|
||||
) -> QualifiedName {
|
||||
if let Some(idx) = name_str.find(':') {
|
||||
let prefix = &name_str[..idx];
|
||||
let local = &name_str[idx + 1..];
|
||||
if let Some(uri) = ns_map.get(prefix) {
|
||||
return QualifiedName::new(uri.clone(), local.to_string());
|
||||
}
|
||||
}
|
||||
// Fallback: no prefix or unknown prefix → use legacy extraction
|
||||
QualifiedName::new(
|
||||
Self::extract_namespace(name_str),
|
||||
Self::extract_local_name(name_str),
|
||||
)
|
||||
}
|
||||
|
||||
/// Parse a PROPFIND XML request
|
||||
pub fn parse_propfind<R: Read>(reader: R) -> Result<PropFindRequest> {
|
||||
let mut xml_reader = Reader::from_reader(BufReader::new(reader));
|
||||
@@ -135,10 +170,12 @@ impl WebDavAdapter {
|
||||
let mut in_allprop = false;
|
||||
let mut in_propname = false;
|
||||
let mut props = Vec::new();
|
||||
let mut ns_map = std::collections::HashMap::<String, String>::new();
|
||||
|
||||
loop {
|
||||
match xml_reader.read_event_into(&mut buffer) {
|
||||
Ok(Event::Start(ref e)) => {
|
||||
Self::collect_ns_decls(e, &mut ns_map);
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
@@ -155,11 +192,8 @@ impl WebDavAdapter {
|
||||
{
|
||||
in_propname = true;
|
||||
} else if in_prop {
|
||||
// Add property to request
|
||||
let namespace = Self::extract_namespace(name_str);
|
||||
let prop_name = Self::extract_local_name(name_str);
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
let qname = Self::resolve_name(name_str, &ns_map);
|
||||
props.push(qname);
|
||||
}
|
||||
}
|
||||
Ok(Event::End(ref e)) => {
|
||||
@@ -177,6 +211,7 @@ impl WebDavAdapter {
|
||||
}
|
||||
}
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
Self::collect_ns_decls(e, &mut ns_map);
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
@@ -187,11 +222,8 @@ impl WebDavAdapter {
|
||||
{
|
||||
in_propname = true;
|
||||
} else if in_prop {
|
||||
// Add property to request (empty element)
|
||||
let namespace = Self::extract_namespace(name_str);
|
||||
let prop_name = Self::extract_local_name(name_str);
|
||||
|
||||
props.push(QualifiedName::new(namespace, prop_name));
|
||||
let qname = Self::resolve_name(name_str, &ns_map);
|
||||
props.push(qname);
|
||||
}
|
||||
}
|
||||
Ok(Event::Eof) => break,
|
||||
@@ -635,10 +667,12 @@ impl WebDavAdapter {
|
||||
let mut props_to_set = Vec::new();
|
||||
let mut props_to_remove = Vec::new();
|
||||
let mut current_text = String::new();
|
||||
let mut ns_map = std::collections::HashMap::<String, String>::new();
|
||||
|
||||
loop {
|
||||
match xml_reader.read_event_into(&mut buffer) {
|
||||
Ok(Event::Start(ref e)) => {
|
||||
Self::collect_ns_decls(e, &mut ns_map);
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
@@ -656,11 +690,7 @@ impl WebDavAdapter {
|
||||
in_prop = true
|
||||
}
|
||||
_ if in_prop => {
|
||||
// This is a property element
|
||||
let namespace = Self::extract_namespace(name_str);
|
||||
let prop_name = Self::extract_local_name(name_str);
|
||||
|
||||
current_prop = Some(QualifiedName::new(namespace, prop_name));
|
||||
current_prop = Some(Self::resolve_name(name_str, &ns_map));
|
||||
current_text.clear();
|
||||
}
|
||||
_ => (),
|
||||
@@ -704,15 +734,12 @@ impl WebDavAdapter {
|
||||
}
|
||||
}
|
||||
Ok(Event::Empty(ref e)) => {
|
||||
Self::collect_ns_decls(e, &mut ns_map);
|
||||
let name = e.name();
|
||||
let name_str = std::str::from_utf8(name.as_ref()).unwrap_or("");
|
||||
|
||||
if in_prop {
|
||||
// Empty property element
|
||||
let namespace = Self::extract_namespace(name_str);
|
||||
let prop_name = Self::extract_local_name(name_str);
|
||||
|
||||
let qname = QualifiedName::new(namespace, prop_name);
|
||||
let qname = Self::resolve_name(name_str, &ns_map);
|
||||
|
||||
if in_set {
|
||||
props_to_set.push(PropValue {
|
||||
|
||||
Reference in New Issue
Block a user