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,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,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Schemurai
|
|
4
|
+
module Internal
|
|
5
|
+
module UniqueItems
|
|
6
|
+
private def unique_items?(values)
|
|
7
|
+
# Avoid building an index for small arrays and early duplicates.
|
|
8
|
+
if values.length <= 16
|
|
9
|
+
values.each_with_index do |item, index|
|
|
10
|
+
@budget&.checkpoint
|
|
11
|
+
index.times { |previous| return false if json_equal?(values[previous], item) }
|
|
12
|
+
end
|
|
13
|
+
return true
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
buckets = {}
|
|
17
|
+
# Store a single index until a collision needs a bucket array.
|
|
18
|
+
index = 0
|
|
19
|
+
while index < values.length
|
|
20
|
+
@budget&.checkpoint
|
|
21
|
+
item = values[index]
|
|
22
|
+
fingerprint = json_fingerprint(item)
|
|
23
|
+
if (bucket = buckets[fingerprint])
|
|
24
|
+
if bucket.is_a?(Integer)
|
|
25
|
+
return false if json_equal?(values[bucket], item)
|
|
26
|
+
|
|
27
|
+
buckets[fingerprint] = [bucket, index]
|
|
28
|
+
else
|
|
29
|
+
return false if bucket.any? { |previous| json_equal?(values[previous], item) }
|
|
30
|
+
|
|
31
|
+
bucket << index
|
|
32
|
+
end
|
|
33
|
+
else
|
|
34
|
+
buckets[fingerprint] = index
|
|
35
|
+
end
|
|
36
|
+
index += 1
|
|
37
|
+
end
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private def json_fingerprint(value)
|
|
42
|
+
case value
|
|
43
|
+
when Numeric
|
|
44
|
+
# JSON numbers compare across Integer/Float. Converting integral
|
|
45
|
+
# floats to integers also preserves precision for large integers.
|
|
46
|
+
return value.hash if value.is_a?(Integer)
|
|
47
|
+
|
|
48
|
+
number = value.to_f
|
|
49
|
+
(number.finite? && number == number.to_i) ? number.to_i.hash : number.hash
|
|
50
|
+
when Array
|
|
51
|
+
# Mask before multiplication to keep intermediate values immediate
|
|
52
|
+
# integers; hash each step to retain order without temporary arrays.
|
|
53
|
+
fingerprint = value.class.hash
|
|
54
|
+
value.each { |item| fingerprint = (((fingerprint & 0x1fffffff) * 31) ^ json_fingerprint(item)).hash }
|
|
55
|
+
fingerprint
|
|
56
|
+
when Hash
|
|
57
|
+
# Object key order is immaterial. Hash collisions are resolved using
|
|
58
|
+
# json_equal?, so this fingerprint never decides equality itself.
|
|
59
|
+
entries = 0
|
|
60
|
+
value.each_pair { |key, item| entries ^= (((key.hash & 0x1fffffff) * 31) ^ json_fingerprint(item)).hash }
|
|
61
|
+
(value.class.hash ^ value.length ^ entries).hash
|
|
62
|
+
else
|
|
63
|
+
value.hash
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
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
|