#[cfg(feature = "alloc")]
use alloc::{string::String, vec::Vec};
use super::{DispatchError, DispatchResult};
pub fn path_in_write_scope(path: &str, scopes: &[String]) -> DispatchResult<bool> {
let path_parts = validate_write_path(path)?;
for scope in scopes {
let scope_parts = validate_write_scope_pattern(scope)?;
if matches_scope(&path_parts, &scope_parts) {
return Ok(true);
}
}
Ok(false)
}
pub fn validate_write_scope_pattern(scope: &str) -> DispatchResult<Vec<&str>> {
if scope.is_empty()
|| scope.len() > 512
|| scope.starts_with('/')
|| scope.contains(['\\', '\0', ':'])
|| scope.chars().any(char::is_control)
|| !scope.is_ascii()
{
return Err(DispatchError::InvalidWriteScope(scope.into()));
}
let parts: Vec<&str> = scope.split('/').collect();
let valid = parts.iter().all(|part| {
!part.is_empty()
&& *part != "."
&& *part != ".."
&& !part.ends_with('.')
&& !part.ends_with(' ')
&& !part.contains('~')
&& (*part == "*"
|| *part == "**"
|| (part.starts_with('*') && part.len() > 1 && part[1..].find('*').is_none())
|| !part.contains('*'))
});
if valid {
Ok(parts)
} else {
Err(DispatchError::InvalidWriteScope(scope.into()))
}
}
fn validate_write_path(path: &str) -> DispatchResult<Vec<&str>> {
if path.is_empty()
|| path.len() > 4_096
|| path.starts_with('/')
|| path.contains(['\\', '\0', '*', ':'])
|| path.chars().any(char::is_control)
|| !path.is_ascii()
|| path
.split('/')
.any(|part| part.ends_with('.') || part.ends_with(' ') || part.contains('~'))
{
return Err(DispatchError::InvalidWriteScope(path.into()));
}
let parts: Vec<&str> = path.split('/').collect();
if parts
.iter()
.any(|part| part.is_empty() || *part == "." || *part == "..")
{
return Err(DispatchError::InvalidWriteScope(path.into()));
}
Ok(parts)
}
fn matches_scope(path: &[&str], scope: &[&str]) -> bool {
match scope.split_first() {
None => path.is_empty(),
Some((&"**", rest)) => {
rest.is_empty() || (0..=path.len()).any(|skip| matches_scope(&path[skip..], rest))
}
Some((expected, rest)) => {
let Some((actual, tail)) = path.split_first() else {
return false;
};
segment_matches(expected, actual) && matches_scope(tail, rest)
}
}
}
fn segment_matches(expected: &str, actual: &str) -> bool {
expected == "*"
|| expected == actual
|| expected
.strip_prefix('*')
.is_some_and(|suffix| !suffix.is_empty() && actual.ends_with(suffix))
}