feat(webdav): RFC 4918 prop classification and 404 propstat

This commit is contained in:
M.Schmidt
2026-06-24 23:08:51 +02:00
parent f9999cdd0f
commit 5913fff047
3 changed files with 499 additions and 276 deletions
+125 -86
View File
@@ -301,6 +301,63 @@ impl WebDavAdapter {
Ok(PropFindRequest { prop_find_type })
}
fn folder_prop_is_known(prop: &QualifiedName) -> bool {
prop.namespace == "DAV:"
&& matches!(
prop.name.as_str(),
"resourcetype"
| "displayname"
| "creationdate"
| "getlastmodified"
| "getetag"
| "getcontentlength"
| "getcontenttype"
)
}
fn file_prop_is_known(prop: &QualifiedName) -> bool {
prop.namespace == "DAV:"
&& matches!(
prop.name.as_str(),
"resourcetype"
| "displayname"
| "getcontenttype"
| "getcontentlength"
| "creationdate"
| "getlastmodified"
| "getetag"
)
}
/// Write a 404 propstat block for unknown properties (RFC 4918 §9.2).
fn write_unknown_props_404<W: Write>(
xml_writer: &mut Writer<W>,
unknown: &[&QualifiedName],
) -> Result<()> {
if unknown.is_empty() {
return Ok(());
}
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
for prop in unknown {
if prop.namespace == "DAV:" {
xml_writer
.write_event(Event::Empty(BytesStart::new(format!("D:{}", prop.name))))?;
} else {
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
"{}:{}",
prop.namespace, prop.name
))))?;
}
}
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 404 Not Found")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
Ok(())
}
/// Write folder properties as a response
fn write_folder_response<W: Write>(
xml_writer: &mut Writer<W>,
@@ -308,50 +365,50 @@ impl WebDavAdapter {
request: &PropFindRequest,
href: &str,
) -> Result<()> {
// Start response element
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
// Write href
xml_writer.write_event(Event::Start(BytesStart::new("D:href")))?;
xml_writer.write_event(Event::Text(BytesText::new(href)))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:href")))?;
// Write propstat
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
// Start prop
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
// Write properties based on request type
match &request.prop_find_type {
PropFindType::AllProp => {
// Write all standard properties for a folder
Self::write_folder_standard_props(xml_writer, folder)?;
}
PropFindType::PropName => {
// Write only property names (empty elements)
Self::write_folder_prop_names(xml_writer)?;
}
PropFindType::Prop(props) => {
// Write requested properties
Self::write_folder_requested_props(xml_writer, folder, props)?;
// RFC 4918 §9.2: known props → 200 propstat; unknown → 404 propstat.
let (known, unknown): (Vec<_>, Vec<_>) =
props.iter().partition(|p| Self::folder_prop_is_known(p));
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
Self::write_folder_requested_props(xml_writer, folder, &known)?;
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
Self::write_unknown_props_404(xml_writer, &unknown)?;
}
other => {
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
match other {
PropFindType::AllProp => {
Self::write_folder_standard_props(xml_writer, folder)?;
}
PropFindType::PropName => {
Self::write_folder_prop_names(xml_writer)?;
}
PropFindType::Prop(_) => unreachable!(),
}
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
}
}
// End prop
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
// Write status
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
// End propstat
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
// End response
xml_writer.write_event(Event::End(BytesEnd::new("D:response")))?;
Ok(())
}
@@ -362,50 +419,50 @@ impl WebDavAdapter {
request: &PropFindRequest,
href: &str,
) -> Result<()> {
// Start response element
xml_writer.write_event(Event::Start(BytesStart::new("D:response")))?;
// Write href
xml_writer.write_event(Event::Start(BytesStart::new("D:href")))?;
xml_writer.write_event(Event::Text(BytesText::new(href)))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:href")))?;
// Write propstat
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
// Start prop
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
// Write properties based on request type
match &request.prop_find_type {
PropFindType::AllProp => {
// Write all standard properties for a file
Self::write_file_standard_props(xml_writer, file)?;
}
PropFindType::PropName => {
// Write only property names (empty elements)
Self::write_file_prop_names(xml_writer)?;
}
PropFindType::Prop(props) => {
// Write requested properties
Self::write_file_requested_props(xml_writer, file, props)?;
// RFC 4918 §9.2: known props → 200 propstat; unknown → 404 propstat.
let (known, unknown): (Vec<_>, Vec<_>) =
props.iter().partition(|p| Self::file_prop_is_known(p));
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
Self::write_file_requested_props(xml_writer, file, &known)?;
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
Self::write_unknown_props_404(xml_writer, &unknown)?;
}
other => {
xml_writer.write_event(Event::Start(BytesStart::new("D:propstat")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:prop")))?;
match other {
PropFindType::AllProp => {
Self::write_file_standard_props(xml_writer, file)?;
}
PropFindType::PropName => {
Self::write_file_prop_names(xml_writer)?;
}
PropFindType::Prop(_) => unreachable!(),
}
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
}
}
// End prop
xml_writer.write_event(Event::End(BytesEnd::new("D:prop")))?;
// Write status
xml_writer.write_event(Event::Start(BytesStart::new("D:status")))?;
xml_writer.write_event(Event::Text(BytesText::new("HTTP/1.1 200 OK")))?;
xml_writer.write_event(Event::End(BytesEnd::new("D:status")))?;
// End propstat
xml_writer.write_event(Event::End(BytesEnd::new("D:propstat")))?;
// End response
xml_writer.write_event(Event::End(BytesEnd::new("D:response")))?;
Ok(())
}
@@ -611,20 +668,11 @@ impl WebDavAdapter {
xml_writer.write_event(Event::End(BytesEnd::new("D:getcontenttype")))?;
}
_ => {
// Property not supported - write empty element
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
"D:{}",
prop.name
))))?;
// Unknown prop — skipped here; caller writes 404 propstat.
}
}
} else {
// Non-DAV namespace, not supported
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
"{}:{}",
prop.namespace, prop.name
))))?;
}
// Non-DAV namespace props are unknown — skipped; caller writes 404 propstat.
}
Ok(())
@@ -694,20 +742,11 @@ impl WebDavAdapter {
xml_writer.write_event(Event::End(BytesEnd::new("D:getetag")))?;
}
_ => {
// Property not supported - write empty element
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
"D:{}",
prop.name
))))?;
// Unknown prop — skipped here; caller writes 404 propstat.
}
}
} else {
// Non-DAV namespace, not supported
xml_writer.write_event(Event::Empty(BytesStart::new(format!(
"{}:{}",
prop.namespace, prop.name
))))?;
}
// Non-DAV namespace props are unknown — skipped; caller writes 404 propstat.
}
Ok(())