use std::{
borrow::Cow,
collections::{BTreeMap, HashMap, HashSet},
convert::Infallible,
rc::Rc,
sync::Arc,
};
use crate::{
graph::{MontyGraph, MontyNode, NodeId},
object::MontyObject,
os::MontyPath,
unstable::{self, PushValue},
};
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize, monty_macros::ToArgs)]
pub struct ScanArgs {
pub path: MontyPath,
#[from_args(kw_only)]
pub max_depth: Option<u32>,
#[from_args(kw_only)]
pub follow_symlinks: bool,
#[from_args(kw_only)]
pub pattern: Option<Vec<String>>,
#[from_args(kw_only)]
pub case_sensitive: Option<bool>,
#[from_args(kw_only)]
pub recurse_symlinks: bool,
}
impl ScanArgs {
#[must_use]
pub fn listing(path: MontyPath, max_depth: Option<u32>, follow_symlinks: bool) -> Self {
Self {
path,
max_depth,
follow_symlinks,
pattern: None,
case_sensitive: None,
recurse_symlinks: false,
}
}
#[must_use]
pub fn glob(path: MontyPath, parts: Vec<String>, case_sensitive: Option<bool>, recurse_symlinks: bool) -> Self {
let recursive = parts.iter().any(|part| part == "**");
let depth = parts.iter().filter(|part| !is_special(part)).count();
let descends_wildcard = parts
.iter()
.take(parts.len().saturating_sub(1))
.any(|part| part != "**" && !is_special(part));
Self {
path,
max_depth: if recursive {
None
} else {
Some(u32::try_from(depth).unwrap_or(u32::MAX))
},
follow_symlinks: recurse_symlinks || descends_wildcard,
pattern: Some(parts),
case_sensitive,
recurse_symlinks,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EntryInfo {
pub is_dir: bool,
pub is_file: bool,
pub is_symlink: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScanEntry {
pub path: String,
pub info: EntryInfo,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ScanReply {
Tree(Vec<ScanEntry>),
Matches(Vec<String>),
}
impl ScanReply {
#[must_use]
pub fn into_object(self) -> MontyObject {
match self {
Self::Tree(entries) => scan_reply(entries),
Self::Matches(matches) => MontyObject::dict([(
MontyObject::string(MATCHES_KEY.to_owned()),
MontyObject::list(matches.into_iter().map(MontyObject::string)),
)]),
}
}
}
const MATCHES_KEY: &str = "matches";
#[must_use]
pub fn scan_reply(entries: impl IntoIterator<Item = ScanEntry>) -> MontyObject {
MontyObject::list(entries.into_iter().map(|entry| {
MontyObject::tuple([
MontyObject::string(entry.path),
MontyObject::bool(entry.info.is_dir),
MontyObject::bool(entry.info.is_file),
MontyObject::bool(entry.info.is_symlink),
])
}))
}
pub fn parse_scan_reply(reply: &MontyObject) -> Result<ScanReply, String> {
let (graph, _) = unstable::graph_parts(reply);
match unstable::root_node(reply) {
MontyNode::List(ids) => ids
.iter()
.map(|&id| parse_entry(graph, id))
.collect::<Result<_, _>>()
.map(ScanReply::Tree),
MontyNode::Dict(pairs) => match matches_list(graph, pairs) {
Some(ids) => ids
.iter()
.map(|&id| parse_match(graph, id))
.collect::<Result<_, _>>()
.map(ScanReply::Matches),
None => Err(format!(
"expected {{'{MATCHES_KEY}': [path, ...]}}, got a different dict"
)),
},
_ => Err(format!(
"expected a list of (path, is_dir, is_file, is_symlink) tuples, got {}",
reply.as_ref().type_name()
)),
}
}
fn matches_list<'g>(graph: &'g MontyGraph, pairs: &[(NodeId, NodeId)]) -> Option<&'g [NodeId]> {
let [(key, value)] = pairs else {
return None;
};
match (graph.node(*key), graph.node(*value)) {
(MontyNode::String(key), MontyNode::List(ids)) if key == MATCHES_KEY => Some(ids),
_ => None,
}
}
fn parse_match(graph: &MontyGraph, id: NodeId) -> Result<String, String> {
let (MontyNode::String(path) | MontyNode::Path(path)) = graph.node(id) else {
return Err(format!(
"expected glob matches to be paths, got {}",
graph.type_name(id)
));
};
let trimmed = path.trim_end_matches('/');
let normalized = if path.starts_with('/') {
None
} else {
normalize_relative(trimmed)
}
.ok_or_else(|| format!("glob match {path:?} is not relative to the scan root"))?;
let mut normalized = normalized.into_owned();
if trimmed.len() < path.len() && !normalized.is_empty() {
normalized.push('/');
}
Ok(normalized)
}
#[must_use]
pub fn indexed_size(reply: &MontyObject) -> usize {
const ENTRY_OVERHEAD: usize = 160;
let (graph, _) = unstable::graph_parts(reply);
let ids = match unstable::root_node(reply) {
MontyNode::List(ids) => ids.as_slice(),
MontyNode::Dict(pairs) => matches_list(graph, pairs).unwrap_or_default(),
_ => return 0,
};
ids.iter()
.map(|&id| {
let path_len = match graph.node(id) {
MontyNode::String(path) | MontyNode::Path(path) => path.len(),
MontyNode::Tuple(items) | MontyNode::List(items) | MontyNode::NamedTuple { values: items, .. } => {
match items.first().map(|&path| graph.node(path)) {
Some(MontyNode::String(path) | MontyNode::Path(path)) => path.len(),
_ => 0,
}
}
_ => 0,
};
path_len.saturating_mul(3).saturating_add(ENTRY_OVERHEAD)
})
.fold(0, usize::saturating_add)
}
fn parse_entry(graph: &MontyGraph, id: NodeId) -> Result<ScanEntry, String> {
let invalid = || {
format!(
"expected (path, is_dir, is_file, is_symlink) tuples, got {}",
graph.type_name(id)
)
};
let (MontyNode::Tuple(items) | MontyNode::List(items) | MontyNode::NamedTuple { values: items, .. }) =
graph.node(id)
else {
return Err(invalid());
};
let [path, is_dir, is_file, is_symlink] = items.as_slice() else {
return Err(invalid());
};
let flag = |id: &NodeId| match graph.node(*id) {
MontyNode::Bool(value) => Ok(*value),
_ => Err(invalid()),
};
let (MontyNode::String(path) | MontyNode::Path(path)) = graph.node(*path) else {
return Err(invalid());
};
let normalized = if path.starts_with('/') {
None
} else {
normalize_relative(path)
}
.ok_or_else(|| format!("scan entry path {path:?} is not relative to the scan root"))?;
Ok(ScanEntry {
path: normalized.into_owned(),
info: EntryInfo {
is_dir: flag(is_dir)?,
is_file: flag(is_file)?,
is_symlink: flag(is_symlink)?,
},
})
}
impl PushValue for Vec<String> {
fn push_into(self, graph: &mut MontyGraph) -> NodeId {
let items = self.into_iter().map(|item| item.push_into(graph)).collect();
graph.push(MontyNode::List(items))
}
}
pub trait ScanSource {
type Error;
fn list(&mut self, dir: &str) -> Result<Option<Listing>, Self::Error>;
fn lookup(&mut self, path: &str) -> Result<Option<EntryInfo>, Self::Error>;
fn visit(&mut self) -> Result<(), Self::Error>;
fn charge(&mut self, bytes: usize) -> Result<(), Self::Error>;
fn match_capacity(&self) -> usize {
usize::MAX
}
}
pub type Listing = Arc<[(String, EntryInfo)]>;
pub fn collect_scan<E>(args: &ScanArgs, source: &mut impl ScanSource<Error = E>) -> Result<Option<ScanReply>, E> {
let mut recorder = Recorder {
source,
listed: HashMap::new(),
entries: BTreeMap::new(),
};
let Some(root) = recorder.lookup("")? else {
return Ok(None);
};
if root.is_dir {
match &args.pattern {
Some(parts) => {
let selector = GlobSelector::new(parts, args.case_sensitive, args.recurse_symlinks);
if let Some(matches) = selector.select(&mut recorder)? {
return Ok(Some(ScanReply::Matches(matches)));
}
}
None => walk_to_depth(&mut recorder, args.max_depth, args.follow_symlinks)?,
}
}
let entries = recorder
.entries
.into_iter()
.map(|(path, info)| ScanEntry { path, info })
.collect();
Ok(Some(ScanReply::Tree(entries)))
}
fn walk_to_depth<E>(
source: &mut impl ScanSource<Error = E>,
max_depth: Option<u32>,
follow_symlinks: bool,
) -> Result<(), E> {
let mut stack = vec![(String::new(), 0_u32)];
while let Some((dir, depth)) = stack.pop() {
if max_depth.is_some_and(|max| depth >= max) {
continue;
}
for (name, info) in source.list(&dir)?.unwrap_or_default().iter() {
source.visit()?;
if info.is_dir && (follow_symlinks || !info.is_symlink) {
stack.push((join(&dir, name), depth + 1));
}
}
}
Ok(())
}
struct Recorder<'s, S> {
source: &'s mut S,
listed: HashMap<String, Option<Listing>>,
entries: BTreeMap<String, EntryInfo>,
}
impl<E, S: ScanSource<Error = E>> ScanSource for Recorder<'_, S> {
type Error = E;
fn list(&mut self, dir: &str) -> Result<Option<Listing>, E> {
let Some(dir) = normalize_relative(dir) else {
return Ok(None);
};
if let Some(listing) = self.listed.get(dir.as_ref()) {
return Ok(listing.clone());
}
if self.lookup(&dir)?.is_none_or(|info| !info.is_dir) {
self.listed.insert(dir.into_owned(), None);
return Ok(None);
}
let mut listing = self.source.list(&dir)?;
if let Some(children) = &mut listing
&& !children.is_sorted_by(|(a, _), (b, _)| a <= b)
{
Arc::make_mut(children).sort_by(|(a, _), (b, _)| a.cmp(b));
}
for (name, info) in listing.iter().flat_map(|children| children.iter()) {
self.entries.insert(join(&dir, name), *info);
}
self.listed.insert(dir.into_owned(), listing.clone());
Ok(listing)
}
fn lookup(&mut self, path: &str) -> Result<Option<EntryInfo>, E> {
let Some(path) = normalize_relative(path) else {
return Ok(None);
};
if let Some(info) = self.entries.get(path.as_ref()) {
return Ok(Some(*info));
}
let info = self.source.lookup(&path)?;
if let Some(info) = info {
self.entries.insert(path.into_owned(), info);
}
Ok(info)
}
fn visit(&mut self) -> Result<(), E> {
self.source.visit()
}
fn charge(&mut self, bytes: usize) -> Result<(), E> {
self.source.charge(bytes)
}
fn match_capacity(&self) -> usize {
self.entries.len()
}
}
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
pub struct ScanTree {
entries: HashMap<String, EntryInfo>,
children: HashMap<String, Listing>,
}
impl ScanTree {
#[must_use]
pub fn new(entries: Vec<ScanEntry>) -> Self {
let entries: HashMap<String, EntryInfo> = entries.into_iter().map(|entry| (entry.path, entry.info)).collect();
let mut children: HashMap<String, Vec<(String, EntryInfo)>> = HashMap::new();
for (path, info) in &entries {
if !path.is_empty() {
let (parent, name) = path.rsplit_once('/').unwrap_or(("", path));
let child = (name.to_owned(), *info);
match children.get_mut(parent) {
Some(siblings) => siblings.push(child),
None => {
children.insert(parent.to_owned(), vec![child]);
}
}
}
}
let children = children
.into_iter()
.map(|(dir, mut siblings)| {
siblings.sort_by(|(a, _), (b, _)| a.cmp(b));
(dir, Listing::from(siblings))
})
.collect();
Self { entries, children }
}
#[must_use]
pub fn root(&self) -> EntryInfo {
self.entries.get("").copied().unwrap_or(EntryInfo {
is_dir: true,
is_file: false,
is_symlink: false,
})
}
#[must_use]
pub fn described_root(&self) -> Option<EntryInfo> {
self.entries.get("").copied()
}
#[must_use]
pub fn children(&self, dir: &str) -> &[(String, EntryInfo)] {
self.children.get(dir).map_or(&[], |siblings| siblings)
}
#[must_use]
pub fn get(&self, path: &str) -> Option<EntryInfo> {
if path.is_empty() {
Some(self.root())
} else {
self.entries.get(path).copied()
}
}
}
impl ScanSource for ScanTree {
type Error = Infallible;
fn list(&mut self, dir: &str) -> Result<Option<Listing>, Infallible> {
let listing = normalize_relative(dir)
.filter(|dir| self.get(dir).is_some_and(|info| info.is_dir))
.map(|dir| self.children.get(dir.as_ref()).cloned().unwrap_or_default());
Ok(listing)
}
fn lookup(&mut self, path: &str) -> Result<Option<EntryInfo>, Infallible> {
Ok(normalize_relative(path).and_then(|path| self.get(&path)))
}
fn visit(&mut self) -> Result<(), Infallible> {
Ok(())
}
fn charge(&mut self, _bytes: usize) -> Result<(), Infallible> {
Ok(())
}
}
#[must_use]
pub fn split_literal_prefix(parts: &[String], case_sensitive: Option<bool>) -> (String, &[String]) {
let mut prefix = String::new();
let mut rest = parts;
while let Some((part, tail)) = rest.split_first()
&& part != "**"
&& (is_special(part) || (case_sensitive.is_none() && !has_magic(part)))
{
if !prefix.is_empty() || part.is_empty() {
prefix.push('/');
}
prefix.push_str(part);
rest = tail;
}
(prefix, rest)
}
#[derive(Debug)]
pub struct GlobSelector<'p> {
parts: &'p [String],
steps: Vec<Option<Step>>,
recurse_symlinks: bool,
}
#[derive(Debug)]
enum Step {
Exists,
Special,
Literal { joined: String, next: usize },
Wildcard {
matcher: Option<SegmentPattern>,
next: usize,
},
Recursive { matcher: Option<MultiPattern>, next: usize },
}
impl<'p> GlobSelector<'p> {
#[must_use]
pub fn new(parts: &'p [String], case_sensitive: Option<bool>, recurse_symlinks: bool) -> Self {
let mut steps: Vec<Option<Step>> = (0..=parts.len()).map(|_| None).collect();
let mut index = 0;
loop {
let step = compile_step(parts, index, case_sensitive, recurse_symlinks);
let next = match &step {
Step::Exists => None,
Step::Special => Some(index + 1),
Step::Literal { next, .. } | Step::Wildcard { next, .. } | Step::Recursive { next, .. } => Some(*next),
};
steps[index] = Some(step);
match next {
Some(next) => index = next,
None => break,
}
}
Self {
parts,
steps,
recurse_symlinks,
}
}
pub fn select<E>(&self, source: &mut impl ScanSource<Error = E>) -> Result<Option<Vec<String>>, E> {
let mut run = Run {
stack: Vec::new(),
seen: HashSet::new(),
out: Some(Vec::new()),
};
self.run(source, &mut run)?;
Ok(run.out)
}
fn run<E>(&self, source: &mut impl ScanSource<Error = E>, run: &mut Run) -> Result<(), E> {
run.push(0, "", false, source)?;
while let Some((index, path, exists)) = run.stack.pop() {
let step = self.steps[index]
.as_ref()
.expect("states are only pushed by the step chain compiled from part 0");
match step {
Step::Exists => {
if exists || lexists(&path, source)? {
run.emit(path.to_string(), source);
}
}
Step::Special => {
let mut path = format!("{path}{}", self.parts[index]);
if index + 1 < self.parts.len() {
path.push('/');
}
run.push(index + 1, &path, exists, source)?;
}
Step::Literal { joined, next } => run.push(*next, &format!("{path}{joined}"), false, source)?,
Step::Wildcard { matcher, next } => {
let dir_only = *next < self.parts.len();
for (name, info) in source.list(path.trim_end_matches('/'))?.unwrap_or_default().iter() {
source.visit()?;
if matcher.as_ref().is_none_or(|matcher| matcher.matches(name)) {
let entry = join(&path, name);
if !dir_only {
run.emit(entry, source);
} else if info.is_dir {
run.push(*next, &(entry + "/"), true, source)?;
}
}
}
}
Step::Recursive { matcher, next } => {
self.select_recursive(matcher.as_ref(), *next, &path, exists, source, run)?;
}
}
}
Ok(())
}
fn select_recursive<E>(
&self,
matcher: Option<&MultiPattern>,
next: usize,
path: &str,
exists: bool,
source: &mut impl ScanSource<Error = E>,
run: &mut Run,
) -> Result<(), E> {
let dir_only = next < self.parts.len();
let match_pos = path.len();
if matcher.is_none_or(|matcher| matcher.matches("")) {
run.push(next, path, exists, source)?;
}
let mut dirs = vec![path.to_owned()];
while let Some(dir) = dirs.pop() {
for (name, info) in source.list(dir.trim_end_matches('/'))?.unwrap_or_default().iter() {
source.visit()?;
let is_dir = info.is_dir && (self.recurse_symlinks || !info.is_symlink);
if is_dir || !dir_only {
let entry = join(&dir, name);
let matched = matcher.is_none_or(|matcher| matcher.matches(&entry[match_pos..]));
let entry = if dir_only { entry + "/" } else { entry };
if is_dir {
if matched {
if dir_only {
run.push(next, &entry, true, source)?;
} else {
run.emit(entry.clone(), source);
}
}
dirs.push(entry);
} else if matched {
run.emit(entry, source);
}
}
}
}
Ok(())
}
}
type State = (usize, Rc<str>, bool);
const STATE_OVERHEAD: usize = 2 * size_of::<State>() + 2 * size_of::<usize>();
struct Run {
stack: Vec<State>,
seen: HashSet<State>,
out: Option<Vec<String>>,
}
impl Run {
fn push<E>(
&mut self,
index: usize,
path: &str,
exists: bool,
source: &mut impl ScanSource<Error = E>,
) -> Result<(), E> {
let trimmed_len = path.trim_end_matches('/').len();
let dir = trimmed_len > 0 && trimmed_len < path.len();
let normalized: Rc<str> = match normalize_relative(&path[..trimmed_len]) {
None => return Ok(()),
Some(Cow::Owned(mut normalized)) => {
if dir && !normalized.is_empty() {
normalized.push('/');
}
normalized.into()
}
Some(Cow::Borrowed(_)) => path[..trimmed_len + usize::from(dir)].into(),
};
let state = (index, normalized, exists);
if !self.seen.contains(&state) {
source.charge(STATE_OVERHEAD + state.1.len())?;
self.seen.insert(state.clone());
self.stack.push(state);
}
Ok(())
}
fn emit<E>(&mut self, path: String, source: &impl ScanSource<Error = E>) {
if let Some(out) = &mut self.out {
if out.len() < source.match_capacity() {
out.push(path);
} else {
self.out = None;
}
}
}
}
fn compile_step(parts: &[String], index: usize, case_sensitive: Option<bool>, recurse_symlinks: bool) -> Step {
let Some(part) = parts.get(index) else {
return Step::Exists;
};
let pedantic = case_sensitive.is_some();
let case_sensitive = case_sensitive.unwrap_or(true);
if part == "**" {
let mut next = index + 1;
while parts.get(next).is_some_and(|part| part == "**") {
next += 1;
}
let mut merged = vec![MultiPart::Recursive];
if recurse_symlinks {
while let Some(part) = parts.get(next).filter(|part| !is_special(part)) {
merged.push(MultiPart::compile(part, case_sensitive));
next += 1;
}
}
let matcher = (merged.len() > 1).then_some(MultiPattern(merged));
Step::Recursive { matcher, next }
} else if is_special(part) {
Step::Special
} else if !pedantic && !has_magic(part) {
let mut joined = part.clone();
let mut next = index + 1;
while let Some(part) = parts.get(next).filter(|part| !has_magic(part)) {
joined.push('/');
joined.push_str(part);
next += 1;
}
if next < parts.len() {
joined.push('/');
}
Step::Literal { joined, next }
} else {
let matcher = (part != "*").then(|| SegmentPattern::new(part, case_sensitive));
Step::Wildcard {
matcher,
next: index + 1,
}
}
}
fn lexists<E>(path: &str, source: &mut impl ScanSource<Error = E>) -> Result<bool, E> {
let info = source.lookup(path.trim_end_matches('/'))?;
Ok(if path.is_empty() || path.ends_with('/') {
info.is_some_and(|info| info.is_dir)
} else {
info.is_some()
})
}
fn is_special(part: &str) -> bool {
matches!(part, "" | "." | "..")
}
fn has_magic(part: &str) -> bool {
part.contains(['*', '?', '['])
}
fn join(dir: &str, name: &str) -> String {
if dir.is_empty() || dir.ends_with('/') {
format!("{dir}{name}")
} else {
format!("{dir}/{name}")
}
}
#[must_use]
pub fn normalize_relative(path: &str) -> Option<Cow<'_, str>> {
if path.is_empty() || path.split('/').all(|part| !is_special(part)) {
Some(Cow::Borrowed(path))
} else {
let mut parts: Vec<&str> = Vec::new();
for part in path.split('/') {
match part {
"" | "." => {}
".." => {
parts.pop()?;
}
part => parts.push(part),
}
}
Some(Cow::Owned(parts.join("/")))
}
}
#[derive(Debug, Clone)]
pub struct SegmentPattern {
tokens: Vec<Token>,
case_sensitive: bool,
}
#[derive(Debug, Clone, PartialEq)]
enum Token {
Char(char),
Any,
Star,
Class { negated: bool, items: Vec<(char, char)> },
}
impl SegmentPattern {
#[must_use]
pub fn new(pattern: &str, case_sensitive: bool) -> Self {
let chars: Vec<char> = pattern.chars().collect();
let mut tokens = Vec::new();
let mut i = 0;
let mut unterminated = false;
while let Some(&c) = chars.get(i) {
i += 1;
match c {
'*' => {
if tokens.last() != Some(&Token::Star) {
tokens.push(Token::Star);
}
}
'?' => tokens.push(Token::Any),
'[' if !unterminated => {
if let Some((token, end)) = parse_class(&chars, i) {
tokens.push(token);
i = end;
} else {
unterminated = true;
tokens.push(Token::Char('['));
}
}
c => tokens.push(Token::Char(c)),
}
}
Self { tokens, case_sensitive }
}
#[must_use]
pub fn matches(&self, name: &str) -> bool {
let (mut p, mut t) = (0, 0);
let mut star: Option<(usize, usize)> = None;
while let Some(c) = name[t..].chars().next() {
match self.tokens.get(p) {
Some(Token::Star) => {
star = Some((p, t));
p += 1;
}
Some(token) if self.token_matches(token, c) => {
p += 1;
t += c.len_utf8();
}
_ => match star {
Some((star_p, star_t)) => {
let absorbed = name[star_t..].chars().next().map_or(1, char::len_utf8);
p = star_p + 1;
t = star_t + absorbed;
star = Some((star_p, t));
}
None => return false,
},
}
}
self.tokens[p..].iter().all(|token| *token == Token::Star)
}
fn token_matches(&self, token: &Token, c: char) -> bool {
let variants = if self.case_sensitive {
[c, c, c, c]
} else {
let lower = simple_lower(c);
[c, lower, simple_upper(lower), simple_lower(simple_upper(c))]
};
match token {
Token::Char(expected) => variants
.iter()
.any(|v| *v == *expected || (!self.case_sensitive && simple_lower(*expected) == simple_lower(*v))),
Token::Any => true,
Token::Star => false,
Token::Class { negated, items } => {
let hit = variants
.iter()
.any(|v| items.iter().any(|(low, high)| (*low..=*high).contains(v)));
hit != *negated
}
}
}
}
fn parse_class(chars: &[char], start: usize) -> Option<(Token, usize)> {
let mut end = start;
if chars.get(end) == Some(&'!') {
end += 1;
}
if chars.get(end) == Some(&']') {
end += 1;
}
while chars.get(end).is_some_and(|c| *c != ']') {
end += 1;
}
if end >= chars.len() {
return None;
}
let (negated, body) = match &chars[start..end] {
['!', body @ ..] => (true, body),
body => (false, body),
};
let mut items = Vec::new();
let mut k = 0;
while let Some(&c) = body.get(k) {
if let (Some('-'), Some(&high)) = (body.get(k + 1), body.get(k + 2)) {
items.push((c, high));
k += 3;
} else {
items.push((c, c));
k += 1;
}
}
Some((Token::Class { negated, items }, end + 1))
}
fn simple_lower(c: char) -> char {
c.to_lowercase().next().unwrap_or(c)
}
fn simple_upper(c: char) -> char {
let mut upper = c.to_uppercase();
match (upper.next(), upper.next()) {
(Some(upper), None) => upper,
_ => c,
}
}
#[derive(Debug)]
struct MultiPattern(Vec<MultiPart>);
#[derive(Debug)]
enum MultiPart {
Recursive,
Segment(SegmentPattern),
}
impl MultiPart {
fn compile(part: &str, case_sensitive: bool) -> Self {
if part == "**" {
Self::Recursive
} else {
Self::Segment(SegmentPattern::new(part, case_sensitive))
}
}
}
impl MultiPattern {
fn matches(&self, path: &str) -> bool {
let segments: Vec<&str> = if path.is_empty() {
Vec::new()
} else {
path.split('/').collect()
};
let mut reachable = vec![false; segments.len() + 1];
reachable[0] = true;
for (index, part) in self.0.iter().enumerate() {
let mut next = vec![false; segments.len() + 1];
match part {
MultiPart::Recursive => {
let at_least_one = index + 1 == self.0.len();
let mut any = false;
for (position, slot) in next.iter_mut().enumerate() {
if !at_least_one {
any |= reachable[position];
}
*slot = any;
if at_least_one {
any |= reachable[position];
}
}
}
MultiPart::Segment(pattern) => {
for (position, segment) in segments.iter().enumerate() {
if reachable[position] && pattern.matches(segment) {
next[position + 1] = true;
}
}
}
}
reachable = next;
}
reachable[segments.len()]
}
}