///|
/// Structured failures produced while compiling a bounded pattern-list.
pub(all) suberror PatternError {
EmptyPatternList
EmptyPattern(index~ : Int)
PatternTooLong(length~ : Int, limit~ : Int)
TooManyPatterns(count~ : Int, limit~ : Int)
MissingMatchArgument(condition~ : String)
InvalidAllCondition
UnsupportedMatchCondition(name~ : String)
} derive(Debug, Eq)
///|
struct CompiledPattern {
source_index : Int
negated : Bool
glob : String
} derive(Debug, Eq)
///|
/// A checked OpenSSH pattern-list. Construct it with `compile_pattern_list`
/// before using it in a resolver or Match condition.
pub struct PatternList {
patterns : Array[CompiledPattern]
} derive(Debug, Eq)
///|
/// Explainable result of evaluating a pattern-list. Indices refer to the
/// original input array, are zero-based, and remain populated even when a
/// negative item excludes a matching positive item.
pub(all) struct PatternDecision {
matched : Bool
positive_index : Int?
negative_index : Int?
} derive(Debug, Eq)
///|
fn compile_item(item : String, source_index : Int) -> CompiledPattern {
if item.length() > 0 && item[0] == '!' {
{ source_index, negated: true, glob: lower_ascii(item[1:].to_owned()) }
} else {
{ source_index, negated: false, glob: lower_ascii(item) }
}
}
///|
/// Compile a pattern-list using the P0 resource limits.
pub fn compile_pattern_list(
patterns : Array[String],
) -> PatternList raise PatternError {
compile_pattern_list_with_limits(patterns)
}
///|
/// Compile a pattern-list with caller-supplied limits. This is useful for
/// fuzzing and for embedders which need tighter request budgets.
pub fn compile_pattern_list_with_limits(
patterns : Array[String],
pattern_length_limit? : Int = default_pattern_length_limit,
pattern_count_limit? : Int = default_pattern_list_limit,
) -> PatternList raise PatternError {
if patterns.length() == 0 {
raise EmptyPatternList
}
if patterns.length() > pattern_count_limit {
raise TooManyPatterns(count=patterns.length(), limit=pattern_count_limit)
}
let compiled : Array[CompiledPattern] = []
for index = 0; index < patterns.length(); index = index + 1 {
let item = patterns[index]
if item.is_empty() || item == "!" {
raise EmptyPattern(index~)
}
if item.length() > pattern_length_limit {
raise PatternTooLong(length=item.length(), limit=pattern_length_limit)
}
compiled.push(compile_item(item, index))
}
{ patterns: compiled }
}
///|
/// Evaluate a checked list against a value. A matching negative pattern has
/// precedence regardless of where it appears in the list.
pub fn PatternList::decide(
self : PatternList,
value : String,
) -> PatternDecision {
if value.length() > default_match_value_length_limit {
return { matched: false, positive_index: None, negative_index: None }
}
let normalized_value = lower_ascii(value)
let mut positive_index : Int? = None
let mut negative_index : Int? = None
for item in self.patterns {
if glob_from(item.glob, 0, normalized_value, 0) {
if item.negated {
if negative_index is None {
negative_index = Some(item.source_index)
}
} else if positive_index is None {
positive_index = Some(item.source_index)
}
}
}
{
matched: positive_index is Some(_) && negative_index is None,
positive_index,
negative_index,
}
}
///|
/// Evaluate a checked list without recording explain information.
pub fn PatternList::matches(self : PatternList, value : String) -> Bool {
self.decide(value).matched
}