schemurai 3.1.0-aarch64-linux-gnu
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +395 -0
- data/lib/schemurai/backend.rb +34 -0
- data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
- data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
- data/lib/schemurai/container_checks.rb +534 -0
- data/lib/schemurai/decimal.rb +11 -0
- data/lib/schemurai/dialect.rb +106 -0
- data/lib/schemurai/dialect_keywords.rb +91 -0
- data/lib/schemurai/dialects/draft2019_09.rb +27 -0
- data/lib/schemurai/dialects/draft2020_12.rb +27 -0
- data/lib/schemurai/dialects/draft7.rb +27 -0
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/error_message.rb +102 -0
- data/lib/schemurai/evaluation.rb +63 -0
- data/lib/schemurai/evaluation_limits.rb +65 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +861 -0
- data/lib/schemurai/formats.rb +403 -0
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/limit_budget.rb +78 -0
- data/lib/schemurai/limit_exceeded.rb +18 -0
- data/lib/schemurai/limited_evaluator.rb +43 -0
- data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
- data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
- data/lib/schemurai/meta_schemas/draft7.rb +121 -0
- data/lib/schemurai/meta_schemas.rb +28 -0
- data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
- data/lib/schemurai/native/diagnostics.rb +53 -0
- data/lib/schemurai/native/evaluator.rb +87 -0
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/schema_domain.rb +95 -0
- data/lib/schemurai/schema_graph.rb +538 -0
- data/lib/schemurai/schema_node.rb +55 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/unique_items.rb +68 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +6 -0
- data/lib/schemurai/vm/compiler.rb +410 -0
- data/lib/schemurai/vm/evaluator.rb +663 -0
- data/lib/schemurai/vm/limited_evaluator.rb +27 -0
- data/lib/schemurai.rb +307 -0
- data/lib/schemurai_native.rb +4 -0
- data/sig/schemurai.rbs +108 -0
- metadata +121 -0
|
@@ -0,0 +1,663 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../decimal"
|
|
4
|
+
require "base64"
|
|
5
|
+
require "json"
|
|
6
|
+
require_relative "../evaluation_pool"
|
|
7
|
+
require_relative "../evaluation"
|
|
8
|
+
require_relative "../error_message"
|
|
9
|
+
require_relative "../unique_items"
|
|
10
|
+
require_relative "../value_equality"
|
|
11
|
+
require_relative "../container_checks"
|
|
12
|
+
require_relative "../ecma_regexp"
|
|
13
|
+
require_relative "../json_pointer"
|
|
14
|
+
require_relative "compiler"
|
|
15
|
+
require_relative "../string_checks"
|
|
16
|
+
|
|
17
|
+
module Schemurai
|
|
18
|
+
module VM
|
|
19
|
+
EvaluationBuffer = Internal::EvaluationBuffer
|
|
20
|
+
private_constant :EvaluationBuffer
|
|
21
|
+
|
|
22
|
+
class Evaluator
|
|
23
|
+
include Internal::UniqueItems
|
|
24
|
+
include Internal::ValueEquality
|
|
25
|
+
include Internal::ContainerChecks::Bounds
|
|
26
|
+
include Internal::ContainerChecks::ObjectChildren
|
|
27
|
+
include Internal::ContainerChecks::ArrayChildren
|
|
28
|
+
include Internal::ContainerChecks::ArrayDiagnostics
|
|
29
|
+
include Internal::ContainerChecks::ObjectDiagnostics
|
|
30
|
+
include Internal::NumberChecks::Evaluation
|
|
31
|
+
include Internal::StringChecks::Evaluation
|
|
32
|
+
include Internal::StringChecks::Regexps
|
|
33
|
+
|
|
34
|
+
MISSING_SEGMENT = Internal::ContainerChecks::ObjectDiagnostics::MISSING_SEGMENT
|
|
35
|
+
|
|
36
|
+
def backend = :vm
|
|
37
|
+
|
|
38
|
+
def initialize(graph, compiler, root, content: false, format: false, number_state: nil, regexps: nil, dynamic_scope: nil, active_state: nil, native_references: nil, native_reports: nil)
|
|
39
|
+
@graph = graph
|
|
40
|
+
@compiler = compiler
|
|
41
|
+
@root = root
|
|
42
|
+
@validate_content = content
|
|
43
|
+
@validate_format = format
|
|
44
|
+
@regexps = regexps
|
|
45
|
+
@active = active_state
|
|
46
|
+
@native_reports = native_reports
|
|
47
|
+
@native_references = native_references
|
|
48
|
+
@resolved_references = nil
|
|
49
|
+
@number_state = number_state
|
|
50
|
+
@dynamic_scope = dynamic_scope
|
|
51
|
+
@instance_path_buffer = nil
|
|
52
|
+
@schema_path_buffer = nil
|
|
53
|
+
@evaluation_pool = native_reports ? native_reports[1] : Internal::EvaluationPool.new
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def validate(instance)
|
|
57
|
+
@errors = []
|
|
58
|
+
prepare_evaluation(paths: true)
|
|
59
|
+
@native_reports[0] = self if @native_reports
|
|
60
|
+
evaluate(@root, instance)
|
|
61
|
+
Result.new(@errors)
|
|
62
|
+
ensure
|
|
63
|
+
@errors = nil
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def valid?(instance)
|
|
67
|
+
@errors = nil
|
|
68
|
+
@error_count = 0
|
|
69
|
+
@dynamic_scope&.clear
|
|
70
|
+
@instance_path = nil
|
|
71
|
+
@schema_path = nil
|
|
72
|
+
reset_evaluation_pool
|
|
73
|
+
@native_reports[0] = self if @native_reports
|
|
74
|
+
evaluate_valid(@root, instance)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Data exchange for retained native blocks during a compatibility traversal.
|
|
78
|
+
# These methods expose collection state, never programs or instructions.
|
|
79
|
+
private def native_report_context
|
|
80
|
+
[@errors, @error_count || 0, @instance_path, @schema_path]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
private def native_clear_collection(paths = false)
|
|
84
|
+
@errors = nil
|
|
85
|
+
@error_count = 0
|
|
86
|
+
if paths
|
|
87
|
+
(@instance_path_buffer ||= []).clear
|
|
88
|
+
(@schema_path_buffer ||= []).clear
|
|
89
|
+
@instance_path = @instance_path_buffer
|
|
90
|
+
@schema_path = @schema_path_buffer
|
|
91
|
+
else
|
|
92
|
+
@instance_path = nil
|
|
93
|
+
@schema_path = nil
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private def native_append_report(errors, count, reset)
|
|
98
|
+
native_clear_collection(reset == 2) unless reset.zero?
|
|
99
|
+
@error_count += count
|
|
100
|
+
@errors.concat(errors) if @errors
|
|
101
|
+
@native_reports[0] = self
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
private def prepare_evaluation(paths:)
|
|
105
|
+
@error_count = 0
|
|
106
|
+
@dynamic_scope&.clear
|
|
107
|
+
reset_evaluation_pool
|
|
108
|
+
if paths
|
|
109
|
+
(@instance_path_buffer ||= []).clear
|
|
110
|
+
(@schema_path_buffer ||= []).clear
|
|
111
|
+
@instance_path = @instance_path_buffer
|
|
112
|
+
@schema_path = @schema_path_buffer
|
|
113
|
+
else
|
|
114
|
+
@instance_path = nil
|
|
115
|
+
@schema_path = nil
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private def evaluate_valid(program, instance)
|
|
120
|
+
code = program.code
|
|
121
|
+
flags = code[0]
|
|
122
|
+
entered_scope = false
|
|
123
|
+
unless flags.zero?
|
|
124
|
+
return evaluate(program, instance).valid? if (flags & TRACKS_EVALUATION) != 0
|
|
125
|
+
|
|
126
|
+
if (flags & TRACKS_DYNAMIC_SCOPE) != 0
|
|
127
|
+
resource = program.node.resource
|
|
128
|
+
unless @dynamic_scope&.last.equal?(resource)
|
|
129
|
+
(@dynamic_scope ||= []) << resource
|
|
130
|
+
entered_scope = true
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
instruction_index = 1
|
|
135
|
+
while (opcode = code[instruction_index])
|
|
136
|
+
@budget&.checkpoint
|
|
137
|
+
operand = code[instruction_index + 1]
|
|
138
|
+
case opcode
|
|
139
|
+
when :boolean
|
|
140
|
+
return operand
|
|
141
|
+
when :ref
|
|
142
|
+
target = reference_target(program, operand)
|
|
143
|
+
return false unless valid_reference?(program, target, instance)
|
|
144
|
+
when :recursive_ref
|
|
145
|
+
target = recursive_target(program, operand)
|
|
146
|
+
return false unless valid_reference?(program, target, instance)
|
|
147
|
+
when :dynamic_ref
|
|
148
|
+
target = dynamic_target(program, operand)
|
|
149
|
+
return false unless valid_reference?(program, target, instance)
|
|
150
|
+
when :type_null
|
|
151
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_NULL)
|
|
152
|
+
when :type_boolean
|
|
153
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_BOOLEAN)
|
|
154
|
+
when :type_object
|
|
155
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_OBJECT)
|
|
156
|
+
when :type_array
|
|
157
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_ARRAY)
|
|
158
|
+
when :type_number
|
|
159
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_NUMBER)
|
|
160
|
+
when :type_integer
|
|
161
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_INTEGER)
|
|
162
|
+
when :type_string
|
|
163
|
+
return false unless Internal::TypeChecks.single?(instance, TYPE_STRING)
|
|
164
|
+
when :typed_number
|
|
165
|
+
return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && valid_number?(operand, instance)
|
|
166
|
+
when :typed_integer
|
|
167
|
+
return false unless instance.is_a?(Numeric) && !instance.is_a?(Complex) && instance.finite? &&
|
|
168
|
+
instance.to_i == instance && valid_number?(operand, instance)
|
|
169
|
+
when :typed_string
|
|
170
|
+
return false unless instance.is_a?(String) && valid_string?(operand, instance)
|
|
171
|
+
when :typed_array
|
|
172
|
+
return false unless instance.is_a?(Array) && valid_array?(operand, instance)
|
|
173
|
+
when :typed_object
|
|
174
|
+
return false unless instance.is_a?(Hash) && valid_object?(operand, instance)
|
|
175
|
+
when :types
|
|
176
|
+
return false if (operand.mask & instance_type_mask(instance, integer: (operand.mask & TYPE_INTEGER) != 0)).zero?
|
|
177
|
+
when :enum
|
|
178
|
+
return false unless operand.any? { |candidate| json_equal?(candidate, instance) }
|
|
179
|
+
when :const
|
|
180
|
+
return false unless json_equal?(operand, instance)
|
|
181
|
+
when :allOf
|
|
182
|
+
return false unless operand.all? { |child| evaluate_valid(child, instance) }
|
|
183
|
+
when :anyOf
|
|
184
|
+
return false unless operand.any? { |child| evaluate_valid(child, instance) }
|
|
185
|
+
when :oneOf
|
|
186
|
+
matches = 0
|
|
187
|
+
operand.each do |child|
|
|
188
|
+
@budget&.checkpoint
|
|
189
|
+
matches += 1 if evaluate_valid(child, instance)
|
|
190
|
+
return false if matches > 1
|
|
191
|
+
end
|
|
192
|
+
return false unless matches == 1
|
|
193
|
+
when :not
|
|
194
|
+
return false if evaluate_valid(operand, instance)
|
|
195
|
+
when :conditional
|
|
196
|
+
if evaluate_valid(operand.condition, instance)
|
|
197
|
+
return false if operand.then_branch && !evaluate_valid(operand.then_branch, instance)
|
|
198
|
+
elsif operand.else_branch && !evaluate_valid(operand.else_branch, instance)
|
|
199
|
+
return false
|
|
200
|
+
end
|
|
201
|
+
when :number
|
|
202
|
+
return false if instance.is_a?(Numeric) && !instance.is_a?(Complex) && !valid_number?(operand, instance)
|
|
203
|
+
when :string
|
|
204
|
+
return false if instance.is_a?(String) && !valid_string?(operand, instance)
|
|
205
|
+
when :array
|
|
206
|
+
return false if instance.is_a?(Array) && !valid_array?(operand, instance)
|
|
207
|
+
when :object
|
|
208
|
+
return false if instance.is_a?(Hash) && !valid_object?(operand, instance)
|
|
209
|
+
else
|
|
210
|
+
raise "unknown VM instruction #{opcode.inspect}"
|
|
211
|
+
end
|
|
212
|
+
instruction_index += 2
|
|
213
|
+
end
|
|
214
|
+
true
|
|
215
|
+
rescue ResolutionError
|
|
216
|
+
false
|
|
217
|
+
ensure
|
|
218
|
+
leave_scope if entered_scope
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
private def reset_evaluation_pool
|
|
222
|
+
@evaluation_pool.reset
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
private def tracked_evaluation
|
|
226
|
+
@evaluation_pool.acquire
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private def evaluate(program, instance)
|
|
230
|
+
before = @error_count
|
|
231
|
+
evaluation = Evaluation.valid
|
|
232
|
+
code = program.code
|
|
233
|
+
flags = code[0]
|
|
234
|
+
entered_scope = false
|
|
235
|
+
if (flags & TRACKS_DYNAMIC_SCOPE) != 0
|
|
236
|
+
resource = program.node.resource
|
|
237
|
+
unless @dynamic_scope&.last.equal?(resource)
|
|
238
|
+
(@dynamic_scope ||= []) << resource
|
|
239
|
+
entered_scope = true
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
instruction_index = 1
|
|
244
|
+
while (opcode = code[instruction_index])
|
|
245
|
+
@budget&.checkpoint
|
|
246
|
+
operand = code[instruction_index + 1]
|
|
247
|
+
case opcode
|
|
248
|
+
when :boolean
|
|
249
|
+
if operand == false
|
|
250
|
+
add_error("falseSchema", append_keyword: false) { Internal::ErrorMessage.false_schema }
|
|
251
|
+
evaluation = Evaluation.invalid
|
|
252
|
+
end
|
|
253
|
+
when :ref
|
|
254
|
+
evaluation = evaluation.merge(evaluate_ref(program, operand, instance))
|
|
255
|
+
when :recursive_ref
|
|
256
|
+
evaluation = evaluation.merge(evaluate_recursive_ref(program, operand, instance))
|
|
257
|
+
when :dynamic_ref
|
|
258
|
+
evaluation = evaluation.merge(evaluate_dynamic_ref(program, operand, instance))
|
|
259
|
+
when :type_null
|
|
260
|
+
check_compiled_type("null", Internal::TypeChecks.single?(instance, TYPE_NULL), instance)
|
|
261
|
+
when :type_boolean
|
|
262
|
+
check_compiled_type("boolean", Internal::TypeChecks.single?(instance, TYPE_BOOLEAN), instance)
|
|
263
|
+
when :type_object
|
|
264
|
+
check_compiled_type("object", Internal::TypeChecks.single?(instance, TYPE_OBJECT), instance)
|
|
265
|
+
when :type_array
|
|
266
|
+
check_compiled_type("array", Internal::TypeChecks.single?(instance, TYPE_ARRAY), instance)
|
|
267
|
+
when :type_number
|
|
268
|
+
check_compiled_type("number", Internal::TypeChecks.single?(instance, TYPE_NUMBER), instance)
|
|
269
|
+
when :type_integer
|
|
270
|
+
check_compiled_type("integer", Internal::TypeChecks.single?(instance, TYPE_INTEGER), instance)
|
|
271
|
+
when :type_string
|
|
272
|
+
check_compiled_type("string", Internal::TypeChecks.single?(instance, TYPE_STRING), instance)
|
|
273
|
+
when :typed_number
|
|
274
|
+
if instance.is_a?(Numeric) && !instance.is_a?(Complex)
|
|
275
|
+
check_number(operand, instance)
|
|
276
|
+
else
|
|
277
|
+
check_compiled_type("number", false, instance)
|
|
278
|
+
end
|
|
279
|
+
when :typed_integer
|
|
280
|
+
if instance.is_a?(Numeric) && !instance.is_a?(Complex)
|
|
281
|
+
integer = instance.finite? && instance.to_i == instance
|
|
282
|
+
check_compiled_type("integer", integer, instance)
|
|
283
|
+
check_number(operand, instance)
|
|
284
|
+
else
|
|
285
|
+
check_compiled_type("integer", false, instance)
|
|
286
|
+
end
|
|
287
|
+
when :typed_string
|
|
288
|
+
if instance.is_a?(String)
|
|
289
|
+
check_string(operand, instance)
|
|
290
|
+
else
|
|
291
|
+
check_compiled_type("string", false, instance)
|
|
292
|
+
end
|
|
293
|
+
when :typed_array
|
|
294
|
+
if instance.is_a?(Array)
|
|
295
|
+
result = check_array(operand, instance, evaluation)
|
|
296
|
+
return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
|
|
297
|
+
evaluation = evaluation.merge(result)
|
|
298
|
+
else
|
|
299
|
+
check_compiled_type("array", false, instance)
|
|
300
|
+
end
|
|
301
|
+
when :typed_object
|
|
302
|
+
if instance.is_a?(Hash)
|
|
303
|
+
result = check_object(operand, instance, evaluation)
|
|
304
|
+
return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
|
|
305
|
+
evaluation = evaluation.merge(result)
|
|
306
|
+
else
|
|
307
|
+
check_compiled_type("object", false, instance)
|
|
308
|
+
end
|
|
309
|
+
when :types
|
|
310
|
+
check_compiled_types(operand, instance)
|
|
311
|
+
when :enum
|
|
312
|
+
add_error("enum") { Internal::ErrorMessage.enum } unless operand.any? { |candidate| json_equal?(candidate, instance) }
|
|
313
|
+
when :const
|
|
314
|
+
add_error("const") { Internal::ErrorMessage.const } unless json_equal?(operand, instance)
|
|
315
|
+
when :allOf, :anyOf, :oneOf, :not, :conditional
|
|
316
|
+
result = check_combiner(opcode, operand, instance)
|
|
317
|
+
return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
|
|
318
|
+
evaluation = evaluation.merge(result)
|
|
319
|
+
when :number
|
|
320
|
+
check_number(operand, instance) if instance.is_a?(Numeric) && !instance.is_a?(Complex)
|
|
321
|
+
when :string
|
|
322
|
+
check_string(operand, instance) if instance.is_a?(String)
|
|
323
|
+
when :array
|
|
324
|
+
if instance.is_a?(Array)
|
|
325
|
+
result = check_array(operand, instance, evaluation)
|
|
326
|
+
return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
|
|
327
|
+
evaluation = evaluation.merge(result)
|
|
328
|
+
end
|
|
329
|
+
when :object
|
|
330
|
+
if instance.is_a?(Hash)
|
|
331
|
+
result = check_object(operand, instance, evaluation)
|
|
332
|
+
return Evaluation.invalid if !@errors && (!result.valid? || @error_count != before)
|
|
333
|
+
evaluation = evaluation.merge(result)
|
|
334
|
+
end
|
|
335
|
+
else
|
|
336
|
+
raise "unknown VM instruction #{opcode.inspect}"
|
|
337
|
+
end
|
|
338
|
+
instruction_index += 2
|
|
339
|
+
end
|
|
340
|
+
(@error_count == before) ? evaluation : Evaluation.invalid
|
|
341
|
+
ensure
|
|
342
|
+
leave_scope if entered_scope
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
private def leave_scope
|
|
346
|
+
@dynamic_scope.pop
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
private def recursive_target(program, rules)
|
|
350
|
+
target = reference_target(program, rules)
|
|
351
|
+
return target unless rules.fragment == "" && target.recursive_anchor?
|
|
352
|
+
|
|
353
|
+
@dynamic_scope&.each do |resource|
|
|
354
|
+
@budget&.checkpoint
|
|
355
|
+
compiled = @compiler.compile(resource.root)
|
|
356
|
+
return compiled if compiled.recursive_anchor?
|
|
357
|
+
end
|
|
358
|
+
target
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
private def dynamic_target(program, rules)
|
|
362
|
+
target = reference_target(program, rules)
|
|
363
|
+
fragment = rules.fragment
|
|
364
|
+
return target if fragment.nil? || fragment.empty? || fragment.start_with?("/")
|
|
365
|
+
|
|
366
|
+
return target unless target.dynamic_anchor == fragment
|
|
367
|
+
|
|
368
|
+
Array(@dynamic_scope).each do |resource|
|
|
369
|
+
@budget&.checkpoint
|
|
370
|
+
dynamic = @graph.dynamic_anchor(resource, fragment)
|
|
371
|
+
return @compiler.compile(dynamic) if dynamic
|
|
372
|
+
end
|
|
373
|
+
target
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
private def valid_reference?(source, target, instance)
|
|
377
|
+
instances = active_instances(source)
|
|
378
|
+
return true if instances.key?(instance) || @native_references&.reference_active?(source, instance)
|
|
379
|
+
|
|
380
|
+
instances[instance] = true
|
|
381
|
+
activated = true
|
|
382
|
+
evaluate_valid(target, instance)
|
|
383
|
+
ensure
|
|
384
|
+
instances&.delete(instance) if activated
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
private def reference_target(program, rules)
|
|
388
|
+
references = (@resolved_references ||= {}.compare_by_identity)
|
|
389
|
+
references.fetch(rules) { references[rules] = @compiler.resolve(program, rules.value) }
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
private def evaluate_ref(source, rules, instance)
|
|
393
|
+
target = reference_target(source, rules)
|
|
394
|
+
evaluate_reference(source, target, instance, "$ref")
|
|
395
|
+
rescue ResolutionError => error
|
|
396
|
+
unresolved_reference("$ref", error)
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
private def evaluate_recursive_ref(source, rules, instance)
|
|
400
|
+
evaluate_reference(source, recursive_target(source, rules), instance, "$recursiveRef")
|
|
401
|
+
rescue ResolutionError => error
|
|
402
|
+
unresolved_reference("$recursiveRef", error)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
private def evaluate_dynamic_ref(source, rules, instance)
|
|
406
|
+
evaluate_reference(source, dynamic_target(source, rules), instance, "$dynamicRef")
|
|
407
|
+
rescue ResolutionError => error
|
|
408
|
+
unresolved_reference("$dynamicRef", error)
|
|
409
|
+
end
|
|
410
|
+
|
|
411
|
+
private def evaluate_reference(source, target, instance, keyword)
|
|
412
|
+
instances = active_instances(source)
|
|
413
|
+
return Evaluation.valid if instances.key?(instance) || @native_references&.reference_active?(source, instance)
|
|
414
|
+
|
|
415
|
+
instances[instance] = true
|
|
416
|
+
activated = true
|
|
417
|
+
evaluate_at(target, instance, MISSING_SEGMENT, keyword)
|
|
418
|
+
ensure
|
|
419
|
+
instances&.delete(instance) if activated
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
private def unresolved_reference(keyword, error)
|
|
423
|
+
add_error(keyword, error.message, append_keyword: false)
|
|
424
|
+
Evaluation.invalid
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
private def active_instances(program)
|
|
428
|
+
active = (@active ||= {}.compare_by_identity)
|
|
429
|
+
active[program] ||= {}.compare_by_identity
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
private def check_compiled_type(name, valid, value)
|
|
433
|
+
add_error("type") { Internal::ErrorMessage.type(name, value) } unless valid
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
private def check_compiled_types(rules, value)
|
|
437
|
+
instance_mask = instance_type_mask(value, integer: (rules.mask & TYPE_INTEGER) != 0)
|
|
438
|
+
return unless (rules.mask & instance_mask).zero?
|
|
439
|
+
|
|
440
|
+
add_error("type") { Internal::ErrorMessage.type(rules.names, value) }
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
private def instance_type_mask(value, integer:)
|
|
444
|
+
Internal::TypeChecks.instance_mask(value, integer: integer)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
private def integer?(value)
|
|
448
|
+
Internal::TypeChecks.integer?(value)
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
private def check_number(rules, value)
|
|
452
|
+
mask = rules.mask
|
|
453
|
+
actual = (mask & MULTIPLE_OF).zero? ? value : number_state.decimal(value)
|
|
454
|
+
if (mask & MAXIMUM) != 0 && actual > rules.maximum
|
|
455
|
+
add_error("maximum") { Internal::ErrorMessage.numeric_limit("maximum", rules.maximum) }
|
|
456
|
+
end
|
|
457
|
+
if (mask & MINIMUM) != 0 && actual < rules.minimum
|
|
458
|
+
add_error("minimum") { Internal::ErrorMessage.numeric_limit("minimum", rules.minimum) }
|
|
459
|
+
end
|
|
460
|
+
if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
|
|
461
|
+
add_error("exclusiveMaximum") do
|
|
462
|
+
Internal::ErrorMessage.numeric_limit("exclusiveMaximum", rules.exclusive_maximum)
|
|
463
|
+
end
|
|
464
|
+
end
|
|
465
|
+
if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
|
|
466
|
+
add_error("exclusiveMinimum") do
|
|
467
|
+
Internal::ErrorMessage.numeric_limit("exclusiveMinimum", rules.exclusive_minimum)
|
|
468
|
+
end
|
|
469
|
+
end
|
|
470
|
+
return if (mask & MULTIPLE_OF).zero?
|
|
471
|
+
|
|
472
|
+
divisor = rules.multiple_of
|
|
473
|
+
valid = valid_multiple?(rules, value)
|
|
474
|
+
add_error("multipleOf") { Internal::ErrorMessage.multiple_of(divisor) } unless valid
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
private def check_string(rules, value)
|
|
478
|
+
length = value.length
|
|
479
|
+
if rules.max_length && length > rules.max_length
|
|
480
|
+
add_error("maxLength") { Internal::ErrorMessage.size("maxLength", rules.max_length, length) }
|
|
481
|
+
end
|
|
482
|
+
if rules.min_length && length < rules.min_length
|
|
483
|
+
add_error("minLength") { Internal::ErrorMessage.size("minLength", rules.min_length, length) }
|
|
484
|
+
end
|
|
485
|
+
if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
|
|
486
|
+
add_error("pattern") { Internal::ErrorMessage.pattern(rules.pattern) }
|
|
487
|
+
end
|
|
488
|
+
if rules.format && (@validate_format || rules.format_assertion) && !rules.format.call(value)
|
|
489
|
+
add_error("format") { Internal::ErrorMessage.format(rules.format.name) }
|
|
490
|
+
end
|
|
491
|
+
check_content(rules, value) if @validate_content && (rules.decode_base64 || rules.parse_json)
|
|
492
|
+
rescue RegexpError
|
|
493
|
+
add_error("pattern") { Internal::ErrorMessage.invalid_pattern(rules.pattern) }
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
private def check_content(rules, value)
|
|
497
|
+
decoded = rules.decode_base64 ? Base64.strict_decode64(value) : value
|
|
498
|
+
return unless rules.parse_json
|
|
499
|
+
|
|
500
|
+
JSON.parse(decoded)
|
|
501
|
+
rescue ArgumentError, JSON::ParserError
|
|
502
|
+
keyword = rules.decode_base64 ? "contentEncoding" : "contentMediaType"
|
|
503
|
+
add_error(keyword) do
|
|
504
|
+
(keyword == "contentEncoding") ? Internal::ErrorMessage.content_encoding : Internal::ErrorMessage.content_media_type
|
|
505
|
+
end
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
private def valid_array?(rules, value)
|
|
509
|
+
length = value.length
|
|
510
|
+
return false unless valid_container_bounds?(length, rules.max_items, rules.min_items)
|
|
511
|
+
return false if rules.unique && !unique_items?(value)
|
|
512
|
+
|
|
513
|
+
valid_array_children?(rules, value, length) { |child, item| evaluate_valid(child, item) }
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
private def valid_object?(rules, value)
|
|
517
|
+
length = value.length
|
|
518
|
+
return false unless valid_container_bounds?(length, rules.max_properties, rules.min_properties)
|
|
519
|
+
return false unless valid_required_keys?(rules.required, value)
|
|
520
|
+
|
|
521
|
+
valid_object_children?(rules, value) { |child, item| evaluate_valid(child, item) }
|
|
522
|
+
end
|
|
523
|
+
|
|
524
|
+
private def check_combiner(opcode, operand, value)
|
|
525
|
+
evaluation = Evaluation.valid
|
|
526
|
+
case opcode
|
|
527
|
+
when :allOf
|
|
528
|
+
index = 0
|
|
529
|
+
while index < operand.length
|
|
530
|
+
@budget&.checkpoint
|
|
531
|
+
evaluation = evaluation.merge(evaluate_at(operand[index], value, MISSING_SEGMENT, "allOf", index))
|
|
532
|
+
index += 1
|
|
533
|
+
end
|
|
534
|
+
when :anyOf
|
|
535
|
+
matches = 0
|
|
536
|
+
index = 0
|
|
537
|
+
while index < operand.length
|
|
538
|
+
@budget&.checkpoint
|
|
539
|
+
result = trial(operand[index], value)
|
|
540
|
+
if result.valid?
|
|
541
|
+
matches += 1
|
|
542
|
+
evaluation = evaluation.merge(result)
|
|
543
|
+
end
|
|
544
|
+
index += 1
|
|
545
|
+
end
|
|
546
|
+
add_error("anyOf") { Internal::ErrorMessage.any_of } if matches.zero?
|
|
547
|
+
when :oneOf
|
|
548
|
+
matches = 0
|
|
549
|
+
matched_evaluation = nil
|
|
550
|
+
index = 0
|
|
551
|
+
while index < operand.length
|
|
552
|
+
@budget&.checkpoint
|
|
553
|
+
result = trial(operand[index], value)
|
|
554
|
+
if result.valid?
|
|
555
|
+
matches += 1
|
|
556
|
+
matched_evaluation = result
|
|
557
|
+
end
|
|
558
|
+
index += 1
|
|
559
|
+
end
|
|
560
|
+
if matches == 1
|
|
561
|
+
evaluation = evaluation.merge(matched_evaluation)
|
|
562
|
+
else
|
|
563
|
+
add_error("oneOf") { Internal::ErrorMessage.one_of(matches) }
|
|
564
|
+
end
|
|
565
|
+
when :not
|
|
566
|
+
add_error("not") { Internal::ErrorMessage.not } if trial(operand, value).valid?
|
|
567
|
+
when :conditional
|
|
568
|
+
condition = trial(operand.condition, value)
|
|
569
|
+
condition_valid = condition.valid?
|
|
570
|
+
evaluation = evaluation.merge(condition) if condition_valid
|
|
571
|
+
if condition_valid && operand.then_branch
|
|
572
|
+
evaluation = evaluation.merge(evaluate_at(operand.then_branch, value, MISSING_SEGMENT, "then"))
|
|
573
|
+
elsif !condition_valid && operand.else_branch
|
|
574
|
+
evaluation = evaluation.merge(evaluate_at(operand.else_branch, value, MISSING_SEGMENT, "else"))
|
|
575
|
+
end
|
|
576
|
+
end
|
|
577
|
+
evaluation
|
|
578
|
+
end
|
|
579
|
+
|
|
580
|
+
private def trial(program, value)
|
|
581
|
+
saved_errors = @errors
|
|
582
|
+
saved_count = @error_count
|
|
583
|
+
saved_instance_path = @instance_path
|
|
584
|
+
@errors = nil
|
|
585
|
+
@error_count = 0
|
|
586
|
+
# Trials retain annotations, but discard errors. Descendants therefore
|
|
587
|
+
# need neither instance nor schema paths until the trial has returned.
|
|
588
|
+
if saved_instance_path
|
|
589
|
+
saved_schema_path = @schema_path
|
|
590
|
+
@instance_path = nil
|
|
591
|
+
@schema_path = nil
|
|
592
|
+
end
|
|
593
|
+
evaluate(program, value)
|
|
594
|
+
ensure
|
|
595
|
+
@errors = saved_errors
|
|
596
|
+
@error_count = saved_count
|
|
597
|
+
@native_reports[0] = self if @native_reports
|
|
598
|
+
if saved_instance_path
|
|
599
|
+
@instance_path = saved_instance_path
|
|
600
|
+
@schema_path = saved_schema_path
|
|
601
|
+
end
|
|
602
|
+
end
|
|
603
|
+
|
|
604
|
+
private def evaluate_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
|
|
605
|
+
return evaluate(program, instance) unless @instance_path
|
|
606
|
+
|
|
607
|
+
evaluate_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
|
|
608
|
+
end
|
|
609
|
+
|
|
610
|
+
private def evaluate_child_at(program, instance, instance_segment, schema_segment, child_segment = MISSING_SEGMENT)
|
|
611
|
+
return evaluate_at(program, instance, instance_segment, schema_segment, child_segment).valid? if @errors
|
|
612
|
+
|
|
613
|
+
valid = evaluate_valid(program, instance)
|
|
614
|
+
@error_count += 1 unless valid
|
|
615
|
+
valid
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
private def evaluate_at_with_path(program, instance, instance_segment, schema_segment, child_segment)
|
|
619
|
+
@instance_path << instance_segment unless instance_segment.equal?(MISSING_SEGMENT)
|
|
620
|
+
@schema_path << schema_segment
|
|
621
|
+
@schema_path << child_segment unless child_segment.equal?(MISSING_SEGMENT)
|
|
622
|
+
evaluate(program, instance)
|
|
623
|
+
ensure
|
|
624
|
+
@schema_path.pop unless child_segment.equal?(MISSING_SEGMENT)
|
|
625
|
+
@schema_path.pop
|
|
626
|
+
@instance_path.pop unless instance_segment.equal?(MISSING_SEGMENT)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
private def add_error(keyword, message = nil, append_keyword: true)
|
|
630
|
+
@error_count += 1
|
|
631
|
+
return false unless @errors
|
|
632
|
+
|
|
633
|
+
final_segment = append_keyword ? keyword : MISSING_SEGMENT
|
|
634
|
+
@errors << ValidationError.new(
|
|
635
|
+
keyword: keyword,
|
|
636
|
+
instance_path: pointer(@instance_path),
|
|
637
|
+
schema_path: pointer(@schema_path, final_segment),
|
|
638
|
+
message: message || yield
|
|
639
|
+
)
|
|
640
|
+
false
|
|
641
|
+
end
|
|
642
|
+
|
|
643
|
+
private def number?(value)
|
|
644
|
+
Internal::TypeChecks.number?(value)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
private def pointer(path, final_segment = MISSING_SEGMENT)
|
|
648
|
+
result = +""
|
|
649
|
+
path.each { |segment| append_pointer_segment(result, segment) }
|
|
650
|
+
append_pointer_segment(result, final_segment) unless final_segment.equal?(MISSING_SEGMENT)
|
|
651
|
+
result
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
private def append_pointer_segment(pointer, segment)
|
|
655
|
+
pointer << "/" << Internal::JsonPointer.escape(segment)
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
private_constant :MISSING_SEGMENT
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
private_constant :VM
|
|
663
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../limit_exceeded"
|
|
4
|
+
require_relative "../evaluation_limits"
|
|
5
|
+
require_relative "evaluator"
|
|
6
|
+
|
|
7
|
+
module Schemurai
|
|
8
|
+
module VM
|
|
9
|
+
# VM execution with invocation-local limits; no Ruby evaluator delegation.
|
|
10
|
+
class LimitedEvaluator < Evaluator
|
|
11
|
+
include Internal::ValueEquality::Checkpoints
|
|
12
|
+
include Internal::StringChecks::TimedRegexps
|
|
13
|
+
include Internal::EvaluationLimits
|
|
14
|
+
|
|
15
|
+
private def add_error(keyword, message = nil, append_keyword: true, &block)
|
|
16
|
+
@budget.reserve_error if @errors
|
|
17
|
+
super
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private def number_state
|
|
21
|
+
state = super
|
|
22
|
+
state.budget = @budget
|
|
23
|
+
state
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|