pub mod fs;
pub mod ls;
pub mod method;
pub mod xml;
use std::collections::HashMap;
use std::fmt;
use std::fs::Metadata;
use std::path::PathBuf;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use axum::http::HeaderMap;
use derive_new::new;
use percent_encoding::percent_decode_str;
use quick_xml::Reader;
use quick_xml::events::{BytesStart, Event};
use crate::scandir::DirEntryMeta;
pub use ls::{IfCondition, IfList, parse_if_header};
pub use method::Method;
pub use xml::{El, XmlWriter, XmlWriterExt};
pub type DeadPropertyStore = HashMap<PathBuf, HashMap<String, String>>;
#[derive(Debug, Clone)]
pub struct PropPatchAction(pub String, pub Option<String>);
#[derive(Debug, Clone, new)]
pub struct PropPatchOp {
pub actions: Vec<PropPatchAction>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Depth {
Zero,
One,
Infinity,
}
impl fmt::Display for Depth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Zero => "0".fmt(f),
Self::One => "1".fmt(f),
Self::Infinity => "infinity".fmt(f),
}
}
}
#[derive(Debug, Clone)]
pub enum PropRequest {
AllProp,
PropName,
Named(Vec<String>),
}
#[derive(Debug, Clone, new)]
pub struct PropEntry {
pub href: String,
pub modified: SystemTime,
pub created: Option<SystemTime>,
pub size: u64,
pub is_dir: bool,
#[new(value = "None")]
pub content_type: Option<String>,
#[new(value = "None")]
pub dead_props: Option<HashMap<String, String>>,
#[new(value = "None")]
pub active_locks: Option<Vec<LockInfo>>,
#[new(value = "None")]
pub canonical_path: Option<PathBuf>,
}
impl PropEntry {
pub fn from_meta(meta: &Metadata, href: String, is_dir: bool) -> Self {
Self::new(
href,
meta.modified().unwrap_or(UNIX_EPOCH),
meta.created().ok(),
meta.len(),
is_dir,
)
}
pub(crate) fn from_dirent(meta: &DirEntryMeta, href: String) -> Self {
Self::new(href, meta.modified, meta.created, meta.size, meta.is_dir)
}
}
pub type LockStore = HashMap<PathBuf, Vec<LockInfo>>;
#[derive(Debug, Clone, new)]
pub struct LockInfo {
pub scope: LockScope,
pub token: String,
pub owner: Option<String>,
pub created: SystemTime,
pub timeout: Option<Duration>,
pub depth: Depth,
}
impl LockInfo {
pub fn is_expired(&self) -> bool {
let Some(timeout) = self.timeout else {
return false;
};
self.created.elapsed().unwrap_or_default() >= timeout
}
pub fn is_exclusive(&self) -> bool {
matches!(self.scope, LockScope::Exclusive)
}
}
#[derive(Debug, Clone, Copy)]
pub enum LockScope {
Exclusive,
Shared,
}
pub fn generate_lock_token() -> String {
use std::hash::{Hash, Hasher};
use std::time::UNIX_EPOCH;
let nanos = std::time::SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_nanos();
let mut h = std::collections::hash_map::DefaultHasher::new();
nanos.hash(&mut h);
format!("opaquelocktoken:{:016x}", h.finish())
}
pub fn parse_lock_token_header(headers: &HeaderMap) -> Option<String> {
let value = headers.get("lock-token")?.to_str().ok()?;
value
.trim_matches('<')
.trim_matches('>')
.trim()
.to_string()
.into()
}
pub fn parse_timeout(headers: &HeaderMap) -> Option<std::time::Duration> {
let value = headers.get("timeout")?.to_str().ok()?;
let seconds = value
.strip_prefix("Second-")
.and_then(|s| s.parse::<u64>().ok())?;
Some(std::time::Duration::from_secs(seconds))
}
pub fn parse_depth(headers: &HeaderMap) -> Depth {
let depth = headers.get("depth");
match depth.and_then(|v| v.to_str().ok()).unwrap_or("infinity") {
"0" => Depth::Zero,
"1" => Depth::One,
_ => Depth::Infinity,
}
}
#[derive(Debug)]
pub enum ParseError {
InvalidBody(&'static str),
Xml(quick_xml::Error),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Xml(e) => write!(f, "XML parse error: {e}"),
Self::InvalidBody(s) => write!(f, "invalid body: {s}"),
}
}
}
impl From<quick_xml::Error> for ParseError {
fn from(e: quick_xml::Error) -> Self {
Self::Xml(e)
}
}
pub fn clark_key(ns: &str, local: &str) -> String {
if ns.is_empty() {
local.to_string()
} else {
format!("{{{}}}{}", ns, local)
}
}
pub fn parse_clark(key: &str) -> Option<(&str, &str)> {
if let Some(rest) = key.strip_prefix('{') {
let (ns, local) = rest.split_once('}')?;
Some((ns, local))
} else {
Some(("", key))
}
}
fn extract_element_ns(e: &BytesStart) -> Result<(String, String), ParseError> {
let qname = e.name();
let name = qname.as_ref();
let (prefix, local) = match name.iter().position(|&b| b == b':') {
Some(pos) => (
Some(String::from_utf8_lossy(&name[..pos]).to_string()),
String::from_utf8_lossy(&name[pos + 1..]).to_string(),
),
None => (None, String::from_utf8_lossy(name).to_string()),
};
let ns = match prefix {
Some(ref p) => {
let key = format!("xmlns:{}", p);
let attr = e
.attributes()
.flatten()
.find(|a| String::from_utf8_lossy(a.key.as_ref()) == key);
match attr {
Some(a) => {
let value = String::from_utf8_lossy(&a.value);
if value.is_empty() {
return Err(ParseError::InvalidBody(
"invalid namespace declaration: empty URI",
));
}
value.to_string()
}
None => String::new(),
}
}
None => e
.attributes()
.flatten()
.find(|a| a.key.as_ref() == b"xmlns")
.map(|a| String::from_utf8_lossy(&a.value).to_string())
.unwrap_or_default(),
};
Ok((ns, local))
}
pub fn parse_propfind_request(xml: &[u8]) -> Result<PropRequest, ParseError> {
let mut reader = Reader::from_reader(xml);
reader.config_mut().trim_text(true);
let mut props = Vec::new();
let mut in_prop = false;
let mut found_allprop = false;
let mut found_propname = false;
let mut seen_element = false;
loop {
match reader.read_event()? {
Event::Start(e) | Event::Empty(e) => {
seen_element = true;
let (ns, local) = extract_element_ns(&e)?;
let name = local.as_bytes();
match name {
b"prop" => in_prop = true,
b"allprop" => found_allprop = true,
b"propname" => found_propname = true,
_ if in_prop => {
props.push(clark_key(&ns, &local));
}
_ => {}
}
}
Event::End(e) => {
let local_name = e.local_name();
let local = String::from_utf8_lossy(local_name.as_ref());
if local == "prop" {
in_prop = false;
}
}
Event::Eof => break,
_ => {}
}
}
if found_allprop || (props.is_empty() && !seen_element) {
Ok(PropRequest::AllProp)
} else if props.is_empty() && seen_element {
Err(ParseError::InvalidBody("invalid PROPFIND request body"))
} else if found_propname {
Ok(PropRequest::PropName)
} else {
Ok(PropRequest::Named(props))
}
}
pub fn parse_destination(headers: &HeaderMap) -> Option<String> {
let value = headers.get("destination")?.to_str().ok()?;
let mut path = if let Some(pos) = value.find("://") {
let after_scheme = &value[pos + 3..];
if let Some(slash_pos) = after_scheme.find('/') {
percent_decode_str(&after_scheme[slash_pos..])
.decode_utf8_lossy()
.to_string()
} else {
return None;
}
} else if value.starts_with('/') {
percent_decode_str(value).decode_utf8_lossy().to_string()
} else {
return None;
};
path.truncate(path.trim_end_matches('/').len());
Some(path)
}
pub fn parse_overwrite(headers: &HeaderMap) -> bool {
headers
.get("overwrite")
.and_then(|v| v.to_str().ok())
.map(|v| v.to_ascii_uppercase())
.unwrap_or_else(|| "T".into())
!= "F"
}
fn decode_xml_char_refs(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let mut rest = text;
while let Some(pos) = rest.find("&#") {
result.push_str(&rest[..pos]);
rest = &rest[pos + 2..];
let hex = rest.starts_with('x') || rest.starts_with('X');
if hex {
rest = &rest[1..];
}
if let Some(end) = rest.find(';') {
let num_str = &rest[..end];
let radix = if hex { 16 } else { 10 };
if let Ok(n) = u32::from_str_radix(num_str, radix)
&& let Some(c) = char::from_u32(n)
{
result.push(c);
}
rest = &rest[end + 1..];
} else {
break;
}
}
result.push_str(rest);
result
}
pub fn parse_proppatch_request(xml: &[u8]) -> Result<PropPatchOp, ParseError> {
let decoded = decode_xml_char_refs(&String::from_utf8_lossy(xml));
let mut reader = Reader::from_reader(decoded.as_bytes());
reader.config_mut().trim_text(true);
let mut actions = Vec::new();
let mut in_set = false;
let mut in_remove = false;
let mut current_name: Option<String> = None;
loop {
match reader.read_event()? {
Event::Start(e) => {
let (ns, local) = extract_element_ns(&e)?;
match &*local {
"set" => in_set = true,
"remove" => in_remove = true,
"prop" => {}
_ if in_set => {
current_name = Some(clark_key(&ns, &local));
}
_ if in_remove => {
actions.push(PropPatchAction(clark_key(&ns, &local), None));
}
_ => {}
}
}
Event::Empty(e) => {
let (ns, local) = extract_element_ns(&e)?;
if in_remove && local != "prop" {
actions.push(PropPatchAction(clark_key(&ns, &local), None));
} else if in_set && local != "prop" {
actions.push(PropPatchAction(clark_key(&ns, &local), Some(String::new())));
}
}
Event::Text(t) if in_set && current_name.is_some() => {
let raw = String::from_utf8_lossy(t.as_ref());
let val = decode_xml_char_refs(&raw);
actions.push(PropPatchAction(current_name.take().unwrap(), Some(val)));
}
Event::End(e) => {
let local_name = e.local_name();
let local = String::from_utf8_lossy(local_name.as_ref());
match &*local {
"set" => in_set = false,
"remove" => in_remove = false,
_ if in_set && current_name.is_some() => {
actions.push(PropPatchAction(
current_name.take().unwrap(),
Some(String::new()),
));
}
_ => {}
}
}
Event::Eof => break,
_ => {}
}
}
if actions.is_empty() {
return Err(ParseError::InvalidBody("invalid PROPPATCH body"));
}
Ok(PropPatchOp::new(actions))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_element_ns_default_ns() {
let mut elem = BytesStart::new("prop0");
elem.push_attribute(("xmlns", "http://example.com/neon/litmus/"));
let (ns, local) = extract_element_ns(&elem).unwrap();
assert_eq!(ns, "http://example.com/neon/litmus/");
assert_eq!(local, "prop0");
}
#[test]
fn test_extract_element_ns_no_ns() {
let elem = BytesStart::new("prop0");
let (ns, local) = extract_element_ns(&elem).unwrap();
assert_eq!(ns, "");
assert_eq!(local, "prop0");
}
#[test]
fn test_extract_element_ns_prefixed() {
let mut elem = BytesStart::new("X:prop0");
elem.push_attribute(("xmlns:X", "http://example.com/ns"));
let (ns, local) = extract_element_ns(&elem).unwrap();
assert_eq!(ns, "http://example.com/ns");
assert_eq!(local, "prop0");
}
#[test]
fn test_extract_element_ns_invalid_empty_uri() {
let mut elem = BytesStart::new("bar:foo");
elem.push_attribute(("xmlns:bar", ""));
assert!(extract_element_ns(&elem).is_err());
}
}