use http::Method;
use crate::{
Depth, PropertyName,
dav::WebDavError,
names,
requests::{DavRequest, ParseResponseError, PreparedRequest, xml_content_type_header},
xmlutils::XmlNode,
};
pub struct Propfind<'a> {
href: &'a str,
properties: Vec<&'a PropertyName<'a, 'a>>,
depth: Depth,
}
impl<'a> Propfind<'a> {
#[must_use]
pub fn new(href: &'a str) -> Self {
Self {
href,
properties: Vec::new(),
depth: Depth::Zero,
}
}
#[must_use]
pub fn with_properties(mut self, properties: &[&'a PropertyName<'a, 'a>]) -> Self {
self.properties = properties.to_vec();
self
}
#[must_use]
pub fn with_depth(mut self, depth: Depth) -> Self {
self.depth = depth;
self
}
#[must_use]
pub fn properties(&self) -> &[&'a PropertyName<'a, 'a>] {
&self.properties
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PropfindResponse {
pub body: Vec<u8>,
}
impl PropfindResponse {
pub fn xml_tree(&self) -> Result<roxmltree::Document<'_>, ParseResponseError> {
let body = std::str::from_utf8(&self.body)?;
let doc = roxmltree::Document::parse(body)?;
Ok(doc)
}
}
impl DavRequest for Propfind<'_> {
type Response = PropfindResponse;
type ParseError = ParseResponseError;
type Error<E> = WebDavError<E>;
fn prepare_request(&self) -> Result<PreparedRequest, http::Error> {
let mut propfind = XmlNode::new(&names::PROPFIND);
propfind.children = if self.properties.is_empty() {
vec![XmlNode::new(&names::ALLPROP)]
} else {
let mut prop = XmlNode::new(&names::PROP);
prop.children = self.properties.iter().map(|p| XmlNode::new(p)).collect();
vec![prop]
};
Ok(PreparedRequest {
method: Method::from_bytes(b"PROPFIND")?,
path: self.href.to_string(),
body: propfind.render_node(),
headers: vec![
("Depth".to_string(), self.depth.to_string()),
xml_content_type_header(),
],
})
}
fn parse_response(
&self,
parts: &http::response::Parts,
body: &[u8],
) -> Result<Self::Response, ParseResponseError> {
if !parts.status.is_success() {
return Err(ParseResponseError::BadStatusCode(parts.status));
}
Ok(PropfindResponse {
body: body.to_vec(),
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::names;
use http::StatusCode;
#[test]
fn test_prepare_request_allprop() {
let req = Propfind::new("/calendars/personal/");
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert_eq!(prepared.path, "/calendars/personal/");
assert!(prepared.body.contains("allprop"));
assert!(prepared.body.contains(r#"xmlns:D="DAV:""#));
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "0".to_string()))
);
}
#[test]
fn test_prepare_request_with_properties() {
let req = Propfind::new("/calendars/personal/")
.with_properties(&[&names::DISPLAY_NAME, &names::GETETAG]);
let prepared = req.prepare_request().unwrap();
assert_eq!(prepared.method, Method::from_bytes(b"PROPFIND").unwrap());
assert_eq!(prepared.path, "/calendars/personal/");
assert!(prepared.body.contains("displayname"));
assert!(prepared.body.contains("getetag"));
assert!(!prepared.body.contains("allprop"));
}
#[test]
fn test_prepare_request_depth_one() {
let req = Propfind::new("/calendars/").with_depth(Depth::One);
let prepared = req.prepare_request().unwrap();
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "1".to_string()))
);
}
#[test]
fn test_prepare_request_depth_infinity() {
let req = Propfind::new("/calendars/").with_depth(Depth::Infinity);
let prepared = req.prepare_request().unwrap();
assert!(
prepared
.headers
.contains(&("Depth".to_string(), "infinity".to_string()))
);
}
#[test]
fn test_parse_response_success() {
let req = Propfind::new("/calendars/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname>Personal</displayname>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, body).unwrap();
assert_eq!(result.body, body.to_vec());
}
#[test]
fn test_parse_response_bad_status() {
let req = Propfind::new("/calendars/personal/");
let response = http::Response::builder()
.status(StatusCode::FORBIDDEN)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"");
assert!(matches!(
result,
Err(ParseResponseError::BadStatusCode(StatusCode::FORBIDDEN))
));
}
#[test]
fn test_parse_response_not_found() {
let req = Propfind::new("/calendars/nonexistent/");
let response = http::Response::builder()
.status(StatusCode::NOT_FOUND)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, b"");
assert!(matches!(
result,
Err(ParseResponseError::BadStatusCode(StatusCode::NOT_FOUND))
));
}
#[test]
fn test_xml_tree_parsing() {
let req = Propfind::new("/calendars/personal/");
let body = br#"<?xml version="1.0" encoding="utf-8"?>
<multistatus xmlns="DAV:">
<response>
<href>/calendars/personal/</href>
<propstat>
<prop>
<displayname>Personal</displayname>
</prop>
<status>HTTP/1.1 200 OK</status>
</propstat>
</response>
</multistatus>"#;
let response = http::Response::builder()
.status(StatusCode::MULTI_STATUS)
.body(())
.unwrap();
let (parts, ()) = response.into_parts();
let result = req.parse_response(&parts, body).unwrap();
let doc = result.xml_tree().unwrap();
assert_eq!(doc.root_element().tag_name().name(), "multistatus");
}
#[test]
fn test_xml_tree_invalid_utf8() {
let invalid_utf8 = vec![0xFF, 0xFE];
let resp = PropfindResponse { body: invalid_utf8 };
assert!(resp.xml_tree().is_err());
}
#[test]
fn test_xml_tree_malformed_xml() {
let malformed = b"<not valid xml";
let resp = PropfindResponse {
body: malformed.to_vec(),
};
assert!(resp.xml_tree().is_err());
}
}