use std::path::Path;
use std::time::SystemTime;
use axum::http::{HeaderMap, StatusCode};
use derive_new::new;
use super::{Depth, LockInfo, LockStore};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IfCondition {
StateToken(String),
Not(Box<IfCondition>),
}
impl IfCondition {
pub fn eval(&self, infos: &[LockInfo]) -> bool {
let active: Vec<&LockInfo> = active_slice(infos).collect();
self.eval_active(&active)
}
pub fn eval_active(&self, active: &[&LockInfo]) -> bool {
match self {
IfCondition::StateToken(t) if t == "DAV:no-lock" => active.is_empty(),
IfCondition::StateToken(t) => active.iter().any(|l| l.token == *t),
IfCondition::Not(inner) => !inner.eval_active(active),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, new)]
pub struct IfList {
pub resource_tag: Option<String>,
pub conditions: Vec<IfCondition>,
}
impl IfList {
pub fn positive_tokens(&self) -> Vec<&str> {
self.positive_tokens_iter().collect()
}
pub fn positive_tokens_iter(&self) -> impl Iterator<Item = &str> + '_ {
self.conditions.iter().filter_map(|c| match c {
IfCondition::StateToken(t) => Some(t.as_str()),
_ => None,
})
}
pub fn has_lock_token(&self) -> bool {
self.conditions.iter().any(|c| match c {
IfCondition::StateToken(t) => t != "DAV:no-lock",
IfCondition::Not(inner) => {
matches!(inner.as_ref(), IfCondition::StateToken(t) if t != "DAV:no-lock")
}
})
}
}
pub fn parse_if_header(headers: &HeaderMap) -> Vec<IfList> {
let value = match headers.get("if").and_then(|v| v.to_str().ok()) {
Some(v) => v,
None => return Vec::new(),
};
let bytes = value.as_bytes();
let mut pos = 0;
let mut lists = Vec::new();
while pos < bytes.len() {
pos = skip_ws(bytes, pos);
if pos >= bytes.len() {
break;
}
match bytes[pos] {
b'<' => {
let Some((tag, new_pos)) = read_angle_bracket(bytes, pos) else {
break;
};
pos = skip_ws(bytes, new_pos);
if pos < bytes.len() && bytes[pos] == b'(' {
let resource_tag = tag;
while pos < bytes.len() && bytes[pos] == b'(' {
let (conditions, new_pos) = read_list(bytes, pos);
pos = skip_ws(bytes, new_pos);
lists.push(IfList::new(Some(resource_tag.clone()), conditions));
}
} else {
lists.push(IfList::new(None, vec![IfCondition::StateToken(tag)]));
}
}
b'(' => {
let (conditions, new_pos) = read_list(bytes, pos);
pos = skip_ws(bytes, new_pos);
lists.push(IfList::new(None, conditions));
}
_ => {
pos += 1;
}
}
}
lists
}
fn skip_ws(bytes: &[u8], mut p: usize) -> usize {
while p < bytes.len() && bytes[p].is_ascii_whitespace() {
p += 1;
}
p
}
fn read_angle_bracket(bytes: &[u8], p: usize) -> Option<(String, usize)> {
debug_assert!(bytes.get(p) == Some(&b'<'));
let start = p + 1;
let end = bytes[start..].iter().position(|&b| b == b'>')?;
Some((
std::str::from_utf8(&bytes[start..start + end])
.ok()?
.to_string(),
start + end + 1,
))
}
fn read_list(bytes: &[u8], mut p: usize) -> (Vec<IfCondition>, usize) {
debug_assert!(bytes.get(p) == Some(&b'('));
p += 1;
let mut conditions = Vec::new();
loop {
p = skip_ws(bytes, p);
if p >= bytes.len() || bytes[p] == b')' {
if p < bytes.len() {
p += 1;
}
break;
}
let mut negated = false;
if bytes[p..].starts_with(b"Not") {
let after = p + 3;
if after >= bytes.len()
|| bytes[after].is_ascii_whitespace()
|| bytes[after] == b'<'
|| bytes[after] == b'('
{
negated = true;
p = skip_ws(bytes, after);
}
}
if p < bytes.len() && bytes[p] == b'<' {
let Some((token, new_p)) = read_angle_bracket(bytes, p) else {
break;
};
p = new_p;
let cond = IfCondition::StateToken(token);
conditions.push(if negated {
IfCondition::Not(Box::new(cond))
} else {
cond
});
} else {
p += 1;
}
}
(conditions, p)
}
pub fn active_slice(infos: &[LockInfo]) -> impl Iterator<Item = &LockInfo> + '_ {
infos.iter().filter(|l| !l.is_expired())
}
pub fn walk_locked_ancestors<'a>(
locks: &'a LockStore,
target: &Path,
root_canonical: &Path,
mut f: impl FnMut(&'a [LockInfo]) -> bool,
) -> bool {
if locks.is_empty() {
return false;
}
let inf_entries: Vec<_> = locks
.iter()
.filter(|(_, infos)| infos.iter().any(|l| l.depth == Depth::Infinity))
.collect();
if inf_entries.is_empty() {
return false;
}
if inf_entries.len() <= 10 {
for (lock_path, infos) in inf_entries {
if is_ancestor_of(lock_path, target) && f(infos) {
return true;
}
}
false
} else {
let mut current = target.parent();
while let Some(parent) = current {
if !parent.starts_with(root_canonical) {
break;
}
if let Some(infos) = locks.get(parent)
&& f(infos)
{
return true;
}
current = parent.parent();
}
false
}
}
fn is_ancestor_of(ancestor: &Path, target: &Path) -> bool {
let an = ancestor.as_os_str().as_encoded_bytes();
let ta = target.as_os_str().as_encoded_bytes();
if an.is_empty() || an.len() >= ta.len() {
return false;
}
if !target.starts_with(ancestor) {
return false;
}
if an == b"/" {
return true;
}
ta[an.len()] == b'/'
}
pub fn find_ancestor_lock<'a>(
locks: &'a LockStore,
target: &Path,
root_canonical: &Path,
predicate: impl Fn(&LockInfo) -> bool,
) -> Option<&'a LockInfo> {
let mut result: Option<&'a LockInfo> = None;
walk_locked_ancestors(locks, target, root_canonical, |infos| {
for lock in infos {
if lock.depth == Depth::Infinity && !lock.is_expired() && predicate(lock) {
result = Some(lock);
return true;
}
}
false
});
result
}
pub fn find_and_refresh_ancestor_lock(
locks: &mut LockStore,
target: &Path,
predicate: impl Fn(&LockInfo) -> bool,
) -> Option<LockInfo> {
let candidate_paths: Vec<std::path::PathBuf> = locks
.iter()
.filter(|(path, infos)| {
infos
.iter()
.any(|l| l.depth == Depth::Infinity && !l.is_expired())
&& is_ancestor_of(path, target)
})
.map(|(path, _)| path.clone())
.collect();
for path in candidate_paths {
if let Some(infos) = locks.get_mut(&path) {
for lock in infos.iter_mut() {
if lock.depth == Depth::Infinity && !lock.is_expired() && predicate(lock) {
lock.created = SystemTime::now();
return Some(lock.clone());
}
}
}
}
None
}
pub fn eval_if(lists: &[IfList], infos: &[LockInfo], request_path: &str) -> bool {
if lists.is_empty() {
return active_slice(infos).next().is_none();
}
let mut applicable = lists
.iter()
.filter(|l| match &l.resource_tag {
Some(tag) => tag == request_path,
None => true,
})
.peekable();
if applicable.peek().is_none() {
return true;
}
let active: Vec<&LockInfo> = active_slice(infos).collect();
applicable.all(|l| l.conditions.iter().all(|c| c.eval_active(&active)))
}
pub fn check_existing_exclusive(
entry: &[LockInfo],
if_tokens: &[String],
) -> Result<Option<String>, StatusCode> {
let token_info = active_slice(entry).find(|l| l.is_exclusive());
match token_info.map(|l| l.token.clone()) {
Some(t) if if_tokens.contains(&t) => Ok(Some(t)),
Some(_) => Err(StatusCode::LOCKED),
None => Ok(None),
}
}
#[cfg(test)]
mod tests {
use std::time::{Duration, SystemTime};
use super::*;
use crate::webdav::LockScope;
fn make_lock(scope: LockScope, token: &str) -> LockInfo {
LockInfo::new(
scope,
token.into(),
None,
SystemTime::now(),
None,
Depth::Zero,
)
}
fn make_expired_lock(token: &str) -> LockInfo {
LockInfo::new(
LockScope::Exclusive,
token.into(),
None,
SystemTime::now() - Duration::from_secs(2),
Some(Duration::from_secs(1)),
Depth::Zero,
)
}
#[test]
fn test_eval_condition_state_token_match() {
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
let cond = IfCondition::StateToken("t1".into());
assert!(cond.eval(&infos));
}
#[test]
fn test_eval_condition_state_token_no_match() {
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
let cond = IfCondition::StateToken("t2".into());
assert!(!cond.eval(&infos));
}
#[test]
fn test_eval_condition_dav_no_lock_unlocked() {
let infos: Vec<LockInfo> = vec![];
let cond = IfCondition::StateToken("DAV:no-lock".into());
assert!(cond.eval(&infos));
}
#[test]
fn test_eval_condition_dav_no_lock_locked() {
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
let cond = IfCondition::StateToken("DAV:no-lock".into());
assert!(!cond.eval(&infos));
}
#[test]
fn test_eval_condition_not_token() {
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
let cond = IfCondition::Not(Box::new(IfCondition::StateToken("t2".into())));
assert!(cond.eval(&infos));
}
#[test]
fn test_eval_condition_not_token_reject() {
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
let cond = IfCondition::Not(Box::new(IfCondition::StateToken("t1".into())));
assert!(!cond.eval(&infos));
}
#[test]
fn test_eval_if_no_lists_unlocked() {
let lists: Vec<IfList> = vec![];
let infos: Vec<LockInfo> = vec![];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_no_lists_locked() {
let lists: Vec<IfList> = vec![];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(!eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_matched_token() {
let lists = vec![IfList::new(
None,
vec![IfCondition::StateToken("t1".into())],
)];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_wrong_token() {
let lists = vec![IfList::new(
None,
vec![IfCondition::StateToken("t2".into())],
)];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(!eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_not_no_lock_locked() {
let lists = vec![IfList::new(
None,
vec![IfCondition::Not(Box::new(IfCondition::StateToken(
"DAV:no-lock".into(),
)))],
)];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_not_no_lock_unlocked() {
let lists = vec![IfList::new(
None,
vec![IfCondition::Not(Box::new(IfCondition::StateToken(
"DAV:no-lock".into(),
)))],
)];
let infos: Vec<LockInfo> = vec![];
assert!(!eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_resource_tag_mismatch() {
let lists = vec![IfList::new(
Some("/b".into()),
vec![IfCondition::StateToken("t2".into())],
)];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_resource_tag_match() {
let lists = vec![IfList::new(
Some("/a".into()),
vec![IfCondition::StateToken("t1".into())],
)];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_eval_if_no_lists_expired_ignored() {
let lists: Vec<IfList> = vec![];
let infos = vec![make_expired_lock("t1")];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_dav_no_lock_expired_ignored() {
let infos = vec![make_expired_lock("t1")];
let cond = IfCondition::StateToken("DAV:no-lock".into());
assert!(cond.eval(&infos));
}
#[test]
fn test_token_match_expired_rejected() {
let infos = vec![make_expired_lock("t1")];
let cond = IfCondition::StateToken("t1".into());
assert!(!cond.eval(&infos));
}
#[test]
fn test_eval_if_dav_no_lock_unlocked() {
let lists = vec![IfList::new(
None,
vec![IfCondition::StateToken("DAV:no-lock".into())],
)];
let infos: Vec<LockInfo> = vec![];
assert!(eval_if(&lists, &infos, "/a"));
}
#[test]
fn test_check_existing_exclusive_empty() {
let entry: Vec<LockInfo> = vec![];
let tokens: Vec<String> = vec![];
assert_eq!(check_existing_exclusive(&entry, &tokens), Ok(None));
}
#[test]
fn test_check_existing_exclusive_matching_token() {
let entry = vec![make_lock(LockScope::Exclusive, "t1")];
let tokens = vec!["t1".into()];
assert_eq!(
check_existing_exclusive(&entry, &tokens),
Ok(Some("t1".into()))
);
}
#[test]
fn test_check_existing_exclusive_wrong_token() {
let entry = vec![make_lock(LockScope::Exclusive, "t1")];
let tokens = vec!["t2".into()];
assert_eq!(
check_existing_exclusive(&entry, &tokens),
Err(StatusCode::LOCKED)
);
}
#[test]
fn test_check_existing_exclusive_expired_ignored() {
let entry = vec![make_expired_lock("t1")];
let tokens: Vec<String> = vec![];
assert_eq!(check_existing_exclusive(&entry, &tokens), Ok(None));
}
#[test]
fn test_check_existing_exclusive_shared_only() {
let entry = vec![make_lock(LockScope::Shared, "t1")];
let tokens: Vec<String> = vec![];
assert_eq!(check_existing_exclusive(&entry, &tokens), Ok(None));
}
#[test]
fn test_eval_if_dav_no_lock_locked() {
let lists = vec![IfList::new(
None,
vec![IfCondition::StateToken("DAV:no-lock".into())],
)];
let infos = vec![make_lock(LockScope::Exclusive, "t1")];
assert!(!eval_if(&lists, &infos, "/a"));
}
use std::collections::HashMap;
use std::path::PathBuf;
fn lock_store(entries: Vec<(&str, LockInfo)>) -> LockStore {
let mut store = LockStore::new();
for (path, info) in entries {
store.entry(PathBuf::from(path)).or_default().push(info);
}
store
}
fn make_infinity_lock(scope: LockScope, token: &str) -> LockInfo {
LockInfo::new(
scope,
token.into(),
None,
SystemTime::now(),
None,
Depth::Infinity,
)
}
#[test]
fn test_walk_locked_ancestors_empty_store() {
let store: LockStore = HashMap::new();
let target = Path::new("/a/b/c/file.txt");
let root = Path::new("/a");
assert!(!walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_walk_locked_ancestors_no_infinity() {
let store = lock_store(vec![("/a/b", make_lock(LockScope::Exclusive, "t1"))]);
let target = Path::new("/a/b/c/file.txt");
let root = Path::new("/a");
assert!(!walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_walk_locked_ancestors_ancestor_match() {
let store = lock_store(vec![(
"/a/b",
make_infinity_lock(LockScope::Exclusive, "t1"),
)]);
let target = Path::new("/a/b/c/file.txt");
let root = Path::new("/a");
assert!(walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_walk_locked_ancestors_no_match() {
let store = lock_store(vec![(
"/x/y",
make_infinity_lock(LockScope::Exclusive, "t1"),
)]);
let target = Path::new("/a/b/c/file.txt");
let root = Path::new("/a");
assert!(!walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_walk_locked_ancestors_path_boundary() {
let store = lock_store(vec![(
"/a/b",
make_infinity_lock(LockScope::Exclusive, "t1"),
)]);
let target = Path::new("/a/bc/file.txt");
let root = Path::new("/a");
assert!(!walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_walk_locked_ancestors_root_lock() {
let store = lock_store(vec![("/", make_infinity_lock(LockScope::Exclusive, "t1"))]);
let target = Path::new("/anything/deep/file.txt");
let root = Path::new("/");
assert!(walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_walk_locked_ancestors_many_locks() {
let mut store = LockStore::new();
for i in 0..15 {
store
.entry(PathBuf::from(format!("/level_{i}")))
.or_default()
.push(make_infinity_lock(LockScope::Exclusive, &format!("t{i}")));
}
let target = Path::new("/level_0/sub/file.txt");
let root = Path::new("/");
assert!(walk_locked_ancestors(&store, target, root, |_| true));
}
#[test]
fn test_is_ancestor_of_boundary_cases() {
assert!(is_ancestor_of(
Path::new("/a/b"),
Path::new("/a/b/c/file.txt")
));
assert!(!is_ancestor_of(
Path::new("/a/b"),
Path::new("/a/bc/file.txt")
));
assert!(is_ancestor_of(Path::new("/"), Path::new("/a")));
assert!(is_ancestor_of(Path::new("/"), Path::new("/file.txt")));
assert!(!is_ancestor_of(Path::new("/a/b"), Path::new("/a/b")));
assert!(!is_ancestor_of(Path::new("/a/b/c"), Path::new("/a/b")));
assert!(!is_ancestor_of(Path::new(""), Path::new("/a")));
}
#[test]
fn test_find_ancestor_lock_ignores_expired() {
let store = lock_store(vec![(
"/a",
LockInfo::new(
LockScope::Exclusive,
"expired-token".into(),
None,
SystemTime::now() - Duration::from_secs(10),
Some(Duration::from_secs(1)),
Depth::Infinity,
),
)]);
let target = Path::new("/a/b/file.txt");
let root = Path::new("/");
let found = find_ancestor_lock(&store, target, root, |l| l.token == "expired-token");
assert!(found.is_none(), "expired ancestor lock should not be found");
}
#[test]
fn test_find_and_refresh_ancestor_lock_updates_created() {
let old_time = SystemTime::now() - Duration::from_secs(3600);
let mut store = lock_store(vec![(
"/a",
LockInfo::new(
LockScope::Exclusive,
"t1".into(),
None,
old_time,
Some(Duration::from_secs(7200)),
Depth::Infinity,
),
)]);
let target = Path::new("/a/b/file.txt");
let refreshed = find_and_refresh_ancestor_lock(&mut store, target, |l| l.token == "t1");
assert!(refreshed.is_some());
let new_created = store.get(Path::new("/a")).unwrap()[0].created;
let elapsed = new_created.duration_since(old_time).unwrap();
assert!(elapsed >= Duration::from_secs(3599));
}
#[test]
fn test_find_and_refresh_ancestor_lock_ignores_expired() {
let mut store = lock_store(vec![(
"/a",
LockInfo::new(
LockScope::Exclusive,
"expired".into(),
None,
SystemTime::now() - Duration::from_secs(10),
Some(Duration::from_secs(1)),
Depth::Infinity,
),
)]);
let target = Path::new("/a/b/file.txt");
let result = find_and_refresh_ancestor_lock(&mut store, target, |l| l.token == "expired");
assert!(result.is_none());
}
#[test]
fn test_find_and_refresh_ancestor_lock_no_match() {
let mut store = lock_store(vec![(
"/a",
LockInfo::new(
LockScope::Exclusive,
"t1".into(),
None,
SystemTime::now(),
None,
Depth::Infinity,
),
)]);
let target = Path::new("/a/b/file.txt");
let result =
find_and_refresh_ancestor_lock(&mut store, target, |l| l.token == "wrong-token");
assert!(result.is_none());
}
}