Class: Ibex::TableArtifact::Validator
- Inherits:
-
Object
- Object
- Ibex::TableArtifact::Validator
- Includes:
- ValidationSupport
- Defined in:
- lib/ibex/table_artifact/validator.rb,
sig/ibex/table_artifact/validator.rbs
Overview
Closed structural and referential validation for untrusted sidecars.
Constant Summary collapse
- ROOT_KEYS =
%w[artifact_type schema_version identity payload cost].freeze
- PAYLOAD_KEYS =
%w[ table_format source state_count symbols tokens entry_states tables productions cst recovery semantic_actions ].freeze
Constants included from ValidationSupport
Ibex::TableArtifact::ValidationSupport::DIGEST
Class Method Summary collapse
Instance Method Summary collapse
- #expected_costs(payload) ⇒ [ String, String ]
-
#initialize(data) ⇒ Validator
constructor
A new instance of Validator.
- #occupied(table, values_key) ⇒ Integer
- #symbol_ids(symbols, kind) ⇒ Set[Integer]
-
#validate! ⇒ true
rubocop:disable Naming/PredicateMethod -- bang denotes fail-fast validation.
- #validate_cost(data, payload) ⇒ void
- #validate_entries(data, symbols, state_count) ⇒ void
- #validate_named_digest(identity, payload, identity_name, payload_name) ⇒ void
- #validate_productions(data, symbols) ⇒ Array[Hash[String, ValidationSupport::json_value]]
- #validate_root_identity ⇒ void
- #validate_source(data) ⇒ void
- #validate_symbols(data) ⇒ Array[Hash[String, ValidationSupport::json_value]]
- #validate_table_format(data) ⇒ String
- #validate_tokens(data, symbols) ⇒ void
Methods included from ValidationSupport
#array, #boolean, #digest, #enum, #integer, #invalid, #nullable_integer, #nullable_string, #record, #sorted_unique!, #string
Constructor Details
#initialize(data) ⇒ Validator
Returns a new instance of Validator.
26 27 28 |
# File 'lib/ibex/table_artifact/validator.rb', line 26 def initialize(data) @data = data end |
Class Method Details
.validate!(data) ⇒ true
20 21 22 |
# File 'lib/ibex/table_artifact/validator.rb', line 20 def validate!(data) new(data).validate! end |
Instance Method Details
#expected_costs(payload) ⇒ [ String, String ]
208 209 210 211 212 213 214 215 216 217 218 |
# File 'lib/ibex/table_artifact/validator.rb', line 208 def expected_costs(payload) table_format = payload.fetch("table_format") #: Hash[String, ValidationSupport::json_value] compact = table_format.fetch("representation") == "compact" lookup = compact ? "O(1) row-displacement probe" : "O(log row width) binary search" recognition = if compact "O(input tokens + reductions)" else "O((input tokens + reductions) * log maximum row width)" end [lookup, "#{recognition}; lexer, recovery search, and semantic actions excluded"] end |
#occupied(table, values_key) ⇒ Integer
221 222 223 224 225 226 227 228 229 |
# File 'lib/ibex/table_artifact/validator.rb', line 221 def occupied(table, values_key) if table.key?("rows") rows = table.fetch("rows") #: Array[Array[ValidationSupport::json_value]] return rows.sum(&:length) end values = table.fetch(values_key) #: Array[ValidationSupport::json_value] values.compact.length end |
#symbol_ids(symbols, kind) ⇒ Set[Integer]
232 233 234 235 236 237 238 |
# File 'lib/ibex/table_artifact/validator.rb', line 232 def symbol_ids(symbols, kind) symbols.filter_map do |symbol| next unless symbol.fetch("kind") == kind symbol.fetch("id") #: Integer end.to_set end |
#validate! ⇒ true
rubocop:disable Naming/PredicateMethod -- bang denotes fail-fast validation.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 |
# File 'lib/ibex/table_artifact/validator.rb', line 32 def validate! record(@data, "$", ROOT_KEYS) payload = record(@data.fetch("payload"), "$.payload", PAYLOAD_KEYS) validate_root_identity representation = validate_table_format(payload.fetch("table_format")) validate_source(payload.fetch("source")) state_count = integer(payload.fetch("state_count"), "$.payload.state_count", minimum: 1) symbols = validate_symbols(payload.fetch("symbols")) validate_tokens(payload.fetch("tokens"), symbols) validate_entries(payload.fetch("entry_states"), symbols, state_count) productions = validate_productions(payload.fetch("productions"), symbols) tables = payload.fetch("tables") #: Hash[String, ValidationSupport::json_value] TableValidator.new( tables, state_count: state_count, production_count: productions.length, terminal_ids: symbol_ids(symbols, "terminal"), nonterminal_ids: symbol_ids(symbols, "nonterminal"), representation: representation ).validate! MetadataValidator.new(payload, symbols: symbols, productions: productions).validate! validate_cost(@data.fetch("cost"), payload) true end |
#validate_cost(data, payload) ⇒ void
This method returns an undefined value.
179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 |
# File 'lib/ibex/table_artifact/validator.rb', line 179 def validate_cost(data, payload) path = "$.cost" keys = %w[ canonical_payload_bytes action_cells goto_cells lookup_cost recognition_cost measurement bounded_by_max_steps ] data = record(data, path, keys) bytes = integer(data.fetch("canonical_payload_bytes"), "#{path}.canonical_payload_bytes", minimum: 1) unless bytes == Serializer.compact(payload).bytesize invalid("#{path}.canonical_payload_bytes", "does not match payload") end action_cells = integer(data.fetch("action_cells"), "#{path}.action_cells", minimum: 0) goto_cells = integer(data.fetch("goto_cells"), "#{path}.goto_cells", minimum: 0) tables = payload.fetch("tables") #: Hash[String, ValidationSupport::json_value] actions = tables.fetch("actions") #: Hash[String, ValidationSupport::json_value] gotos = tables.fetch("gotos") #: Hash[String, ValidationSupport::json_value] invalid("#{path}.action_cells", "does not match actions") unless action_cells == occupied(actions, "codes") invalid("#{path}.goto_cells", "does not match gotos") unless goto_cells == occupied(gotos, "values") expected_lookup, expected_recognition = expected_costs(payload) invalid("#{path}.lookup_cost", "does not match table representation") unless data.fetch("lookup_cost") == expected_lookup invalid("#{path}.recognition_cost", "does not match table representation") unless data.fetch("recognition_cost") == expected_recognition enum(data.fetch("measurement"), "#{path}.measurement", ["not-measured"]) invalid("#{path}.bounded_by_max_steps", "must be true") unless boolean(data.fetch("bounded_by_max_steps"), "#{path}.bounded_by_max_steps") end |
#validate_entries(data, symbols, state_count) ⇒ void
This method returns an undefined value.
139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 |
# File 'lib/ibex/table_artifact/validator.rb', line 139 def validate_entries(data, symbols, state_count) entries = array(data, "$.payload.entry_states") #: Array[Hash[String, ValidationSupport::json_value]] invalid("$.payload.entry_states", "must not be empty") if entries.empty? names = entries.map.with_index do |entry, index| path = "$.payload.entry_states[#{index}]" record(entry, path, %w[name state]) name = string(entry.fetch("name"), "#{path}.name") symbol = symbols.find { |candidate| candidate.fetch("name") == name } invalid("#{path}.name", "must reference a nonterminal") unless symbol&.fetch("kind") == "nonterminal" state = integer(entry.fetch("state"), "#{path}.state", minimum: 0) invalid("#{path}.state", "references a missing state") unless state < state_count name end invalid("$.payload.entry_states", "names must be unique") unless names.uniq.length == names.length end |
#validate_named_digest(identity, payload, identity_name, payload_name) ⇒ void
This method returns an undefined value.
243 244 245 246 247 248 249 250 |
# File 'lib/ibex/table_artifact/validator.rb', line 243 def validate_named_digest(identity, payload, identity_name, payload_name) expected = Serializer.digest(payload.fetch(payload_name)) path = "$.identity.#{identity_name}_digest" return if identity.fetch("#{identity_name}_digest") == expected invalid(path, "does not match payload #{payload_name}") end |
#validate_productions(data, symbols) ⇒ Array[Hash[String, ValidationSupport::json_value]]
157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
# File 'lib/ibex/table_artifact/validator.rb', line 157 def validate_productions(data, symbols) productions = array(data, "$.payload.productions") #: Array[Hash[String, ValidationSupport::json_value]] nonterminals = symbol_ids(symbols, "nonterminal") productions.each_with_index do |production, index| path = "$.payload.productions[#{index}]" record(production, path, %w[id lhs rhs rhs_length action_slot]) invalid("#{path}.id", "must be contiguous and ordered") unless production.fetch("id") == index lhs = integer(production.fetch("lhs"), "#{path}.lhs", minimum: 0) invalid("#{path}.lhs", "must reference a nonterminal") unless nonterminals.include?(lhs) rhs = array(production.fetch("rhs"), "#{path}.rhs") rhs.each_with_index do |symbol_id, rhs_index| symbol_id = integer(symbol_id, "#{path}.rhs[#{rhs_index}]", minimum: 0) invalid("#{path}.rhs[#{rhs_index}]", "references a missing symbol") unless symbol_id < symbols.length end invalid("#{path}.rhs_length", "must match rhs") unless production.fetch("rhs_length") == rhs.length slot = nullable_integer(production.fetch("action_slot"), "#{path}.action_slot", minimum: 0) invalid("#{path}.action_slot", "must equal production id") if slot && slot != index end productions end |
#validate_root_identity ⇒ void
This method returns an undefined value.
61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
# File 'lib/ibex/table_artifact/validator.rb', line 61 def validate_root_identity invalid("$.artifact_type", "is unsupported") unless @data.fetch("artifact_type") == ARTIFACT_TYPE invalid("$.schema_version", "is unsupported") unless @data.fetch("schema_version") == SCHEMA_VERSION identity_keys = %w[ grammar_digest automaton_digest payload_digest cst_metadata_digest recovery_metadata_digest ] identity = record(@data.fetch("identity"), "$.identity", identity_keys) identity.each { |key, value| digest(value, "$.identity.#{key}") } expected = Serializer.digest(@data.fetch("payload")) invalid("$.identity.payload_digest", "does not match the canonical payload") unless identity.fetch("payload_digest") == expected payload = @data.fetch("payload") validate_named_digest(identity, payload, "cst_metadata", "cst") validate_named_digest(identity, payload, "recovery_metadata", "recovery") end |
#validate_source(data) ⇒ void
This method returns an undefined value.
96 97 98 99 100 101 102 103 |
# File 'lib/ibex/table_artifact/validator.rb', line 96 def validate_source(data) source = record(data, "$.payload.source", %w[grammar_digest automaton_digest]) source.each { |key, value| digest(value, "$.payload.source.#{key}") } identity = @data.fetch("identity") invalid("$.payload.source", "must match root identity") unless source.fetch("grammar_digest") == identity.fetch("grammar_digest") && source.fetch("automaton_digest") == identity.fetch("automaton_digest") end |
#validate_symbols(data) ⇒ Array[Hash[String, ValidationSupport::json_value]]
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 |
# File 'lib/ibex/table_artifact/validator.rb', line 106 def validate_symbols(data) symbols = array(data, "$.payload.symbols") #: Array[Hash[String, ValidationSupport::json_value]] invalid("$.payload.symbols", "must not be empty") if symbols.empty? names = [] #: Array[String] symbols.each_with_index do |symbol, index| path = "$.payload.symbols[#{index}]" record(symbol, path, %w[id name kind display_name]) invalid("#{path}.id", "must be contiguous and ordered") unless symbol.fetch("id") == index names << string(symbol.fetch("name"), "#{path}.name") enum(symbol.fetch("kind"), "#{path}.kind", %w[terminal nonterminal]) nullable_string(symbol.fetch("display_name"), "#{path}.display_name") end invalid("$.payload.symbols", "names must be unique") unless names.uniq.length == names.length symbols end |
#validate_table_format(data) ⇒ String
78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
# File 'lib/ibex/table_artifact/validator.rb', line 78 def validate_table_format(data) path = "$.payload.table_format" keys = %w[family version representation action_encoding goto_encoding] data = record(data, path, keys) enum(data.fetch("family"), "#{path}.family", ["ibex-runtime-parser-table"]) invalid("#{path}.version", "must be 6") unless integer(data.fetch("version"), "#{path}.version") == 6 representation = enum(data.fetch("representation"), "#{path}.representation", %w[plain compact]) #: String expected = if representation == "compact" %w[signed-row-displacement-v1 row-displacement-v1] else %w[signed-sparse-rows-v1 sparse-rows-v1] end actual = [data.fetch("action_encoding"), data.fetch("goto_encoding")] invalid(path, "encodings do not match representation") unless actual == expected representation end |
#validate_tokens(data, symbols) ⇒ void
This method returns an undefined value.
123 124 125 126 127 128 129 130 131 132 133 134 135 |
# File 'lib/ibex/table_artifact/validator.rb', line 123 def validate_tokens(data, symbols) tokens = array(data, "$.payload.tokens") #: Array[Hash[String, ValidationSupport::json_value]] expected = symbols.select { |symbol| symbol.fetch("kind") == "terminal" }.map do |symbol| symbol.slice("id", "name", "display_name") end tokens.each_with_index do |token, index| record(token, "$.payload.tokens[#{index}]", %w[id name display_name]) end invalid("$.payload.tokens", "must exactly index terminal symbols") unless tokens == expected token_id = tokens.first&.fetch("id") invalid("$.payload.tokens", "token id 0 must be $eof") unless token_id.is_a?(Integer) && token_id.zero? && tokens.first.fetch("name") == "$eof" end |