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
-
.parse(string) ⇒ Object
A Ruby value (Number/Integer for JSON numbers; String/true/false/nil/Array/Hash).
-
.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.
Class Method Details
.parse(string) ⇒ Object
Returns a Ruby value (Number/Integer for JSON numbers; String/true/false/nil/Array/Hash).
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.
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 |