// builtin_operators.mbt — Regex-based and IP-based matcher operators.
//
// KeyMatch2..5, KeyGet2/3, RegexMatch, GlobMatch, and IPMatch mirror
// Casbin's `util/builtin_operators.go`. The rewrite-and-match operators
// follow Casbin exactly: the pattern is rewritten textually and then
// matched as an anchored regular expression through the official
// `moonbitlang/regexp` engine, which reproduces Casbin's behavior —
// including how stray regex metacharacters in a pattern are interpreted.
// Casbin's `keyMatchShortcut` optimization is reproduced as well: patterns
// without regex metacharacters skip the engine, which also means plain
// patterns are never regex-interpreted.
//
// GlobMatch approximates `doublestar.Match` by translating the pattern to a
// regular expression: `**` crosses path separators, `*` and `?` do not,
// `[...]` classes are supported (`!` or `^` negates), and `{a,b}` provides
// alternation without nesting. Invalid patterns raise `MatcherEval`.
//
// Casbin raises (and recovers into errors) for invalid regex, invalid glob,
// and invalid IP arguments; here the same situations raise `MatcherEval`.
///|
fn key_match2_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("keyMatch2", arguments)
Value::Bool(key_match2(key1, key2))
}
///|
fn key_match3_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("keyMatch3", arguments)
Value::Bool(key_match3(key1, key2))
}
///|
fn key_match4_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("keyMatch4", arguments)
Value::Bool(key_match4(key1, key2))
}
///|
fn key_match5_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("keyMatch5", arguments)
Value::Bool(key_match5(key1, key2))
}
///|
fn key_get2_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2, path_var) = three_string_arguments("keyGet2", arguments)
Value::String(key_get2(key1, key2, path_var))
}
///|
fn key_get3_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2, path_var) = three_string_arguments("keyGet3", arguments)
Value::String(key_get3(key1, key2, path_var))
}
///|
fn regex_match_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("regexMatch", arguments)
Value::Bool(regex_match(key1, key2))
}
///|
fn glob_match_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("globMatch", arguments)
Value::Bool(glob_match(key1, key2))
}
///|
fn ip_match_function(arguments : Array[Value]) -> Value raise CasbinError {
let (key1, key2) = two_string_arguments("ipMatch", arguments)
Value::Bool(ip_match(key1, key2))
}
///|
/// Casbin's `keyMatch2`: `:param` matches one non-empty path segment and
/// `/*` matches the rest of the path.
fn key_match2(key1 : String, key2 : String) -> Bool raise CasbinError {
match key_match_shortcut(key1, key2, ":") {
Some(matched) => matched
None => {
let (pattern, _tokens) = rewrite_colon_params(
strip_slash_star(key2),
"[^/]+",
)
regex_match_anchored(pattern, key1)
}
}
}
///|
/// Casbin's `keyMatch3`: like keyMatch2 but with `{param}` placeholders.
fn key_match3(key1 : String, key2 : String) -> Bool raise CasbinError {
match key_match_shortcut(key1, key2, "") {
Some(matched) => matched
None => {
let (pattern, _tokens) = rewrite_braces(
strip_slash_star(key2),
"[^/]+",
true,
)
regex_match_anchored(pattern, key1)
}
}
}
///|
/// Casbin's `keyMatch4`: `{param}` placeholders; repeated placeholders must
/// capture equal values.
fn key_match4(key1 : String, key2 : String) -> Bool raise CasbinError {
match key_match_shortcut(key1, key2, "") {
Some(matched) => matched
None => {
let (pattern, tokens) = rewrite_braces(
strip_slash_star(key2),
"([^/]+)",
true,
)
let compiled = compile_pattern("^" + pattern + "$")
let result = compiled.execute(key1)
if !result.matched() {
return false
}
let captures = result.results()
if captures.length() != tokens.length() + 1 {
raise casbin_error(
MatcherEval,
"keyMatch4: number of tokens is not equal to number of values",
)
}
let seen : Array[(String, String)] = []
for i in 0.. capture.to_owned()
None => ""
}
match find_pair_value(seen, tokens[i]) {
Some(existing) => if existing != value { return false }
None => seen.push((tokens[i], value))
}
}
true
}
}
}
///|
/// Casbin's `keyMatch5`: `{param}` placeholders, and the query string of
/// `key1` is ignored.
fn key_match5(key1 : String, key2 : String) -> Bool raise CasbinError {
let key1 = match key1.find("?") {
Some(index) => key1[:index].to_owned()
None => key1
}
match key_match_shortcut(key1, key2, "") {
Some(matched) => matched
None => {
let (pattern, _tokens) = rewrite_braces(
strip_slash_star(key2),
"[^/]+",
true,
)
regex_match_anchored(pattern, key1)
}
}
}
///|
/// Casbin's `keyGet2`: the value captured for `path_var` by a `:param`
/// pattern, or the empty string.
fn key_get2(
key1 : String,
key2 : String,
path_var : String,
) -> String raise CasbinError {
let (pattern, tokens) = rewrite_colon_params(
strip_slash_star(key2),
"([^/]+)",
)
let compiled = compile_pattern("^" + pattern + "$")
let result = compiled.execute(key1)
if !result.matched() {
return ""
}
let captures = result.results()
for i in 0.. capture.to_owned()
None => ""
}
}
}
""
}
///|
/// Casbin's `keyGet3`: the value captured for `path_var` by a `{param}`
/// pattern, or the empty string.
fn key_get3(
key1 : String,
key2 : String,
path_var : String,
) -> String raise CasbinError {
let (pattern, tokens) = rewrite_braces(
strip_slash_star(key2),
"([^/]+?)",
false,
)
let compiled = compile_pattern("^" + pattern + "$")
let result = compiled.execute(key1)
if !result.matched() {
return ""
}
let captures = result.results()
for i in 0.. capture.to_owned()
None => ""
}
}
}
""
}
///|
/// Casbin's `regexMatch`: an unanchored regular expression search.
fn regex_match(key1 : String, key2 : String) -> Bool raise CasbinError {
let compiled = compile_pattern(key2)
match compiled.match_(key1) {
Some(_) => true
None => false
}
}
///|
/// Casbin's `globMatch` (`doublestar.Match(key2, key1)`).
fn glob_match(key1 : String, key2 : String) -> Bool raise CasbinError {
let pattern = glob_to_regex(key2)
regex_match_anchored(pattern, key1)
}
///|
/// Casbin's `keyMatchShortcut`: `*` matches everything, and patterns
/// without regex metacharacters are plain equality comparisons.
fn key_match_shortcut(
key1 : String,
key2 : String,
extra_chars : String,
) -> Bool? {
if key2 == "*" {
return Some(true)
}
if !key2.contains_any(chars="\\.+*?()|[]{}^$") &&
!key2.contains_any(chars=extra_chars) {
return Some(key1 == key2)
}
None
}
///|
fn strip_slash_star(pattern : String) -> String {
pattern.replace_all(old="/*", new="/.*")
}
///|
/// Rewrites `:name` placeholders into `replacement`, recording the names in
/// order of appearance.
fn rewrite_colon_params(
pattern : String,
replacement : String,
) -> (String, Array[String]) {
let chars : Array[Char] = pattern.iter().collect()
let output : Array[Char] = []
let tokens : Array[String] = []
let mut index = 0
while index < chars.length() {
if chars[index] == ':' {
let mut end = index + 1
while end < chars.length() && chars[end] != '/' {
end += 1
}
if end > index + 1 {
tokens.push(slice_to_string(chars, index + 1, end))
push_text(output, replacement)
index = end
continue
}
}
output.push(chars[index])
index += 1
}
(StringView::from_iter(output.iter()).to_owned(), tokens)
}
///|
/// Rewrites `{name}` placeholders into `replacement`, recording the names in
/// order of appearance. `greedy` selects Casbin's greedy `\{[^/]+\}` for
/// keyMatch3/5; non-greedy selects `\{[^/]+?\}` for keyGet3.
fn rewrite_braces(
pattern : String,
replacement : String,
greedy : Bool,
) -> (String, Array[String]) {
let chars : Array[Char] = pattern.iter().collect()
let output : Array[Char] = []
let tokens : Array[String] = []
let mut index = 0
while index < chars.length() {
if chars[index] == '{' {
// The match may not cross a path separator.
let mut limit = index + 1
while limit < chars.length() && chars[limit] != '/' {
limit += 1
}
let closing = if greedy {
last_brace_before(chars, index + 1, limit)
} else {
first_brace_before(chars, index + 1, limit)
}
match closing {
Some(end) =>
if end > index + 1 {
tokens.push(slice_to_string(chars, index + 1, end))
push_text(output, replacement)
index = end + 1
continue
}
None => ()
}
}
output.push(chars[index])
index += 1
}
// Escape the braces that survived the rewrite: they cannot be placeholders
// any more, Go's regexp treats a stray `{` as a literal, and this engine
// raises `MissingRepeatArgument` for it.
let escaped : Array[Char] = []
for ch in output.iter() {
if ch == '{' || ch == '}' {
escaped.push('\\')
}
escaped.push(ch)
}
(StringView::from_iter(escaped.iter()).to_owned(), tokens)
}
///|
fn first_brace_before(chars : Array[Char], start : Int, limit : Int) -> Int? {
for i in start.. Int? {
let mut found : Int? = None
for i in start.. String raise CasbinError {
let chars : Array[Char] = pattern.iter().collect()
let output : Array[Char] = []
let mut index = 0
while index < chars.length() {
let ch = chars[index]
match ch {
'*' => {
let start = index
let mut end = index
while end < chars.length() && chars[end] == '*' {
end += 1
}
let run = end - start
let at_segment_start = start == 0 || chars[start - 1] == '/'
let at_segment_end = end == chars.length() || chars[end] == '/'
if run >= 2 && at_segment_start && at_segment_end {
if start == 0 && end == chars.length() {
// A bare `**` matches everything.
push_text(output, ".*")
index = end
} else if end == chars.length() {
// A trailing `/**` also matches the directory itself, so the
// separator before it becomes optional.
ignore(output.pop())
push_text(output, "(/.*)?")
index = end
} else {
// `**/` matches zero or more leading segments; the separator is
// part of the group, so consume it here.
push_text(output, "(.*/)?")
index = end + 1
}
} else {
// A single star, or a doublestar glued to a name, does not cross
// path separators.
push_text(output, "[^/]*")
index = end
}
continue
}
'?' => push_text(output, "[^/]")
'[' => {
let mut end = index + 1
if end < chars.length() && (chars[end] == '!' || chars[end] == '^') {
end += 1
}
while end < chars.length() && chars[end] != ']' {
end += 1
}
if end >= chars.length() {
raise casbin_error(
MatcherEval,
"globMatch: unterminated character class in \"" + pattern + "\"",
)
}
output.push('[')
let mut inner = index + 1
if inner < end && chars[inner] == '!' {
output.push('^')
inner += 1
}
while inner <= end {
output.push(chars[inner])
inner += 1
}
index = end + 1
continue
}
'{' => {
let mut end = index + 1
while end < chars.length() && chars[end] != '}' {
end += 1
}
if end >= chars.length() {
raise casbin_error(
MatcherEval,
"globMatch: unterminated alternation in \"" + pattern + "\"",
)
}
let body = slice_to_string(chars, index + 1, end)
output.push('(')
for option in body.split(",") {
// Translate each alternative as a glob so `*` and `?` keep their
// glob meaning inside `{...}`.
push_text(output, glob_to_regex(option.to_owned()))
output.push('|')
}
// Replace the trailing '|' with the closing parenthesis.
ignore(output.pop())
output.push(')')
index = end + 1
continue
}
'.' | '+' | '(' | ')' | '|' | '^' | '$' | '\\' => {
output.push('\\')
output.push(ch)
}
_ => output.push(ch)
}
index += 1
}
StringView::from_iter(output.iter()).to_owned()
}
///|
fn regex_match_anchored(
pattern : String,
text : String,
) -> Bool raise CasbinError {
let compiled = compile_pattern("^" + pattern + "$")
match compiled.match_(text) {
Some(_) => true
None => false
}
}
///|
fn compile_pattern(pattern : String) -> @regexp.Regexp raise CasbinError {
@regexp.compile(pattern) catch {
error =>
raise casbin_error(
MatcherEval,
"invalid regular expression \"" + pattern + "\": " + error.to_string(),
)
}
}
///|
fn three_string_arguments(
name : String,
arguments : Array[Value],
) -> (String, String, String) raise CasbinError {
if arguments.length() != 3 {
raise casbin_error(
MatcherEval,
name + ": expected 3 arguments, but got " + arguments.length().to_string(),
)
}
let first = match arguments[0] {
Value::String(value) => value
other =>
raise casbin_error(
MatcherEval,
name + ": argument must be a string, got " + other.type_name(),
)
}
let second = match arguments[1] {
Value::String(value) => value
other =>
raise casbin_error(
MatcherEval,
name + ": argument must be a string, got " + other.type_name(),
)
}
let third = match arguments[2] {
Value::String(value) => value
other =>
raise casbin_error(
MatcherEval,
name + ": argument must be a string, got " + other.type_name(),
)
}
(first, second, third)
}
///|
fn find_pair_value(pairs : Array[(String, String)], key : String) -> String? {
for pair in pairs {
if pair.0 == key {
return Some(pair.1)
}
}
None
}
///|
fn push_text(output : Array[Char], text : String) -> Unit {
for ch in text.iter() {
output.push(ch)
}
}