Module: Ruby::Rego::Builtins::Regex

Extended by:
RegistryHelpers
Defined in:
lib/ruby/rego/builtins/regex.rb,
lib/ruby/rego/builtins/regex/replace.rb,
lib/ruby/rego/builtins/regex/matching.rb,
lib/ruby/rego/builtins/regex/template.rb,
lib/ruby/rego/builtins/regex/compilation.rb,
lib/ruby/rego/builtins/regex/go_template.rb,
lib/ruby/rego/builtins/regex/glob_intersection.rb,
lib/ruby/rego/builtins/regex/glob_intersection/trim.rb,
lib/ruby/rego/builtins/regex/glob_intersection/tokenizer.rb,
lib/ruby/rego/builtins/regex/glob_intersection/intersector.rb

Overview

Built-in regex helpers (regex.globs_match).

Defined Under Namespace

Modules: GlobIntersection Classes: GoTemplate

Constant Summary collapse

REGEX_FUNCTIONS =
{
  "regex.match" => { arity: 2, handler: :match },
  "regex.is_valid" => { arity: 1, handler: :is_valid },
  "regex.split" => { arity: 2, handler: :split },
  "regex.find_n" => { arity: 3, handler: :find_n },
  "regex.find_all_string_submatch_n" => { arity: 3, handler: :find_all_string_submatch_n },
  "regex.template_match" => { arity: 4, handler: :template_match },
  "regex.globs_match" => { arity: 2, handler: :globs_match },
  "regex.replace" => { arity: 3, handler: :replace }
}.freeze
REGEX_TIMEOUT_SECONDS =

Wall-clock budget for one regex builtin operation. OPA’s RE2 engine is linear-time and immune to catastrophic backtracking; Ruby’s Onigmo engine is not, so a hostile pattern or input could otherwise hang the evaluator. Applied two ways: as the per-match engine timeout (Regexp.new(timeout:), interrupts a single catastrophic search) and as an aggregate deadline across the match loop (a cheap-per-match pattern over a long subject is O(matches) engine scans, which the per-match timeout — reset each search — does not bound). Either path yields undefined. Override with RUBY_REGO_REGEX_TIMEOUT (seconds).

ENV.fetch("RUBY_REGO_REGEX_TIMEOUT", "1.0").to_f
MAX_REPLACE_OUTPUT =

Upper bound on regex.replace output. The per-match engine timeout does not cover template expansion in the gsub block, so many cheap matches with a large replacement template could otherwise expand output super-linearly (matches x template length). Exceeding this yields undefined — a deliberate anti-DoS divergence from OPA, in the same spirit as the regex timeout above.

32_000_000
MAX_REPLACE_WORK =

Upper bound on total template-segment expansions (matches x template segments). Output size alone is insufficient: references that resolve to empty (an out-of-range $9 or unknown ${name}) do CPU work per segment while emitting nothing, so a large template over many matches would otherwise run unbounded under the output cap. Exceeding this yields undefined.

32_000_000
MAX_REGEX_SOURCE =

Maximum byte length of a pattern or replacement template. Both are split into a character array (String#chars) up front — an uninterruptible O(n) C call that no cooperative deadline can bound (the same category as compile cost) — before the per-element processing runs. An oversized source is rejected here by an O(1) bytesize check rather than materialized; the resulting char array then bounds the downstream scan/parse/expand loops, which are all O(source). ~1000x any real pattern or template; exceeding it yields undefined.

1_000_000
GROUP_NAME_CHAR =

A single RE2 group-name character ([A-Za-z0-9_]). Used to scan a (?P<name> / (?<name> header forward one identifier char at a time, which bounds the scan to the name’s length and stops at the first non-identifier — keeping pattern preprocessing linear even for adversarial input like (?P< repeated with no >.

/[A-Za-z0-9_]/
GLOBS_MATCH_CONTEXT =
"regex.globs_match"

Class Method Summary collapse

Methods included from RegistryHelpers

register_configured_functions

Class Method Details

.find_all_string_submatch_n(pattern_value, string_value, number_value) ⇒ Ruby::Rego::ArrayValue

:reek:TooManyStatements

Parameters:

Returns:



161
162
163
164
165
166
167
# File 'lib/ruby/rego/builtins/regex.rb', line 161

def self.find_all_string_submatch_n(pattern_value, string_value, number_value)
  context = "regex.find_all_string_submatch_n"
  rows = submatches_for(pattern_value, string_value, context)
  limit = NumericHelpers.integer_value(number_value, context: context)
  selected = limit.negative? ? rows : rows.first(limit)
  ArrayValue.new(selected.map { |row| string_array(row) })
end

.find_n(pattern_value, string_value, number_value) ⇒ Ruby::Rego::ArrayValue

Parameters:

Returns:



150
151
152
153
154
# File 'lib/ruby/rego/builtins/regex.rb', line 150

def self.find_n(pattern_value, string_value, number_value)
  matches = matches_for(pattern_value, string_value, "regex.find_n")
  limit = NumericHelpers.integer_value(number_value, context: "regex.find_n")
  string_array(limit.negative? ? matches : matches.first(limit))
end

.globs_match(glob1_value, glob2_value) ⇒ Ruby::Rego::BooleanValue

Whether the intersection of two glob patterns matches a non-empty string. An invalid glob (or a DoS-bound breach) yields undefined, matching OPA.

Parameters:

Returns:



18
19
20
21
22
23
24
# File 'lib/ruby/rego/builtins/regex/glob_intersection.rb', line 18

def self.globs_match(glob1_value, glob2_value)
  lhs = string_arg(glob1_value, GLOBS_MATCH_CONTEXT)
  rhs = string_arg(glob2_value, GLOBS_MATCH_CONTEXT)
  BooleanValue.new(GlobIntersection.non_empty?(lhs, rhs))
rescue GlobIntersection::GlobError => e
  raise_invalid_glob(e.message)
end

.is_valid(pattern_value) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



108
109
110
111
112
113
114
115
116
117
118
# File 'lib/ruby/rego/builtins/regex.rb', line 108

def self.is_valid(pattern_value)
  # OPA's regex.is_valid is total over runtime values: a non-string argument is
  # `false`, not undefined (unlike the other regex built-ins, which type-error to
  # undefined). An over-length pattern is also rejected as not-valid rather than
  # processed (anti-DoS cap; OPA, having no cap, reports a large valid pattern true).
  return BooleanValue.new(false) unless pattern_value.is_a?(StringValue)

  pattern = pattern_value.value
  valid = pattern.valid_encoding? && !source_too_long?(pattern) && compilable?(pattern)
  BooleanValue.new(valid)
end

.match(pattern_value, string_value) ⇒ Ruby::Rego::BooleanValue

Parameters:

Returns:



100
101
102
103
104
# File 'lib/ruby/rego/builtins/regex.rb', line 100

def self.match(pattern_value, string_value)
  regexp = compile(pattern_value, "regex.match")
  string = string_arg(string_value, "regex.match")
  guarded("regex.match") { BooleanValue.new(regexp.match?(string)) }
end

.register!Ruby::Rego::Builtins::BuiltinRegistry



89
90
91
92
93
# File 'lib/ruby/rego/builtins/regex.rb', line 89

def self.register!
  registry = BuiltinRegistry.instance
  register_configured_functions(registry, REGEX_FUNCTIONS)
  registry
end

.replace(string_value, pattern_value, replacement_value) ⇒ Ruby::Rego::StringValue

Replaces every match of pattern in string with replacement, expanding Go’s Expand template syntax in the replacement ($1/${name}/$$), matching OPA’s regex.replace. A backslash is a literal; only $ is special.

Parameters:

Returns:



16
17
18
19
20
21
22
23
# File 'lib/ruby/rego/builtins/regex/replace.rb', line 16

def self.replace(string_value, pattern_value, replacement_value)
  regexp, names = compile_pattern(string_arg(pattern_value, "regex.replace"), "regex.replace")
  string = string_arg(string_value, "regex.replace")
  replacement = string_arg(replacement_value, "regex.replace")
  assert_source_length(replacement, "regex.replace")
  template = GoTemplate.new(replacement, names)
  guarded("regex.replace") { StringValue.new(expand_all(string, regexp, template)) }
end

.split(pattern_value, string_value) ⇒ Ruby::Rego::ArrayValue

Splits a string on a pattern, matching Go’s regexp.Split (n = -1): leading/trailing empty segments are kept, a zero-width match does not produce a trailing empty segment, and empty input yields [””].

Parameters:

Returns:



140
141
142
143
144
# File 'lib/ruby/rego/builtins/regex.rb', line 140

def self.split(pattern_value, string_value)
  regexp = compile(pattern_value, "regex.split")
  string = string_arg(string_value, "regex.split")
  guarded("regex.split") { string_array(split_segments(regexp, string)) }
end

.template_match(template_value, string_value, delim_start_value, delim_end_value) ⇒ Ruby::Rego::BooleanValue

regex.template_match(template, string, delim_start, delim_end) — the template is literal text with delimited regex sections ({...} by default). OPA requires each delimiter to be a single character; anything else (empty or multi-character) or an unbalanced section yields undefined.

:reek:LongParameterList :reek:TooManyStatements



16
17
18
19
20
21
22
23
24
25
# File 'lib/ruby/rego/builtins/regex/template.rb', line 16

def self.template_match(template_value, string_value, delim_start_value, delim_end_value)
  context = "regex.template_match"
  template = string_arg(template_value, context)
  string = string_arg(string_value, context)
  delim_start = single_delimiter(delim_start_value, context)
  delim_end = single_delimiter(delim_end_value, context)
  assert_source_length(template, context)
  regexp = compile_pattern(template_source(template, delim_start, delim_end, context), context).first
  guarded(context) { BooleanValue.new(regexp.match?(string)) }
end