schemurai 3.0.1 → 3.0.2
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 +4 -4
- data/README.md +6 -4
- data/ext/schemurai_native/Cargo.lock +48 -1
- data/ext/schemurai_native/Cargo.toml +3 -1
- data/ext/schemurai_native/extconf.rb +1 -1
- data/ext/schemurai_native/glue.c +1679 -62
- data/ext/schemurai_native/protocol.def +45 -0
- data/ext/schemurai_native/src/budget.rs +49 -0
- data/ext/schemurai_native/src/container.rs +460 -0
- data/ext/schemurai_native/src/content.rs +56 -27
- data/ext/schemurai_native/src/diagnostics/array.rs +392 -0
- data/ext/schemurai_native/src/diagnostics/object.rs +218 -0
- data/ext/schemurai_native/src/diagnostics.rs +1087 -307
- data/ext/schemurai_native/src/enumeration.rs +6 -1
- data/ext/schemurai_native/src/evaluation.rs +49 -20
- data/ext/schemurai_native/src/format.rs +8 -6
- data/ext/schemurai_native/src/graph.rs +37 -0
- data/ext/schemurai_native/src/instance_graph.rs +90 -0
- data/ext/schemurai_native/src/lib.rs +910 -237
- data/ext/schemurai_native/src/memo.rs +11 -38
- data/ext/schemurai_native/src/number/arbitrary.rs +154 -0
- data/ext/schemurai_native/src/number.rs +250 -59
- data/ext/schemurai_native/src/pattern.rs +113 -101
- data/ext/schemurai_native/src/pointer.rs +97 -0
- data/ext/schemurai_native/src/runtime/properties.rs +162 -0
- data/ext/schemurai_native/src/runtime/reports.rs +266 -0
- data/ext/schemurai_native/src/runtime/scopes.rs +430 -0
- data/ext/schemurai_native/src/runtime.rs +1020 -0
- data/ext/schemurai_native/src/scope.rs +60 -16
- data/ext/schemurai_native/src/string.rs +68 -0
- data/ext/schemurai_native/src/tracking.rs +244 -174
- 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 +0 -12
- data/lib/schemurai/ecma_regexp.rb +46 -0
- data/lib/schemurai/evaluation_pool.rb +66 -0
- data/lib/schemurai/evaluator.rb +9 -48
- data/lib/schemurai/json_pointer.rb +11 -0
- data/lib/schemurai/native/diagnostics.rb +4 -3
- data/lib/schemurai/native/evaluator.rb +38 -10
- data/lib/schemurai/native/null_numeric.rb +23 -0
- data/lib/schemurai/number_checks.rb +128 -0
- data/lib/schemurai/string_checks.rb +166 -0
- data/lib/schemurai/type_checks.rb +59 -0
- data/lib/schemurai/value_equality.rb +72 -0
- data/lib/schemurai/version.rb +1 -1
- data/lib/schemurai/vm/compiler.rb +18 -26
- data/lib/schemurai/vm/evaluator.rb +87 -545
- data/lib/schemurai/vm/limited_evaluator.rb +7 -22
- metadata +24 -2
data/lib/schemurai/evaluator.rb
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "decimal"
|
|
4
|
+
require_relative "type_checks"
|
|
3
5
|
require "json"
|
|
4
6
|
require "base64"
|
|
5
7
|
require_relative "evaluation"
|
|
6
8
|
require_relative "error_message"
|
|
7
9
|
require_relative "unique_items"
|
|
10
|
+
require_relative "ecma_regexp"
|
|
11
|
+
require_relative "json_pointer"
|
|
8
12
|
|
|
9
13
|
module Schemurai
|
|
10
14
|
module Internal
|
|
@@ -747,20 +751,11 @@ module Schemurai
|
|
|
747
751
|
end
|
|
748
752
|
|
|
749
753
|
private def type?(value, type)
|
|
750
|
-
|
|
751
|
-
when "null" then value.nil?
|
|
752
|
-
when "boolean" then value == true || value == false
|
|
753
|
-
when "object" then value.is_a?(Hash)
|
|
754
|
-
when "array" then value.is_a?(Array)
|
|
755
|
-
when "number" then number?(value)
|
|
756
|
-
when "integer" then number?(value) && value.finite? && value.to_i == value
|
|
757
|
-
when "string" then value.is_a?(String)
|
|
758
|
-
else false
|
|
759
|
-
end
|
|
754
|
+
TypeChecks.single?(value, TypeChecks::BITS.fetch(type, 0))
|
|
760
755
|
end
|
|
761
756
|
|
|
762
757
|
private def number?(value)
|
|
763
|
-
|
|
758
|
+
TypeChecks.number?(value)
|
|
764
759
|
end
|
|
765
760
|
|
|
766
761
|
private def json_equal?(left, right)
|
|
@@ -784,7 +779,7 @@ module Schemurai
|
|
|
784
779
|
private def decimal(value)
|
|
785
780
|
return value if value.is_a?(Integer) || value.is_a?(Rational)
|
|
786
781
|
|
|
787
|
-
|
|
782
|
+
Internal::Decimal.parse(value)
|
|
788
783
|
end
|
|
789
784
|
|
|
790
785
|
# Ruby and ECMA-262 differ in their ASCII character classes, anchors, and
|
|
@@ -793,41 +788,7 @@ module Schemurai
|
|
|
793
788
|
regexps = (@regexps ||= {})
|
|
794
789
|
return regexps[pattern] if regexps.key?(pattern)
|
|
795
790
|
|
|
796
|
-
|
|
797
|
-
translated = +""
|
|
798
|
-
escaped = false
|
|
799
|
-
in_class = false
|
|
800
|
-
pattern.each_char do |character|
|
|
801
|
-
@budget&.checkpoint
|
|
802
|
-
if escaped
|
|
803
|
-
translated << case character
|
|
804
|
-
when "d" then in_class ? "0-9" : "[0-9]"
|
|
805
|
-
when "D" then in_class ? "^0-9" : "[^0-9]"
|
|
806
|
-
when "w" then in_class ? "A-Za-z0-9_" : "[A-Za-z0-9_]"
|
|
807
|
-
when "W" then in_class ? "^A-Za-z0-9_" : "[^A-Za-z0-9_]"
|
|
808
|
-
when "s" then in_class ? whitespace : "[#{whitespace}]"
|
|
809
|
-
when "S" then in_class ? "^#{whitespace}" : "[^#{whitespace}]"
|
|
810
|
-
else "\\#{character}"
|
|
811
|
-
end
|
|
812
|
-
escaped = false
|
|
813
|
-
elsif character == "\\"
|
|
814
|
-
escaped = true
|
|
815
|
-
elsif character == "["
|
|
816
|
-
in_class = true
|
|
817
|
-
translated << character
|
|
818
|
-
elsif character == "]"
|
|
819
|
-
in_class = false
|
|
820
|
-
translated << character
|
|
821
|
-
elsif character == "^" && !in_class
|
|
822
|
-
translated << "\\A"
|
|
823
|
-
elsif character == "$" && !in_class
|
|
824
|
-
translated << "\\z"
|
|
825
|
-
else
|
|
826
|
-
translated << character
|
|
827
|
-
end
|
|
828
|
-
end
|
|
829
|
-
translated << "\\" if escaped
|
|
830
|
-
regexps[pattern] = Regexp.new(translated)
|
|
791
|
+
regexps[pattern] = Internal::EcmaRegexp.compile(pattern, @budget)
|
|
831
792
|
end
|
|
832
793
|
|
|
833
794
|
private def check_content(schema, value)
|
|
@@ -891,7 +852,7 @@ module Schemurai
|
|
|
891
852
|
end
|
|
892
853
|
|
|
893
854
|
private def append_pointer_segment(pointer, segment)
|
|
894
|
-
pointer << "/" <<
|
|
855
|
+
pointer << "/" << Internal::JsonPointer.escape(segment)
|
|
895
856
|
end
|
|
896
857
|
|
|
897
858
|
private_constant :MISSING_SEGMENT
|
|
@@ -15,9 +15,9 @@ module Schemurai
|
|
|
15
15
|
private_constant :TYPE_NAMES, :ATTRIBUTES
|
|
16
16
|
|
|
17
17
|
module_function def error(row)
|
|
18
|
-
keyword, instance_path, schema_path, opcode, operand, instance, detail = row
|
|
18
|
+
keyword, instance_path, schema_path, opcode, operand, instance, detail, rendered_message = row
|
|
19
19
|
ValidationError.new(keyword: keyword, instance_path: instance_path, schema_path: schema_path,
|
|
20
|
-
message: message(keyword, opcode, operand, instance, detail))
|
|
20
|
+
message: rendered_message || message(keyword, opcode, operand, instance, detail))
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
module_function def message(keyword, opcode, operand, instance, detail) # rubocop:disable Metrics/MethodLength
|
|
@@ -32,7 +32,7 @@ module Schemurai
|
|
|
32
32
|
when "multipleOf" then messages.multiple_of(operand.multiple_of)
|
|
33
33
|
when "maxLength", "minLength", "maxItems", "minItems", "maxProperties", "minProperties"
|
|
34
34
|
messages.size(keyword, operand.public_send(ATTRIBUTES.fetch(keyword)), detail)
|
|
35
|
-
when "pattern" then messages.pattern(operand.pattern)
|
|
35
|
+
when "pattern" then (detail == 1) ? messages.invalid_pattern(operand.pattern) : messages.pattern(operand.pattern)
|
|
36
36
|
when "format" then messages.format(operand.format.name)
|
|
37
37
|
when "contentEncoding" then messages.content_encoding
|
|
38
38
|
when "contentMediaType" then messages.content_media_type
|
|
@@ -43,6 +43,7 @@ module Schemurai
|
|
|
43
43
|
when "anyOf" then messages.any_of
|
|
44
44
|
when "oneOf" then messages.one_of(detail)
|
|
45
45
|
when "not" then messages.not
|
|
46
|
+
when "$ref", "$recursiveRef", "$dynamicRef" then operand
|
|
46
47
|
else raise "unknown native diagnostic #{keyword.inspect}"
|
|
47
48
|
end
|
|
48
49
|
end
|
|
@@ -1,8 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../
|
|
3
|
+
require_relative "../decimal"
|
|
4
|
+
require_relative "../number_checks"
|
|
5
|
+
require_relative "../string_checks"
|
|
6
|
+
require_relative "../value_equality"
|
|
7
|
+
require_relative "../container_checks"
|
|
8
|
+
require_relative "../type_checks"
|
|
9
|
+
require_relative "../vm/compiler"
|
|
10
|
+
require_relative "../evaluation_pool"
|
|
11
|
+
require_relative "../limit_budget"
|
|
12
|
+
require_relative "../ecma_regexp"
|
|
13
|
+
require_relative "../json_pointer"
|
|
4
14
|
require "schemurai_native"
|
|
5
15
|
require_relative "diagnostics"
|
|
16
|
+
require_relative "null_numeric"
|
|
6
17
|
|
|
7
18
|
module Schemurai
|
|
8
19
|
module Native
|
|
@@ -15,29 +26,37 @@ module Schemurai
|
|
|
15
26
|
@root = root
|
|
16
27
|
@content = content
|
|
17
28
|
@format = format
|
|
29
|
+
@graph_size = @graph.nodes.length
|
|
18
30
|
@compiled = compiled || Program.new(root, compiler, format, content)
|
|
31
|
+
@number_state = Internal::NumberChecks::State.new
|
|
32
|
+
@regexps = {}
|
|
33
|
+
@scope_state = []
|
|
34
|
+
@report_state = [nil, Internal::EvaluationPool.new]
|
|
35
|
+
@reference_state = []
|
|
36
|
+
@active_state = {}.compare_by_identity
|
|
19
37
|
@program = @compiled.runner(root)
|
|
20
|
-
@
|
|
38
|
+
@program.share_scalar_state(@number_state, @regexps, @scope_state, @reference_state, @active_state, @report_state)
|
|
39
|
+
@unresolved = @compiled.unresolved_references?
|
|
21
40
|
end
|
|
22
41
|
|
|
23
42
|
def valid?(instance, max_errors: nil, timeout: nil)
|
|
43
|
+
refresh_unresolved
|
|
24
44
|
if !max_errors.nil? || !timeout.nil?
|
|
25
45
|
budget = Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
26
46
|
result = @program.valid_with_budget(instance, budget)
|
|
27
47
|
budget.check_deadline!
|
|
28
|
-
return result
|
|
48
|
+
return result
|
|
29
49
|
end
|
|
30
|
-
|
|
31
|
-
result.nil? ? @vm.valid?(instance) : result
|
|
50
|
+
@program.valid?(instance)
|
|
32
51
|
end
|
|
33
52
|
|
|
34
53
|
def validate(instance, max_errors: nil, timeout: nil)
|
|
54
|
+
refresh_unresolved
|
|
35
55
|
if !max_errors.nil? || !timeout.nil?
|
|
36
56
|
budget = Internal::LimitBudget.new(max_errors: max_errors, timeout: timeout)
|
|
37
57
|
rows = @program.diagnostics_with_budget(instance, budget)
|
|
38
|
-
return limited_vm.validate(instance, budget: budget) if rows.nil?
|
|
39
58
|
|
|
40
|
-
errors = rows.map do |row|
|
|
59
|
+
errors = (rows == false) ? nil : rows.map do |row|
|
|
41
60
|
budget.reserve_error
|
|
42
61
|
Diagnostics.error(row)
|
|
43
62
|
end
|
|
@@ -45,12 +64,21 @@ module Schemurai
|
|
|
45
64
|
return Result.new(errors)
|
|
46
65
|
end
|
|
47
66
|
rows = @program.diagnostics(instance)
|
|
48
|
-
|
|
67
|
+
|
|
68
|
+
return Result.new(nil) if rows == false
|
|
49
69
|
|
|
50
70
|
Result.new(rows.empty? ? rows : rows.map { |row| Diagnostics.error(row) })
|
|
51
71
|
end
|
|
52
|
-
private def
|
|
53
|
-
|
|
72
|
+
private def refresh_unresolved
|
|
73
|
+
while @unresolved && @graph_size != @graph.nodes.length
|
|
74
|
+
graph_size = @graph.nodes.length
|
|
75
|
+
compiled = Program.new(@root, @compiler, @format, @content)
|
|
76
|
+
@program = compiled.runner(@root)
|
|
77
|
+
@program.share_scalar_state(@number_state, @regexps, @scope_state, @reference_state, @active_state, @report_state)
|
|
78
|
+
@compiled = compiled
|
|
79
|
+
@unresolved = @compiled.unresolved_references?
|
|
80
|
+
@graph_size = graph_size
|
|
81
|
+
end
|
|
54
82
|
end
|
|
55
83
|
end
|
|
56
84
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Native
|
|
5
|
+
# Native traversal selects one reached assertion and its comparison mode.
|
|
6
|
+
# Ruby dispatch preserves the exception and method visibility for nil.
|
|
7
|
+
module NullNumeric
|
|
8
|
+
module_function def valid?(value, bit, decimal)
|
|
9
|
+
actual = if decimal && !value.is_a?(Integer) && !value.is_a?(Rational)
|
|
10
|
+
Internal::Decimal.parse(value)
|
|
11
|
+
else
|
|
12
|
+
value
|
|
13
|
+
end
|
|
14
|
+
case bit
|
|
15
|
+
when Protocol::MAXIMUM, Protocol::MINIMUM, Protocol::EXCLUSIVE_MAXIMUM, Protocol::EXCLUSIVE_MINIMUM
|
|
16
|
+
Internal::NumberChecks.bound?(actual, bit, nil)
|
|
17
|
+
when Protocol::MULTIPLE_OF then nil.positive? && actual.remainder(nil).zero?
|
|
18
|
+
else raise "unknown null numeric assertion"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "decimal"
|
|
4
|
+
require_relative "type_checks"
|
|
5
|
+
|
|
6
|
+
module Schemurai
|
|
7
|
+
module Internal
|
|
8
|
+
module NumberChecks
|
|
9
|
+
MAXIMUM = 1 << 0
|
|
10
|
+
MINIMUM = 1 << 1
|
|
11
|
+
EXCLUSIVE_MAXIMUM = 1 << 2
|
|
12
|
+
EXCLUSIVE_MINIMUM = 1 << 3
|
|
13
|
+
MULTIPLE_OF = 1 << 4
|
|
14
|
+
|
|
15
|
+
class State
|
|
16
|
+
CACHE_LIMIT = 16
|
|
17
|
+
private_constant :CACHE_LIMIT
|
|
18
|
+
|
|
19
|
+
attr_writer :budget
|
|
20
|
+
|
|
21
|
+
def decimal(value)
|
|
22
|
+
@budget&.check_deadline!
|
|
23
|
+
result = cached_decimal(value)
|
|
24
|
+
@budget&.check_deadline!
|
|
25
|
+
result
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private def cached_decimal(value)
|
|
29
|
+
return value if value.is_a?(Integer) || value.is_a?(Rational)
|
|
30
|
+
|
|
31
|
+
decimals = (@decimals ||= {})
|
|
32
|
+
return decimals[value] if decimals.key?(value)
|
|
33
|
+
|
|
34
|
+
decimals.clear if decimals.length >= CACHE_LIMIT
|
|
35
|
+
decimals[value] = Decimal.parse(value)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def multiple?(rules, value)
|
|
39
|
+
results = (@multiple_results ||= {}.compare_by_identity)
|
|
40
|
+
values = (results[rules] ||= {})
|
|
41
|
+
return values[value] if values.key?(value)
|
|
42
|
+
|
|
43
|
+
values.clear if values.length >= CACHE_LIMIT
|
|
44
|
+
divisor = rules.multiple_of
|
|
45
|
+
values[value] = divisor.positive? && decimal(value).remainder(divisor).zero?
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
module_function def actual(rules, value, state)
|
|
50
|
+
(rules.mask & MULTIPLE_OF).zero? ? value : state.decimal(value)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
module_function def bound?(actual, bit, limit)
|
|
54
|
+
violated = case bit
|
|
55
|
+
when MAXIMUM then actual > limit
|
|
56
|
+
when MINIMUM then actual < limit
|
|
57
|
+
when EXCLUSIVE_MAXIMUM then actual >= limit
|
|
58
|
+
when EXCLUSIVE_MINIMUM then actual <= limit
|
|
59
|
+
else raise "unknown numeric bound"
|
|
60
|
+
end
|
|
61
|
+
violated ? false : true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Both the VM and native validity frames use the same ordered rules.
|
|
65
|
+
module Evaluation
|
|
66
|
+
private def number_state
|
|
67
|
+
@number_state ||= State.new
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private def valid_number?(rules, value)
|
|
71
|
+
mask = rules.mask
|
|
72
|
+
actual = (mask & MULTIPLE_OF).zero? ? value : number_state.decimal(value)
|
|
73
|
+
return false if (mask & MAXIMUM) != 0 && actual > rules.maximum
|
|
74
|
+
return false if (mask & MINIMUM) != 0 && actual < rules.minimum
|
|
75
|
+
return false if (mask & EXCLUSIVE_MAXIMUM) != 0 && actual >= rules.exclusive_maximum
|
|
76
|
+
return false if (mask & EXCLUSIVE_MINIMUM) != 0 && actual <= rules.exclusive_minimum
|
|
77
|
+
return true if (mask & MULTIPLE_OF).zero?
|
|
78
|
+
|
|
79
|
+
valid_multiple?(rules, value)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
private def valid_multiple?(rules, value)
|
|
83
|
+
number_state.multiple?(rules, value)
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# One native numeric instruction owns this frame while Rust yields between
|
|
88
|
+
# reached assertions. No instance method runs during construction.
|
|
89
|
+
class Frame
|
|
90
|
+
include Evaluation
|
|
91
|
+
|
|
92
|
+
def initialize(rules, value, state)
|
|
93
|
+
@rules = rules
|
|
94
|
+
@value = value
|
|
95
|
+
@number_state = state
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def numeric? = TypeChecks.number?(@value)
|
|
99
|
+
def integral? = TypeChecks.integral?(@value)
|
|
100
|
+
|
|
101
|
+
def valid?(required)
|
|
102
|
+
return required.zero? unless numeric?
|
|
103
|
+
if required == TypeChecks::INTEGER
|
|
104
|
+
return false unless integral?
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
valid_number?(@rules, @value)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def prepare
|
|
111
|
+
@actual = NumberChecks.actual(@rules, @value, @number_state)
|
|
112
|
+
true
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def check(bit)
|
|
116
|
+
case bit
|
|
117
|
+
when MAXIMUM then NumberChecks.bound?(@actual, bit, @rules.maximum)
|
|
118
|
+
when MINIMUM then NumberChecks.bound?(@actual, bit, @rules.minimum)
|
|
119
|
+
when EXCLUSIVE_MAXIMUM then NumberChecks.bound?(@actual, bit, @rules.exclusive_maximum)
|
|
120
|
+
when EXCLUSIVE_MINIMUM then NumberChecks.bound?(@actual, bit, @rules.exclusive_minimum)
|
|
121
|
+
when MULTIPLE_OF then @number_state.multiple?(@rules, @value)
|
|
122
|
+
else raise "unknown numeric assertion"
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "base64"
|
|
4
|
+
require "json"
|
|
5
|
+
require "ipaddr"
|
|
6
|
+
require_relative "ecma_regexp"
|
|
7
|
+
require_relative "error_message"
|
|
8
|
+
|
|
9
|
+
module Schemurai
|
|
10
|
+
module Internal
|
|
11
|
+
module StringChecks
|
|
12
|
+
module Regexps
|
|
13
|
+
private def ecma_regexp(pattern)
|
|
14
|
+
regexps = regexp_cache
|
|
15
|
+
return regexps[pattern] if regexps.key?(pattern)
|
|
16
|
+
|
|
17
|
+
regexps[pattern] = EcmaRegexp.compile(pattern, @budget)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private def regexp_cache = (@regexps ||= {})
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
module TimedRegexps
|
|
24
|
+
private def regexp_budget = @budget
|
|
25
|
+
|
|
26
|
+
private def ecma_regexp(pattern)
|
|
27
|
+
budget = regexp_budget
|
|
28
|
+
budget.check_deadline!
|
|
29
|
+
result = super
|
|
30
|
+
budget.check_deadline!
|
|
31
|
+
result
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
module Evaluation
|
|
36
|
+
private def valid_string?(rules, value)
|
|
37
|
+
length = value.length
|
|
38
|
+
return false if rules.max_length && length > rules.max_length
|
|
39
|
+
return false if rules.min_length && length < rules.min_length
|
|
40
|
+
return false if rules.pattern && !ecma_regexp(rules.pattern).match?(value)
|
|
41
|
+
if rules.format && (@validate_format || rules.format_assertion)
|
|
42
|
+
return false unless rules.format.call(value)
|
|
43
|
+
end
|
|
44
|
+
if @validate_content && (rules.decode_base64 || rules.parse_json)
|
|
45
|
+
return valid_content?(rules, value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
true
|
|
49
|
+
rescue RegexpError, IPAddr::InvalidAddressError
|
|
50
|
+
false
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private def valid_content?(rules, value)
|
|
54
|
+
decoded = rules.decode_base64 ? Base64.strict_decode64(value) : value
|
|
55
|
+
JSON.parse(decoded) if rules.parse_json
|
|
56
|
+
true
|
|
57
|
+
rescue ArgumentError, JSON::ParserError
|
|
58
|
+
false
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class State
|
|
63
|
+
include Regexps
|
|
64
|
+
|
|
65
|
+
attr_reader :budget
|
|
66
|
+
|
|
67
|
+
def ecma_regexp(pattern)
|
|
68
|
+
@budget&.check_deadline!
|
|
69
|
+
result = super
|
|
70
|
+
@budget&.check_deadline!
|
|
71
|
+
result
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def initialize(regexps, budget)
|
|
75
|
+
@regexps = regexps
|
|
76
|
+
@budget = budget
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Rust owns traversal and error limits; this frame retains arbitrary Ruby
|
|
81
|
+
# length results and the VM's rescue state between reached assertions.
|
|
82
|
+
class Frame
|
|
83
|
+
include Evaluation
|
|
84
|
+
|
|
85
|
+
def initialize(rules, value, state, flags)
|
|
86
|
+
@rules = rules
|
|
87
|
+
@value = value
|
|
88
|
+
@state = state
|
|
89
|
+
@budget = state.budget
|
|
90
|
+
@validate_format = (flags & 1) != 0
|
|
91
|
+
@validate_content = (flags & 2) != 0
|
|
92
|
+
@required = (flags & 64) != 0
|
|
93
|
+
@invalid_pattern = false
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def string? = @value.is_a?(String)
|
|
97
|
+
def invalid_pattern? = @invalid_pattern
|
|
98
|
+
|
|
99
|
+
def valid?
|
|
100
|
+
return !@required unless string?
|
|
101
|
+
|
|
102
|
+
valid_string?(@rules, @value)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def prepare
|
|
106
|
+
@budget&.check_deadline!
|
|
107
|
+
check(2)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def check(step)
|
|
111
|
+
case step
|
|
112
|
+
when 2
|
|
113
|
+
@length = @value.length
|
|
114
|
+
true
|
|
115
|
+
when 3
|
|
116
|
+
exceeded = @length > @rules.max_length
|
|
117
|
+
exceeded ? false : true
|
|
118
|
+
when 4
|
|
119
|
+
below = @length < @rules.min_length
|
|
120
|
+
below ? false : true
|
|
121
|
+
when 5 then (!ecma_regexp(@rules.pattern).match?(@value)) ? false : true
|
|
122
|
+
when 6 then (!@rules.format.call(@value)) ? false : true
|
|
123
|
+
when 7 then valid_content?(@rules, @value)
|
|
124
|
+
else raise "unknown string assertion"
|
|
125
|
+
end
|
|
126
|
+
rescue RegexpError
|
|
127
|
+
@invalid_pattern = true
|
|
128
|
+
false
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def message(keyword, rescued)
|
|
132
|
+
return ErrorMessage.invalid_pattern(@rules.pattern) if rescued
|
|
133
|
+
|
|
134
|
+
case keyword
|
|
135
|
+
when "type" then ErrorMessage.type("string", @value)
|
|
136
|
+
when "maxLength" then ErrorMessage.size(keyword, @rules.max_length, @length)
|
|
137
|
+
when "minLength" then ErrorMessage.size(keyword, @rules.min_length, @length)
|
|
138
|
+
when "pattern" then @invalid_pattern ? ErrorMessage.invalid_pattern(@rules.pattern) : ErrorMessage.pattern(@rules.pattern)
|
|
139
|
+
when "format" then ErrorMessage.format(@rules.format.name)
|
|
140
|
+
when "contentEncoding" then ErrorMessage.content_encoding
|
|
141
|
+
when "contentMediaType" then ErrorMessage.content_media_type
|
|
142
|
+
else raise "unknown string diagnostic"
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
private def valid_string?(rules, value)
|
|
147
|
+
@budget&.check_deadline!
|
|
148
|
+
result = super
|
|
149
|
+
@budget&.check_deadline!
|
|
150
|
+
result
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
private def valid_content?(rules, value)
|
|
154
|
+
@budget&.check_deadline!
|
|
155
|
+
result = super
|
|
156
|
+
@budget&.check_deadline!
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
private def ecma_regexp(pattern)
|
|
161
|
+
@state.ecma_regexp(pattern)
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
module TypeChecks
|
|
6
|
+
NULL = 1 << 0
|
|
7
|
+
BOOLEAN = 1 << 1
|
|
8
|
+
OBJECT = 1 << 2
|
|
9
|
+
ARRAY = 1 << 3
|
|
10
|
+
NUMBER = 1 << 4
|
|
11
|
+
INTEGER = 1 << 5
|
|
12
|
+
STRING = 1 << 6
|
|
13
|
+
BITS = {"null" => NULL, "boolean" => BOOLEAN, "object" => OBJECT, "array" => ARRAY,
|
|
14
|
+
"number" => NUMBER, "integer" => INTEGER, "string" => STRING}.freeze
|
|
15
|
+
|
|
16
|
+
module_function def number?(value)
|
|
17
|
+
value.is_a?(Numeric) && !value.is_a?(Complex)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
module_function def integer?(value)
|
|
21
|
+
number?(value) && integral?(value)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
module_function def integral?(value)
|
|
25
|
+
value.finite? && value.to_i == value
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
module_function def single?(value, mask)
|
|
29
|
+
case mask
|
|
30
|
+
when NULL then value.nil?
|
|
31
|
+
when BOOLEAN then value == true || value == false
|
|
32
|
+
when OBJECT then value.is_a?(Hash)
|
|
33
|
+
when ARRAY then value.is_a?(Array)
|
|
34
|
+
when NUMBER then number?(value)
|
|
35
|
+
when INTEGER then integer?(value)
|
|
36
|
+
when STRING then value.is_a?(String)
|
|
37
|
+
else false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# The VM's union opcode classifies in this order, even for a one-name union.
|
|
42
|
+
module_function def instance_mask(value, integer:)
|
|
43
|
+
return NULL if value.nil?
|
|
44
|
+
return BOOLEAN if value == true || value == false
|
|
45
|
+
return OBJECT if value.is_a?(Hash)
|
|
46
|
+
return ARRAY if value.is_a?(Array)
|
|
47
|
+
return STRING if value.is_a?(String)
|
|
48
|
+
return 0 unless number?(value)
|
|
49
|
+
return NUMBER unless integer
|
|
50
|
+
|
|
51
|
+
integral?(value) ? NUMBER | INTEGER : NUMBER
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
module_function def valid?(value, mask, union)
|
|
55
|
+
union ? (mask & instance_mask(value, integer: (mask & INTEGER) != 0)) != 0 : single?(value, mask)
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "unique_items"
|
|
4
|
+
|
|
5
|
+
module Schemurai
|
|
6
|
+
module Internal
|
|
7
|
+
# Ruby method order for VM/native value comparisons. This traverses values,
|
|
8
|
+
# not schema instructions, and retains custom collection/equality methods.
|
|
9
|
+
module ValueEquality
|
|
10
|
+
private def json_equal?(left, right)
|
|
11
|
+
if left.is_a?(Numeric) && !left.is_a?(Complex)
|
|
12
|
+
return right.is_a?(Numeric) && !right.is_a?(Complex) && left == right
|
|
13
|
+
end
|
|
14
|
+
return false unless left.instance_of?(right.class)
|
|
15
|
+
|
|
16
|
+
case left
|
|
17
|
+
when Hash
|
|
18
|
+
left.length == right.length && left.all? do |key, value|
|
|
19
|
+
@budget&.checkpoint
|
|
20
|
+
right.key?(key) && json_equal?(value, right[key])
|
|
21
|
+
end
|
|
22
|
+
when Array
|
|
23
|
+
return false unless left.length == right.length
|
|
24
|
+
|
|
25
|
+
index = 0
|
|
26
|
+
while index < left.length
|
|
27
|
+
@budget&.checkpoint
|
|
28
|
+
return false unless json_equal?(left[index], right[index])
|
|
29
|
+
index += 1
|
|
30
|
+
end
|
|
31
|
+
true
|
|
32
|
+
else
|
|
33
|
+
left == right
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
module Checkpoints
|
|
38
|
+
private def json_equal?(left, right)
|
|
39
|
+
@budget.checkpoint
|
|
40
|
+
super
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private def json_fingerprint(value)
|
|
44
|
+
@budget.checkpoint
|
|
45
|
+
super
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Runtime
|
|
50
|
+
include ValueEquality
|
|
51
|
+
include UniqueItems
|
|
52
|
+
|
|
53
|
+
def initialize(budget)
|
|
54
|
+
@budget = budget
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def evaluate(operation, operand, instance)
|
|
58
|
+
case operation
|
|
59
|
+
when 0 then json_equal?(operand, instance)
|
|
60
|
+
when 1 then operand.any? { |candidate| json_equal?(candidate, instance) }
|
|
61
|
+
when 2 then unique_items?(instance)
|
|
62
|
+
else raise "unknown equality operation"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class LimitedRuntime < Runtime
|
|
68
|
+
include Checkpoints
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
data/lib/schemurai/version.rb
CHANGED