Module: Ruby::Rego::Builtins::Codecs::JsonDecoder

Defined in:
lib/ruby/rego/builtins/codecs/json_decoder.rb

Overview

A strict JSON decoder (RFC 8259 / Go encoding/json, which OPA uses) that PRESERVES number text: a JSON number becomes a Ruby::Rego::Number (non-integer) or an exact Integer, matching OPA’s json.Number model, instead of collapsing to a Float (1.50 -> 1.5, 1e999 -> Infinity, large integers losing precision). Shared by json.unmarshal / json.is_valid / io.jwt.decode and the CLI input/data loader so all “parse untrusted JSON to Rego values” paths agree.

Strict like Go’s encoding/json (verified against opa eval 1.17): rejects comments, trailing commas, leading zeros, a bare .5 / 1., NaN/Infinity, and trailing content; duplicate object keys take the last value. Comment / trailing-comma rejection also closes the one dangerous gem-wide leniency Ruby’s JSON.parse had (it accepted // and /* */ comments OPA rejects).

TOTALITY (the contract — the registry rescues only BuiltinArgumentError, callers rescue ParseError): MAX_DEPTH fires on the way DOWN, before this parser’s own recursion — and the subsequent recursive Value.from_ruby — can overflow the C stack (a SystemStackError there is uncatchable). A number beyond the magnitude cap (Number::MAX_MAGNITUDE_EXPONENT, the same bound the lexer applies to literals) raises rather than materialise an astronomically large rational; see parse_number for why this is a DoS-vs-fail-open tradeoff, not a safe gem-stricter divergence. The input is byte-encoding-guarded up front, and string content is scanned by bytes, so binary / invalid-UTF-8 input maps to undefined instead of raising an uncaught error. rubocop:disable Metrics/ModuleLength

Defined Under Namespace

Classes: ParseError

Constant Summary collapse

MAX_DEPTH =

Bounds both this parser’s recursion and the downstream Value.from_ruby recursion. Matches the max_nesting Ruby’s JSON.parse used (load-bearing — Value.from_ruby SystemStackErrors far below the C-stack limit). Go allows ~10000; staying at 100 is a documented gem-more-strict divergence.

100

Class Method Summary collapse

Class Method Details

.parse(string) ⇒ Object

Returns a Ruby value (Number/Integer for JSON numbers; String/true/false/nil/Array/Hash).

Parameters:

  • string (String)

Returns:

  • (Object)

    a Ruby value (Number/Integer for JSON numbers; String/true/false/nil/Array/Hash)

Raises:



53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# File 'lib/ruby/rego/builtins/codecs/json_decoder.rb', line 53

def self.parse(string)
  raise ParseError, "invalid string encoding" unless Base.byte_safe_encoding?(string)

  # Normalize an input in an exotic ascii-compatible encoding (US-ASCII, or a single-byte
  # non-UTF-8 encoding like ISO-8859-1 / Windows-1252 that byte_safe_encoding? admits) to raw
  # bytes, so a string body with a literal high byte plus a multibyte \uXXXX escape goes through
  # the BINARY append path (concat_escape) rather than clashing two incompatible encodings — an
  # uncaught Encoding::CompatibilityError that would break totality (normalize_string_encoding
  # re-tags valid bytes back to UTF-8). UTF-8 and BINARY inputs already take the right path and
  # are used as-is — no copy of the (attacker-controlled, e.g. base64-decoded JWT) bytes.
  string = string.b unless [Encoding::UTF_8, Encoding::BINARY].include?(string.encoding)
  scanner = StringScanner.new(string)
  scanner.skip(WHITESPACE)
  value = parse_value(scanner, 0)
  scanner.skip(WHITESPACE)
  raise ParseError, "trailing content" unless scanner.eos?

  value
end

.valid?(string) ⇒ bool

Unlike json.unmarshal, json.is_valid does not flow through Codecs.decoded (which rescues EncodingError), so it rescues EncodingError here too: json.is_valid must return false on any un-parseable input, never let an encoding error escape and abort the policy. parse normalizes non-UTF-8 input to bytes so this is defense-in-depth, but it keeps the totality guarantee local.

Returns:

  • (bool)

    whether string is well-formed strict JSON (json.is_valid)



79
80
81
82
83
84
# File 'lib/ruby/rego/builtins/codecs/json_decoder.rb', line 79

def self.valid?(string)
  parse(string)
  true
rescue ParseError, EncodingError
  false
end