use std::io::Cursor;
use std::time::UNIX_EPOCH;
use axum::{body::Body, http::StatusCode, response::Response};
use quick_xml::Writer;
use quick_xml::events::{BytesDecl, BytesEnd, BytesStart, BytesText, Event};
use super::{PropEntry, PropRequest};
pub const DAV_NS: &str = "DAV:";
pub const SUPPORTED_PROPS: &[&str] = &[
"creationdate",
"getcontentlength",
"getcontenttype",
"getetag",
"getlastmodified",
"lockdiscovery",
"resourcetype",
"supportedlock",
];
pub struct El;
impl El {
pub const ACTIVE_LOCK: &str = "D:activelock";
pub const COLLECTION: &str = "D:collection";
pub const DEPTH: &str = "D:depth";
pub const EXCLUSIVE: &str = "D:exclusive";
pub const HREF: &str = "D:href";
pub const LOCK_DISCOVERY: &str = "D:lockdiscovery";
pub const LOCK_ENTRY: &str = "D:lockentry";
pub const LOCK_SCOPE: &str = "D:lockscope";
pub const LOCK_TOKEN: &str = "D:locktoken";
pub const LOCK_TYPE: &str = "D:locktype";
pub const MULTI_STATUS: &str = "D:multistatus";
pub const OWNER: &str = "D:owner";
pub const PROP: &str = "D:prop";
pub const PROP_STAT: &str = "D:propstat";
pub const RESOURCE_TYPE: &str = "D:resourcetype";
pub const RESPONSE: &str = "D:response";
pub const SHARED: &str = "D:shared";
pub const STATUS: &str = "D:status";
pub const SUPPORTED_LOCK: &str = "D:supportedlock";
pub const TIMEOUT: &str = "D:timeout";
pub const WRITE: &str = "D:write";
pub(crate) const CREATION_DATE: &str = "D:creationdate";
pub(crate) const GET_CONTENT_LENGTH: &str = "D:getcontentlength";
pub(crate) const GET_CONTENT_TYPE: &str = "D:getcontenttype";
pub(crate) const GET_ETAG: &str = "D:getetag";
pub(crate) const GET_LAST_MODIFIED: &str = "D:getlastmodified";
}
pub type XmlWriter = Writer<Cursor<Vec<u8>>>;
pub trait XmlWriterExt {
fn ev(&mut self, event: Event<'_>);
}
impl XmlWriterExt for XmlWriter {
fn ev(&mut self, event: Event<'_>) {
self.write_event(event).unwrap();
}
}
pub fn multistatus(xml: String) -> Response {
Response::builder()
.status(StatusCode::MULTI_STATUS) .header("content-type", "application/xml; charset=utf-8")
.body(Body::from(xml))
.unwrap()
}
pub fn build_multistatus(entries: &[PropEntry], prop_request: &PropRequest) -> String {
let mut writer = Writer::new(Cursor::new(Vec::with_capacity(entries.len() * 700)));
writer.ev(Event::Decl(BytesDecl::new("1.0", Some("utf-8"), None)));
let mut ms = BytesStart::new(El::MULTI_STATUS);
ms.push_attribute(("xmlns:D", DAV_NS));
writer.ev(Event::Start(ms));
for entry in entries {
write_response(&mut writer, entry, prop_request);
}
writer.ev(Event::End(BytesEnd::new(El::MULTI_STATUS)));
String::from_utf8(writer.into_inner().into_inner()).unwrap()
}
fn write_response(writer: &mut XmlWriter, entry: &PropEntry, prop_request: &PropRequest) {
writer.ev(Event::Start(BytesStart::new(El::RESPONSE)));
writer.ev(Event::Start(BytesStart::new(El::HREF)));
writer.ev(Event::Text(BytesText::new(&entry.href)));
writer.ev(Event::End(BytesEnd::new(El::HREF)));
if matches!(prop_request, PropRequest::PropName) {
write_propname(writer, SUPPORTED_PROPS);
writer.ev(Event::End(BytesEnd::new(El::RESPONSE)));
return;
}
let (found, missing) = match prop_request {
PropRequest::AllProp => (SUPPORTED_PROPS.to_vec(), vec![]),
PropRequest::Named(names) => {
let (mut found, mut missing) = (Vec::new(), Vec::new());
for n in names {
let local = super::parse_clark(n).map(|(_, l)| l).unwrap_or(n.as_str());
if SUPPORTED_PROPS.contains(&local) {
if !found.contains(&local) {
found.push(local);
}
} else {
missing.push(n.as_str());
}
}
(found, missing)
}
PropRequest::PropName => unreachable!(),
};
let applicable = found
.iter()
.filter(|p| is_applicable(p, entry.is_dir))
.copied();
let mut applicable = applicable.peekable();
if applicable.peek().is_some() {
write_propstat_200(writer, entry, applicable);
}
let mut not_found: Vec<String> = found
.iter()
.filter(|p| !is_applicable(p, entry.is_dir))
.map(|p| p.to_string())
.collect();
not_found.extend(missing.iter().map(|p| p.to_string()));
if let Some(ref dead) = entry.dead_props {
not_found.retain(|n| !dead.contains_key(n));
}
if !not_found.is_empty() {
write_propstat_404(writer, ¬_found);
}
if let Some(ref dead) = entry.dead_props
&& !dead.is_empty()
{
write_dead_propstat(writer, dead);
}
writer.ev(Event::End(BytesEnd::new(El::RESPONSE)));
}
fn is_applicable(prop: &str, is_dir: bool) -> bool {
match prop {
"getcontentlength" | "getcontenttype" | "getetag" => !is_dir,
_ => true,
}
}
fn write_prop_text(writer: &mut XmlWriter, qname: &str, value: &str) {
writer.ev(Event::Start(BytesStart::new(qname)));
writer.ev(Event::Text(BytesText::new(value)));
writer.ev(Event::End(BytesEnd::new(qname)));
}
fn write_propstat_200<'a, I>(writer: &mut XmlWriter, entry: &PropEntry, props: I)
where
I: Iterator<Item = &'a str>,
{
writer.ev(Event::Start(BytesStart::new(El::PROP_STAT)));
writer.ev(Event::Start(BytesStart::new(El::PROP)));
for prop_name in props {
match prop_name {
"creationdate" => {
let date = entry
.created
.map(crate::utils::time::format_rfc3339)
.unwrap_or_default();
write_prop_text(writer, El::CREATION_DATE, &date);
}
"getcontentlength" => {
write_prop_text(writer, El::GET_CONTENT_LENGTH, &entry.size.to_string());
}
"getcontenttype" => {
write_prop_text(
writer,
El::GET_CONTENT_TYPE,
entry.content_type.as_deref().unwrap_or(""),
);
}
"getetag" => {
let etag = format!(
"{:x}-{:x}",
entry
.modified
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs(),
entry.size
);
write_prop_text(writer, El::GET_ETAG, &etag);
}
"getlastmodified" => {
let date = crate::utils::time::format_rfc1123(entry.modified);
write_prop_text(writer, El::GET_LAST_MODIFIED, &date);
}
"lockdiscovery" => {
writer.ev(Event::Start(BytesStart::new(El::LOCK_DISCOVERY)));
if let Some(ref locks) = entry.active_locks {
for lock in locks {
write_activelock(writer, lock);
}
}
writer.ev(Event::End(BytesEnd::new(El::LOCK_DISCOVERY)));
}
"resourcetype" => {
writer.ev(Event::Start(BytesStart::new(El::RESOURCE_TYPE)));
if entry.is_dir {
writer.ev(Event::Empty(BytesStart::new(El::COLLECTION)));
}
writer.ev(Event::End(BytesEnd::new(El::RESOURCE_TYPE)));
}
"supportedlock" => {
writer.ev(Event::Start(BytesStart::new(El::SUPPORTED_LOCK)));
write_lockentry(writer, "exclusive");
write_lockentry(writer, "shared");
writer.ev(Event::End(BytesEnd::new(El::SUPPORTED_LOCK)));
}
_ => {}
}
}
writer.ev(Event::End(BytesEnd::new(El::PROP)));
writer.ev(Event::Start(BytesStart::new(El::STATUS)));
writer.ev(Event::Text(BytesText::new("HTTP/1.1 200 OK")));
writer.ev(Event::End(BytesEnd::new(El::STATUS)));
writer.ev(Event::End(BytesEnd::new(El::PROP_STAT)));
}
fn write_lockentry(writer: &mut XmlWriter, scope: &str) {
writer.ev(Event::Start(BytesStart::new(El::LOCK_ENTRY)));
writer.ev(Event::Start(BytesStart::new(El::LOCK_SCOPE)));
if scope == "exclusive" {
writer.ev(Event::Empty(BytesStart::new(El::EXCLUSIVE)));
} else {
writer.ev(Event::Empty(BytesStart::new(El::SHARED)));
}
writer.ev(Event::End(BytesEnd::new(El::LOCK_SCOPE)));
writer.ev(Event::Start(BytesStart::new(El::LOCK_TYPE)));
writer.ev(Event::Empty(BytesStart::new(El::WRITE)));
writer.ev(Event::End(BytesEnd::new(El::LOCK_TYPE)));
writer.ev(Event::End(BytesEnd::new(El::LOCK_ENTRY)));
}
fn write_propstat_404(writer: &mut XmlWriter, props: &[String]) {
writer.ev(Event::Start(BytesStart::new(El::PROP_STAT)));
writer.ev(Event::Start(BytesStart::new(El::PROP)));
for prop_name in props {
let (ns, local) = super::parse_clark(prop_name).unwrap_or(("", prop_name));
let mut elem = BytesStart::new(local);
if !ns.is_empty() {
elem.push_attribute(("xmlns", ns));
}
writer.ev(Event::Empty(elem));
}
writer.ev(Event::End(BytesEnd::new(El::PROP)));
writer.ev(Event::Start(BytesStart::new(El::STATUS)));
writer.ev(Event::Text(BytesText::new("HTTP/1.1 404 Not Found")));
writer.ev(Event::End(BytesEnd::new(El::STATUS)));
writer.ev(Event::End(BytesEnd::new(El::PROP_STAT)));
}
fn write_propname(writer: &mut XmlWriter, props: &[&str]) {
writer.ev(Event::Start(BytesStart::new(El::PROP_STAT)));
writer.ev(Event::Start(BytesStart::new(El::PROP)));
for prop_name in props {
writer.ev(Event::Empty(BytesStart::new(format!("D:{prop_name}"))));
}
writer.ev(Event::End(BytesEnd::new(El::PROP)));
writer.ev(Event::Start(BytesStart::new(El::STATUS)));
writer.ev(Event::Text(BytesText::new("HTTP/1.1 200 OK")));
writer.ev(Event::End(BytesEnd::new(El::STATUS)));
writer.ev(Event::End(BytesEnd::new(El::PROP_STAT)));
}
pub fn write_activelock(writer: &mut XmlWriter, lock: &super::LockInfo) {
writer.ev(Event::Start(BytesStart::new(El::ACTIVE_LOCK)));
writer.ev(Event::Start(BytesStart::new(El::LOCK_SCOPE)));
match lock.scope {
super::LockScope::Exclusive => {
writer.ev(Event::Empty(BytesStart::new(El::EXCLUSIVE)));
}
super::LockScope::Shared => {
writer.ev(Event::Empty(BytesStart::new(El::SHARED)));
}
}
writer.ev(Event::End(BytesEnd::new(El::LOCK_SCOPE)));
writer.ev(Event::Start(BytesStart::new(El::LOCK_TYPE)));
writer.ev(Event::Empty(BytesStart::new(El::WRITE)));
writer.ev(Event::End(BytesEnd::new(El::LOCK_TYPE)));
let depth_str = match lock.depth {
super::Depth::Zero => "0",
super::Depth::One => "1",
super::Depth::Infinity => "infinity",
};
writer.ev(Event::Start(BytesStart::new(El::DEPTH)));
writer.ev(Event::Text(BytesText::new(depth_str)));
writer.ev(Event::End(BytesEnd::new(El::DEPTH)));
if let Some(ref owner) = lock.owner {
writer.ev(Event::Start(BytesStart::new(El::OWNER)));
writer.ev(Event::Text(BytesText::new(owner)));
writer.ev(Event::End(BytesEnd::new(El::OWNER)));
}
if let Some(d) = lock.timeout {
writer.ev(Event::Start(BytesStart::new(El::TIMEOUT)));
writer.ev(Event::Text(BytesText::new(&format!(
"Second-{}",
d.as_secs()
))));
writer.ev(Event::End(BytesEnd::new(El::TIMEOUT)));
}
writer.ev(Event::Start(BytesStart::new(El::LOCK_TOKEN)));
writer.ev(Event::Start(BytesStart::new(El::HREF)));
writer.ev(Event::Text(BytesText::new(&lock.token)));
writer.ev(Event::End(BytesEnd::new(El::HREF)));
writer.ev(Event::End(BytesEnd::new(El::LOCK_TOKEN)));
writer.ev(Event::End(BytesEnd::new(El::ACTIVE_LOCK)));
}
fn write_dead_propstat(writer: &mut XmlWriter, props: &std::collections::HashMap<String, String>) {
writer.ev(Event::Start(BytesStart::new(El::PROP_STAT)));
writer.ev(Event::Start(BytesStart::new(El::PROP)));
for (clark_key, value) in props {
let (ns, local) = super::parse_clark(clark_key).unwrap_or(("", clark_key));
let mut elem = BytesStart::new(local);
if !ns.is_empty() {
elem.push_attribute(("xmlns", ns));
}
writer.ev(Event::Start(elem));
writer.ev(Event::Text(BytesText::new(value)));
writer.ev(Event::End(BytesEnd::new(local)));
}
writer.ev(Event::End(BytesEnd::new(El::PROP)));
writer.ev(Event::Start(BytesStart::new(El::STATUS)));
writer.ev(Event::Text(BytesText::new("HTTP/1.1 200 OK")));
writer.ev(Event::End(BytesEnd::new(El::STATUS)));
writer.ev(Event::End(BytesEnd::new(El::PROP_STAT)));
}