use std::borrow::Cow;
use std::str::FromStr;
use http::status::InvalidStatusCode;
use http::StatusCode;
use percent_encoding::percent_encode;
use percent_encoding::{percent_decode_str, AsciiSet, NON_ALPHANUMERIC};
use roxmltree::Node;
use crate::dav::{check_status, WebDavError};
use crate::names;
use crate::PropertyName;
pub const DISALLOWED_FOR_HREF: &AsciiSet = &NON_ALPHANUMERIC.remove(b'/').remove(b'.');
pub fn check_multistatus(root: Node) -> Result<(), WebDavError> {
let statuses = root
.descendants()
.filter(|node| node.tag_name() == names::STATUS);
for status in statuses {
let status = status.text().ok_or(WebDavError::InvalidResponse(
"missing text inside 'DAV:status'".into(),
))?;
check_status(parse_statusline(status)?)?;
}
Ok(())
}
pub fn parse_statusline(status_line: impl AsRef<str>) -> Result<StatusCode, InvalidStatusCode> {
let mut iter = status_line.as_ref().splitn(3, ' ');
iter.next();
let code = iter.next().unwrap_or("");
StatusCode::from_str(code)
}
pub(crate) fn render_xml(name: &PropertyName) -> String {
format!("<{0} xmlns=\"{1}\"/>", name.name(), name.namespace())
}
pub fn render_xml_with_text(name: &PropertyName, text: Option<impl AsRef<str>>) -> String {
match text {
None => format!("<{0} xmlns=\"{1}\"/>", name.name(), name.namespace()),
Some(t) => format!(
"<{0} xmlns=\"{1}\">{2}</{0}>",
name.name(),
name.namespace(),
escape_text(t.as_ref())
),
}
}
#[must_use]
#[allow(clippy::missing_panics_doc)]
pub fn escape_text(raw: &str) -> Cow<str> {
{
let bytes = raw.as_bytes();
let mut escaped = None;
let mut iter = bytes.iter();
let mut pos = 0;
while let Some(i) = iter.position(|&b| matches!(b, b'<' | b'>' | b'&')) {
let escaped = escaped.get_or_insert_with(|| Vec::with_capacity(raw.len()));
let new_pos = pos + i;
escaped.extend_from_slice(&bytes[pos..new_pos]);
match bytes[new_pos] {
b'<' => escaped.extend_from_slice(b"<"),
b'>' => escaped.extend_from_slice(b">"),
b'&' => escaped.extend_from_slice(b"&"),
_ => unreachable!("Only '<', '>' and '&', are escaped"),
}
pos = new_pos + 1;
}
if let Some(mut escaped) = escaped {
if let Some(raw) = bytes.get(pos..) {
escaped.extend_from_slice(raw);
}
Cow::Owned(
String::from_utf8(escaped).expect("manually escaped string must be valid utf-8"),
)
} else {
Cow::Borrowed(raw)
}
}
}
pub(crate) fn get_unquoted_href<'a>(node: &'a Node) -> Result<Cow<'a, str>, WebDavError> {
Ok(node
.descendants()
.find(|node| node.tag_name() == crate::names::HREF)
.ok_or(WebDavError::InvalidResponse(
"missing href in response".into(),
))?
.text()
.map(percent_decode_str)
.ok_or(WebDavError::InvalidResponse("missing text in href".into()))?
.decode_utf8()?)
}
pub(crate) fn quote_href(href: &[u8]) -> Cow<'_, str> {
Cow::from(percent_encode(href, DISALLOWED_FOR_HREF))
}
#[inline]
pub(crate) fn get_newline_corrected_text(
node: &Node,
property: &PropertyName<'_, '_>,
) -> Result<String, WebDavError> {
node.descendants()
.find(|node| node.tag_name() == *property)
.ok_or(WebDavError::InvalidResponse(
format!("missing {} in response", property.name()).into(),
))?
.text()
.ok_or(WebDavError::InvalidResponse(
format!("missing text in property {property:?}").into(),
))
.map(normalise_newlines)
}
#[must_use]
pub fn normalise_newlines(orig: &str) -> String {
let mut result = String::new();
let mut last_end = 0;
for (start, part) in orig.match_indices('\n') {
let line = &orig[last_end..start];
result.push_str(line.strip_suffix('\r').unwrap_or(line));
result.push_str("\r\n");
last_end = start + part.len();
}
result.push_str(&orig[last_end..orig.len()]);
result
}
#[cfg(test)]
mod test {
use std::borrow::Cow;
use crate::{
names,
xmlutils::{escape_text, get_newline_corrected_text},
};
#[test]
fn test_escape_text() {
match escape_text("HELLO THERE") {
Cow::Borrowed(s) => assert_eq!(s, "HELLO THERE"),
Cow::Owned(_) => panic!("expected Borrowed, got Owned"),
}
match escape_text("HELLO <") {
Cow::Borrowed(_) => panic!("expected Owned, got Borrowed"),
Cow::Owned(s) => assert_eq!(s, "HELLO <"),
}
match escape_text("HELLO <") {
Cow::Borrowed(_) => panic!("expected Owned, got Borrowed"),
Cow::Owned(s) => assert_eq!(s, "HELLO &lt;"),
}
match escape_text("你吃过了吗?") {
Cow::Borrowed(s) => assert_eq!(s, "你吃过了吗?"),
Cow::Owned(_) => panic!("expected Borrowed, got Owned"),
}
}
#[test]
fn test_get_newline_corrected_text_without_returns() {
let without_returns ="<ns0:multistatus xmlns:ns0=\"DAV:\" xmlns:ns1=\"urn:ietf:params:xml:ns:caldav\"><ns0:response><ns0:href>/user/calendars/qdBEnN9jwjQFLry4/1ehsci7nhH31.ics</ns0:href><ns0:propstat><ns0:status>HTTP/1.1 200 OK</ns0:status><ns0:prop><ns0:getetag>\"2d2c827debd802fb3844309b53254b90dd7fd900\"</ns0:getetag><ns1:calendar-data>BEGIN:VCALENDAR\nVERSION:2.0\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN\nBEGIN:VEVENT\nSUMMARY:hello\\, testing\nDTSTART:19970714T170000Z\nDTSTAMP:19970610T172345Z\nUID:92gDWceCowpO\nEND:VEVENT\nEND:VCALENDAR\n</ns1:calendar-data></ns0:prop></ns0:propstat></ns0:response></ns0:multistatus>";
let expected = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\nBEGIN:VEVENT\r\nSUMMARY:hello\\, testing\r\nDTSTART:19970714T170000Z\r\nDTSTAMP:19970610T172345Z\r\nUID:92gDWceCowpO\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
let doc = roxmltree::Document::parse(without_returns).unwrap();
let responses = doc
.root_element()
.descendants()
.find(|node| node.tag_name() == names::RESPONSE)
.unwrap();
assert_eq!(
get_newline_corrected_text(&responses, &names::CALENDAR_DATA).unwrap(),
expected
);
}
#[test]
fn test_get_newline_corrected_text_with_returns() {
let with_returns= "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<multistatus xmlns=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\">\n <response>\n <href>/dav/calendars/user/vdirsyncer@fastmail.com/UvrlExcG9Jp0gEzQ/2H8kQfNQj8GP.ics</href>\n <propstat>\n <prop>\n <getetag>\"4d92fc1c8bdc18bbf83caf34eeab7e7167eb292e\"</getetag>\n <C:calendar-data><![CDATA[BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\nBEGIN:VEVENT\r\nUID:jSayX7OSdp3V\r\nDTSTAMP:19970610T172345Z\r\nDTSTART:19970714T170000Z\r\nSUMMARY:hello\\, testing\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n]]></C:calendar-data>\n </prop>\n <status>HTTP/1.1 200 OK</status>\n </propstat>\n </response>\n</multistatus>\n";
let expected = "BEGIN:VCALENDAR\r\nVERSION:2.0\r\nPRODID:-//hacksw/handcal//NONSGML v1.0//EN\r\nBEGIN:VEVENT\r\nUID:jSayX7OSdp3V\r\nDTSTAMP:19970610T172345Z\r\nDTSTART:19970714T170000Z\r\nSUMMARY:hello\\, testing\r\nEND:VEVENT\r\nEND:VCALENDAR\r\n";
let doc = roxmltree::Document::parse(with_returns).unwrap();
let responses = doc
.root_element()
.descendants()
.find(|node| node.tag_name() == names::RESPONSE)
.unwrap();
assert_eq!(
get_newline_corrected_text(&responses, &names::CALENDAR_DATA).unwrap(),
expected
);
}
}