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 {
if let Some(ns) = name.namespace() {
format!("<{0} xmlns=\"{1}\"/>", name.name(), ns)
} else {
format!("<{0}/>", name.name())
}
}
pub fn render_xml_with_text(name: &PropertyName, text: Option<impl AsRef<str>>) -> String {
match (name.namespace(), text) {
(None, None) => format!("<{}/>", name.name()),
(None, Some(t)) => format!("<{0}>{1}</{0}>", name.name(), escape_text(t.as_ref())),
(Some(ns), None) => format!("<{0} xmlns=\"{ns}\"/>", name.name()),
(Some(ns), Some(t)) => format!(
"<{0} xmlns=\"{ns}\">{1}</{0}>",
name.name(),
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)
}
}
}
#[cfg(test)]
mod tests {
use std::borrow::Cow;
use crate::xmlutils::escape_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"),
}
}
}
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> {
let raw_data = node
.descendants()
.find(|node| node.tag_name() == *property)
.ok_or(WebDavError::InvalidResponse(
format!("missing {} in response", property.name()).into(),
))?
.text()
.ok_or(WebDavError::InvalidResponse(
"missing text in property".into(),
))?;
let mut result = String::new();
let mut last_end = 0;
for (start, part) in raw_data.match_indices('\n') {
result.push_str(&raw_data[last_end..start]);
result.push_str("\r\n");
last_end = start + part.len();
}
result.push_str(&raw_data[last_end..raw_data.len()]);
Ok(result)
}
#[cfg(test)]
mod test {
use crate::{names, xmlutils::get_newline_corrected_text};
#[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
);
}
}