Module: Ruby::Rego::Builtins::Units

Extended by:
RegistryHelpers
Defined in:
lib/ruby/rego/builtins/units.rb

Overview

Resource-quantity parsers (units.parse, units.parse_bytes), matching OPA. units.parse returns an integer or a value rounded to 10 decimals; units.parse_bytes lowercases its input, multiplies, and truncates toward zero to an integer. A non-string, an empty amount, an embedded space, an unrecognised unit, an unparseable amount, or a scientific exponent of more than MAX_EXPONENT_DIGITS digits yields undefined.

Both are byte-for-byte faithful to OPA, but via DIFFERENT arithmetic, matching OPA’s two code paths: units.parse uses exact rational arithmetic (OPA’s big.Rat), while units.parse_bytes computes in a 64-bit big.Float (OPA’s big.Float: SetString → Mul → truncate toward zero, via Number.prec64_multiply_truncate). The big.Float path rounds, so a fractional byte amount can differ from the exact-rational value (0.001mb → 999; 9999999999.99999999995 → 10000000000), as OPA does. Both share the same amount grammar (AMOUNT_RE) and ASCII/length DoS guards.

Constant Summary collapse

MAX_EXPONENT_DIGITS =

OPA’s maxExponentDigits: a scientific exponent longer than this is undefined.

6
ROUND_DECIMALS =

OPA renders a non-integer units.parse result via big.Rat.FloatString(10): exactly 10 decimal places, rounded half-away-from-zero, trailing zeros kept (see float_string).

10
DECIMAL_SCALE =
10**ROUND_DECIMALS
MAX_SOURCE =

Upper bound on the operand length (OPA has none — it relies on Go’s runtime). Guards the pure-Ruby evaluator against a huge literal numeric string allocating a giant bignum; an over-long operand yields undefined. (The 6-digit exponent cap already bounds scientific-notation blow-up.)

1_000_000
NUMERIC =

Characters that continue the numeric prefix (an e/E exponent is handled apart).

"0123456789.+-"
DIGIT =
/[0-9]/
SIGN =
%w[+ -].freeze
AMOUNT_RE =

Go’s big.Rat.SetString decimal-float grammar (the subset reachable after number_boundary): an optional sign, a mantissa with at least one digit (so a bare “.” is rejected), and an optional exponent that requires at least one digit (so a dangling “1e+” is rejected). Ruby’s Rational is both more lenient (accepts “.” and “1e+”) and stricter (“5.e3”), so the amount is validated against this before being handed to Rational. Anchored to the whole amount.

/\A[+-]?(?:[0-9]+(?:\.[0-9]*)?|\.[0-9]+)(?:[eE][+-]?[0-9]+)?\z/
PARSE_UNITS =

units.parse multipliers. Note the m/M asymmetry: lowercase m is milli, uppercase M is mega; every other first letter is case-insensitive. Keys are the unit with its first character kept and the remainder lowercased, so kI/ki collide while Ki stays distinct from ki (hence both appear below). Milli uses the IEEE-754 double of 0.001 (as OPA’s big.Rat.SetFloat64 does), so e.g. 1000m yields 1.0, not 1.

{
  "" => 1, "m" => 0.001.to_r,
  "k" => 1_000, "K" => 1_000, "M" => 1_000_000,
  "g" => 10**9, "G" => 10**9, "t" => 10**12, "T" => 10**12,
  "p" => 10**15, "P" => 10**15, "e" => 10**18, "E" => 10**18,
  "ki" => 1024, "Ki" => 1024, "mi" => 1024**2, "Mi" => 1024**2,
  "gi" => 1024**3, "Gi" => 1024**3, "ti" => 1024**4, "Ti" => 1024**4,
  "pi" => 1024**5, "Pi" => 1024**5, "ei" => 1024**6, "Ei" => 1024**6
}.freeze
PARSE_BYTES_UNITS =

units.parse_bytes multipliers (the whole input is lowercased first, so keys are too). Both the b-suffixed and bare forms are accepted; a bare b is NOT a unit.

{
  "" => 1, "kb" => 1000, "k" => 1000, "kib" => 1024, "ki" => 1024,
  "mb" => 10**6, "m" => 10**6, "mib" => 1024**2, "mi" => 1024**2,
  "gb" => 10**9, "g" => 10**9, "gib" => 1024**3, "gi" => 1024**3,
  "tb" => 10**12, "t" => 10**12, "tib" => 1024**4, "ti" => 1024**4,
  "pb" => 10**15, "p" => 10**15, "pib" => 1024**5, "pi" => 1024**5,
  "eb" => 10**18, "e" => 10**18, "eib" => 1024**6, "ei" => 1024**6
}.freeze
UNITS_FUNCTIONS =
{
  "units.parse" => { arity: 1, handler: :parse },
  "units.parse_bytes" => { arity: 1, handler: :parse_bytes }
}.freeze

Class Method Summary collapse

Methods included from RegistryHelpers

register_configured_functions

Class Method Details

.parse(value) ⇒ Integer, Ruby::Rego::Number

Parses an SI/binary quantity (e.g. “10K”, “1.5Mi”, “10m”) to a number — exact rational arithmetic. An integer-valued result is an exact Integer; a non-integer result is a precision-preserving Number rendered to 10 decimals the way OPA’s big.Rat does.

Parameters:

Returns:



100
101
102
103
104
105
106
107
108
# File 'lib/ruby/rego/builtins/units.rb', line 100

def self.parse(value)
  amount, unit = split(string_arg(value, "units.parse"), "units.parse")
  multiplier = PARSE_UNITS[normalize_unit(unit)] || raise_unrecognized(unit, "units.parse")
  result = parse_amount(amount, "units.parse") * multiplier # @type var result: Rational
  # Number.literal (not from_numeric/from_binnum, which route through GoNumberFormat's shortest
  # form and would strip the trailing zeros) preserves the fixed 10-decimal text verbatim; its
  # exact value is re-derived lazily from that text.
  result.denominator == 1 ? result.numerator : Number.literal(float_string(result))
end

.parse_bytes(value) ⇒ Integer

Parses a byte quantity (e.g. “10KB”, “1.5GiB”) to an integer, truncating toward zero.

Parameters:

Returns:

  • (Integer)


139
140
141
142
143
144
145
146
147
148
# File 'lib/ruby/rego/builtins/units.rb', line 139

def self.parse_bytes(value)
  amount, unit = split(string_arg(value, "units.parse_bytes").downcase, "units.parse_bytes")
  multiplier = PARSE_BYTES_UNITS[unit] || raise_unrecognized(unit, "units.parse_bytes")
  # OPA computes the byte count in a 64-bit big.Float, not exact rational, so a fractional
  # amount can round (`0.001mb` -> 999, not 1000) — see Number.prec64_multiply_truncate. The
  # amount is bounded by MAX_SOURCE and MAX_EXPONENT_DIGITS to a magnitude far below the engine's
  # emax (2**30), so the product is always finite and `to_i` never raises on an infinity — which
  # would otherwise escape as a non-BuiltinArgumentError and abort the whole policy (a DoS).
  Number.prec64_multiply_truncate(parse_amount(amount, "units.parse_bytes"), multiplier)
end

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



86
87
88
89
90
# File 'lib/ruby/rego/builtins/units.rb', line 86

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