///|
/// Wildcards operate on root-relative paths. ** is a complete path segment;
/// * and ? never cross a slash. Literal path behavior remains unchanged.
pub fn document_path_has_glob(path : String) -> Bool {
path.contains("*") || path.contains("?")
}
///|
pub fn normalize_document_path_pattern(path : String) -> String? {
if valid_document_path_pattern(path) {
Some(normalize_document_config_path(path))
} else {
None
}
}
///|
pub fn valid_document_path_pattern(path : String) -> Bool {
if !valid_document_config_path(path) {
return false
}
if document_path_has_glob(path) {
for part in path.split("/") {
if part == ".." || (part.contains("**") && part != "**") {
return false
}
}
}
true
}
///|
/// Match a glob, or a literal file/directory and its descendants. No filesystem
/// access is performed; callers enforce canonical workspace boundaries.
pub fn document_path_pattern_matches(pattern : String, path : String) -> Bool {
if !valid_document_path_pattern(pattern) || !valid_document_config_path(path) {
return false
}
let pattern = normalize_document_config_path(pattern)
let path = normalize_document_config_path(path)
if !document_path_has_glob(pattern) {
return pattern == "." || path == pattern || path.has_prefix(pattern + "/")
}
let parts = pattern.split("/").to_array()
let names = path.split("/").to_array()
let mut previous = Array::make(names.length() + 1, false)
previous[0] = true
for part in parts {
let next = Array::make(names.length() + 1, false)
if part == "**" {
next[0] = previous[0]
for index = 1; index <= names.length(); index = index + 1 {
next[index] = previous[index] || next[index - 1]
}
} else {
for index = 1; index <= names.length(); index = index + 1 {
next[index] = previous[index - 1] &&
document_glob_segment(part.to_owned(), names[index - 1].to_owned())
}
}
previous = next
}
previous[names.length()]
}
///|
fn document_glob_segment(pattern : String, name : String) -> Bool {
let chars = name.to_array()
let mut previous = Array::make(chars.length() + 1, false)
previous[0] = true
for ch in pattern.iter() {
let next = Array::make(chars.length() + 1, false)
next[0] = ch == '*' && previous[0]
for index = 1; index <= chars.length(); index = index + 1 {
next[index] = if ch == '*' {
previous[index] || next[index - 1]
} else {
previous[index - 1] && (ch == '?' || ch == chars[index - 1])
}
}
previous = next
}
previous[chars.length()]
}