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.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +395 -0
  4. data/lib/schemurai/backend.rb +34 -0
  5. data/lib/schemurai/container_checks/array_diagnostics.rb +119 -0
  6. data/lib/schemurai/container_checks/object_diagnostics.rb +152 -0
  7. data/lib/schemurai/container_checks.rb +534 -0
  8. data/lib/schemurai/decimal.rb +11 -0
  9. data/lib/schemurai/dialect.rb +106 -0
  10. data/lib/schemurai/dialect_keywords.rb +91 -0
  11. data/lib/schemurai/dialects/draft2019_09.rb +27 -0
  12. data/lib/schemurai/dialects/draft2020_12.rb +27 -0
  13. data/lib/schemurai/dialects/draft7.rb +27 -0
  14. data/lib/schemurai/ecma_regexp.rb +46 -0
  15. data/lib/schemurai/error_message.rb +102 -0
  16. data/lib/schemurai/evaluation.rb +63 -0
  17. data/lib/schemurai/evaluation_limits.rb +65 -0
  18. data/lib/schemurai/evaluation_pool.rb +66 -0
  19. data/lib/schemurai/evaluator.rb +861 -0
  20. data/lib/schemurai/formats.rb +403 -0
  21. data/lib/schemurai/json_pointer.rb +11 -0
  22. data/lib/schemurai/limit_budget.rb +78 -0
  23. data/lib/schemurai/limit_exceeded.rb +18 -0
  24. data/lib/schemurai/limited_evaluator.rb +43 -0
  25. data/lib/schemurai/meta_schemas/draft2019_09.rb +149 -0
  26. data/lib/schemurai/meta_schemas/draft2020_12.rb +149 -0
  27. data/lib/schemurai/meta_schemas/draft7.rb +121 -0
  28. data/lib/schemurai/meta_schemas.rb +28 -0
  29. data/lib/schemurai/native/4.0/schemurai_native.so +0 -0
  30. data/lib/schemurai/native/diagnostics.rb +53 -0
  31. data/lib/schemurai/native/evaluator.rb +87 -0
  32. data/lib/schemurai/native/null_numeric.rb +23 -0
  33. data/lib/schemurai/number_checks.rb +128 -0
  34. data/lib/schemurai/schema_domain.rb +95 -0
  35. data/lib/schemurai/schema_graph.rb +538 -0
  36. data/lib/schemurai/schema_node.rb +55 -0
  37. data/lib/schemurai/string_checks.rb +166 -0
  38. data/lib/schemurai/type_checks.rb +59 -0
  39. data/lib/schemurai/unique_items.rb +68 -0
  40. data/lib/schemurai/value_equality.rb +72 -0
  41. data/lib/schemurai/version.rb +6 -0
  42. data/lib/schemurai/vm/compiler.rb +410 -0
  43. data/lib/schemurai/vm/evaluator.rb +663 -0
  44. data/lib/schemurai/vm/limited_evaluator.rb +27 -0
  45. data/lib/schemurai.rb +307 -0
  46. data/lib/schemurai_native.rb +4 -0
  47. data/sig/schemurai.rbs +108 -0
  48. metadata +121 -0
@@ -0,0 +1,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ module Formats
6
+ class Format
7
+ attr_reader :name
8
+
9
+ def initialize(name = nil)
10
+ @name = name
11
+ freeze
12
+ end
13
+
14
+ def call(_value)
15
+ true
16
+ end
17
+
18
+ private def valid_date_at?(value, start)
19
+ return false unless value.getbyte(start + 4) == 45 && value.getbyte(start + 7) == 45
20
+
21
+ century = two_digits(value, start)
22
+ year_in_century = two_digits(value, start + 2)
23
+ month = two_digits(value, start + 5)
24
+ day = two_digits(value, start + 8)
25
+ return false unless century && year_in_century
26
+
27
+ year = (century * 100) + year_in_century
28
+ return false unless year && month && day && month.between?(1, 12)
29
+
30
+ last_day = if month == 2
31
+ leap_year?(year) ? 29 : 28
32
+ elsif month == 4 || month == 6 || month == 9 || month == 11
33
+ 30
34
+ else
35
+ 31
36
+ end
37
+ day.between?(1, last_day)
38
+ end
39
+
40
+ private def valid_time_at?(value, start)
41
+ return false if value.bytesize < start + 9
42
+ return false unless value.getbyte(start + 2) == 58 && value.getbyte(start + 5) == 58
43
+
44
+ hour = two_digits(value, start)
45
+ minute = two_digits(value, start + 3)
46
+ second = two_digits(value, start + 6)
47
+ return false unless hour&.between?(0, 23) && minute&.between?(0, 59) && second&.between?(0, 60)
48
+
49
+ index = start + 8
50
+ if value.getbyte(index) == 46
51
+ index += 1
52
+ fraction_start = index
53
+ while (byte = value.getbyte(index)) && byte >= 48 && byte <= 57
54
+ index += 1
55
+ end
56
+ return false if index == fraction_start
57
+ end
58
+
59
+ zone = value.getbyte(index)
60
+ if zone == 90 || zone == 122
61
+ return false unless index + 1 == value.bytesize
62
+ offset = 0
63
+ else
64
+ return false unless zone == 43 || zone == 45
65
+ return false unless index + 6 == value.bytesize
66
+ return false unless value.getbyte(index + 3) == 58
67
+
68
+ offset_hour = two_digits(value, index + 1)
69
+ offset_minute = two_digits(value, index + 4)
70
+ return false unless offset_hour&.between?(0, 23) && offset_minute&.between?(0, 59)
71
+
72
+ offset = (offset_hour * 60) + offset_minute
73
+ offset = -offset if zone == 45
74
+ end
75
+
76
+ second != 60 || ((hour * 60) + minute - offset) % 1_440 == 1_439
77
+ end
78
+
79
+ private def valid_duration_time_at?(value, index)
80
+ length = value.bytesize
81
+ number_end = ascii_digits_end(value, index)
82
+ return false unless number_end
83
+
84
+ case value.getbyte(number_end)
85
+ when 72 # H
86
+ index = number_end + 1
87
+ if (number_end = ascii_digits_end(value, index))
88
+ return false unless value.getbyte(number_end) == 77 # M
89
+
90
+ index = number_end + 1
91
+ if (number_end = ascii_digits_end(value, index))
92
+ return false unless value.getbyte(number_end) == 83 # S
93
+
94
+ index = number_end + 1
95
+ end
96
+ end
97
+ when 77 # M
98
+ index = number_end + 1
99
+ if (number_end = ascii_digits_end(value, index))
100
+ return false unless value.getbyte(number_end) == 83 # S
101
+
102
+ index = number_end + 1
103
+ end
104
+ when 83 # S
105
+ index = number_end + 1
106
+ else
107
+ return false
108
+ end
109
+
110
+ index == length
111
+ end
112
+
113
+ private def valid_json_pointer_at?(value, index)
114
+ length = value.bytesize
115
+ return true if index == length
116
+ return false unless value.getbyte(index) == 47 # /
117
+
118
+ while index < length
119
+ if value.getbyte(index) == 126 # ~
120
+ index += 1
121
+ return false unless value.getbyte(index) == 48 || value.getbyte(index) == 49 # 0 or 1
122
+ end
123
+ index += 1
124
+ end
125
+
126
+ true
127
+ end
128
+
129
+ private def ascii_digits_end(value, index)
130
+ start = index
131
+ index += 1 while value.getbyte(index)&.between?(48, 57)
132
+ index unless index == start
133
+ end
134
+
135
+ private def two_digits(value, start)
136
+ tens = value.getbyte(start) - 48
137
+ ones = value.getbyte(start + 1) - 48
138
+ return unless tens.between?(0, 9) && ones.between?(0, 9)
139
+
140
+ (tens * 10) + ones
141
+ end
142
+
143
+ private def leap_year?(year)
144
+ (year % 4).zero? && (!(year % 100).zero? || (year % 400).zero?)
145
+ end
146
+
147
+ private def valid_ipv4_at?(value, index)
148
+ length = value.bytesize
149
+
150
+ 4.times do |octet|
151
+ byte = value.getbyte(index)
152
+ return false unless byte&.between?(48, 57)
153
+ return false if byte == 48 && value.getbyte(index + 1)&.between?(48, 57)
154
+
155
+ number = 0
156
+ digits = 0
157
+ while (byte = value.getbyte(index))&.between?(48, 57)
158
+ number = (number * 10) + byte - 48
159
+ digits += 1
160
+ return false if digits > 3
161
+
162
+ index += 1
163
+ end
164
+ return false if number > 255
165
+
166
+ if octet == 3
167
+ return false unless index == length
168
+ else
169
+ return false unless value.getbyte(index) == 46
170
+
171
+ index += 1
172
+ end
173
+ end
174
+
175
+ true
176
+ end
177
+ end
178
+
179
+ class DateFormat < Format
180
+ def initialize = super("date")
181
+
182
+ def call(value)
183
+ value.bytesize == 10 && valid_date_at?(value, 0)
184
+ end
185
+ end
186
+
187
+ class TimeFormat < Format
188
+ def initialize = super("time")
189
+
190
+ def call(value)
191
+ valid_time_at?(value, 0)
192
+ end
193
+ end
194
+
195
+ class DateTimeFormat < Format
196
+ def initialize = super("date-time")
197
+
198
+ def call(value)
199
+ value.bytesize >= 20 &&
200
+ valid_date_at?(value, 0) &&
201
+ (value.getbyte(10) == 84 || value.getbyte(10) == 116) &&
202
+ valid_time_at?(value, 11)
203
+ end
204
+ end
205
+
206
+ class DurationFormat < Format
207
+ def initialize = super("duration")
208
+
209
+ def call(value)
210
+ length = value.bytesize
211
+ return false unless value.getbyte(0) == 80 && length > 1
212
+
213
+ index = 1
214
+ return valid_duration_time_at?(value, index + 1) if value.getbyte(index) == 84 # T
215
+
216
+ number_end = ascii_digits_end(value, index)
217
+ return false unless number_end
218
+
219
+ case value.getbyte(number_end)
220
+ when 87 # W
221
+ return number_end + 1 == length
222
+ when 89 # Y
223
+ index = number_end + 1
224
+ if (number_end = ascii_digits_end(value, index))
225
+ return false unless value.getbyte(number_end) == 77 # M
226
+
227
+ index = number_end + 1
228
+ if (number_end = ascii_digits_end(value, index))
229
+ return false unless value.getbyte(number_end) == 68 # D
230
+
231
+ index = number_end + 1
232
+ end
233
+ end
234
+ when 77 # M
235
+ index = number_end + 1
236
+ if (number_end = ascii_digits_end(value, index))
237
+ return false unless value.getbyte(number_end) == 68 # D
238
+
239
+ index = number_end + 1
240
+ end
241
+ when 68 # D
242
+ index = number_end + 1
243
+ else
244
+ return false
245
+ end
246
+
247
+ return true if index == length
248
+ return false unless value.getbyte(index) == 84 # T
249
+
250
+ valid_duration_time_at?(value, index + 1)
251
+ end
252
+ end
253
+
254
+ class Ipv4Format < Format
255
+ def initialize = super("ipv4")
256
+
257
+ def call(value)
258
+ valid_ipv4_at?(value, 0)
259
+ end
260
+ end
261
+
262
+ class Ipv6Format < Format
263
+ def initialize = super("ipv6")
264
+
265
+ def call(value)
266
+ length = value.bytesize
267
+ return false if length.zero?
268
+
269
+ index = 0
270
+ groups = 0
271
+ compressed = false
272
+
273
+ if value.getbyte(index) == 58 # :
274
+ return false unless value.getbyte(index + 1) == 58
275
+
276
+ compressed = true
277
+ index += 2
278
+ return true if index == length
279
+ end
280
+
281
+ while index < length
282
+ group_start = index
283
+ hex_digits = 0
284
+ while (byte = value.getbyte(index)) &&
285
+ (byte.between?(48, 57) || byte.between?(65, 70) || byte.between?(97, 102))
286
+ hex_digits += 1
287
+ return false if hex_digits > 4
288
+
289
+ index += 1
290
+ end
291
+ return false if hex_digits.zero?
292
+
293
+ if value.getbyte(index) == 46 # .
294
+ return false unless groups <= 6 && valid_ipv4_at?(value, group_start)
295
+
296
+ groups += 2
297
+ index = length
298
+ break
299
+ end
300
+
301
+ groups += 1
302
+ return false if groups > 8
303
+ break if index == length
304
+ return false unless value.getbyte(index) == 58 # :
305
+
306
+ index += 1
307
+ if value.getbyte(index) == 58 # :
308
+ return false if compressed
309
+
310
+ compressed = true
311
+ index += 1
312
+ break if index == length
313
+ elsif index == length
314
+ return false
315
+ end
316
+ end
317
+
318
+ compressed ? groups < 8 : groups == 8
319
+ end
320
+ end
321
+
322
+ class JsonPointerFormat < Format
323
+ def initialize = super("json-pointer")
324
+
325
+ def call(value)
326
+ valid_json_pointer_at?(value, 0)
327
+ end
328
+ end
329
+
330
+ class RelativeJsonPointerFormat < Format
331
+ def initialize = super("relative-json-pointer")
332
+
333
+ def call(value)
334
+ length = value.bytesize
335
+ first = value.getbyte(0)
336
+ return false unless first&.between?(48, 57)
337
+
338
+ index = 1
339
+ return false if first == 48 && value.getbyte(index)&.between?(48, 57)
340
+
341
+ index += 1 while value.getbyte(index)&.between?(48, 57)
342
+ return true if index == length
343
+ return index + 1 == length if value.getbyte(index) == 35 # #
344
+
345
+ valid_json_pointer_at?(value, index)
346
+ end
347
+ end
348
+
349
+ class UuidFormat < Format
350
+ def initialize = super("uuid")
351
+
352
+ def call(value)
353
+ return false unless value.bytesize == 36
354
+
355
+ 36.times do |index|
356
+ byte = value.getbyte(index)
357
+ if index == 8 || index == 13 || index == 18 || index == 23
358
+ return false unless byte == 45 # -
359
+ else
360
+ return false unless byte&.between?(48, 57) || byte&.between?(65, 70) || byte&.between?(97, 102)
361
+ end
362
+ end
363
+
364
+ true
365
+ end
366
+ end
367
+
368
+ UNKNOWN = Format.new
369
+ DATE = DateFormat.new
370
+ TIME = TimeFormat.new
371
+ DATE_TIME = DateTimeFormat.new
372
+ DURATION = DurationFormat.new
373
+ IPV4 = Ipv4Format.new
374
+ IPV6 = Ipv6Format.new
375
+ JSON_POINTER = JsonPointerFormat.new
376
+ RELATIVE_JSON_POINTER = RelativeJsonPointerFormat.new
377
+ UUID = UuidFormat.new
378
+
379
+ class << self
380
+ def resolve(format)
381
+ case format
382
+ when "date" then DATE
383
+ when "time" then TIME
384
+ when "date-time" then DATE_TIME
385
+ when "duration" then DURATION
386
+ when "ipv4" then IPV4
387
+ when "ipv6" then IPV6
388
+ when "json-pointer" then JSON_POINTER
389
+ when "relative-json-pointer" then RELATIVE_JSON_POINTER
390
+ when "uuid" then UUID
391
+ else UNKNOWN
392
+ end
393
+ end
394
+ end
395
+
396
+ private_constant :Format, :DateFormat, :TimeFormat, :DateTimeFormat, :DurationFormat,
397
+ :Ipv4Format, :Ipv6Format, :JsonPointerFormat, :RelativeJsonPointerFormat, :UuidFormat, :UNKNOWN,
398
+ :DATE, :TIME, :DATE_TIME, :DURATION, :IPV4, :IPV6, :JSON_POINTER, :RELATIVE_JSON_POINTER, :UUID
399
+ end
400
+ end
401
+
402
+ private_constant :Internal
403
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ module JsonPointer
6
+ module_function def escape(segment)
7
+ segment.to_s.gsub("~", "~0").gsub("/", "~1")
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ module Internal
5
+ # Mutable state owned by one validation call, including native/VM fallback.
6
+ class LimitBudget
7
+ CHECK_INTERVAL = 256
8
+
9
+ def self.validate_options!(max_errors, timeout)
10
+ valid_errors = max_errors.nil? || (max_errors.is_a?(Integer) && max_errors >= 0)
11
+ unless valid_errors
12
+ raise ArgumentError, "max_errors must be a nonnegative Integer or nil"
13
+ end
14
+ valid_timeout = timeout.nil? || ((timeout.is_a?(Integer) || timeout.is_a?(Float)) && timeout >= 0 && timeout.to_f.finite?)
15
+ unless valid_timeout
16
+ raise ArgumentError, "timeout must be finite nonnegative seconds or nil"
17
+ end
18
+ end
19
+
20
+ def initialize(max_errors: nil, timeout: nil)
21
+ @max_errors = max_errors
22
+ @timeout = timeout
23
+ @errors = 0
24
+ @checks = 0
25
+ if timeout
26
+ @started = monotonic_time
27
+ @deadline = @started + timeout
28
+ check_deadline!
29
+ end
30
+ end
31
+
32
+ # Synchronous polling only; never inject exceptions into another thread.
33
+ def checkpoint
34
+ return unless @deadline
35
+
36
+ @checks += 1
37
+ check_deadline! if (@checks % CHECK_INTERVAL).zero?
38
+ end
39
+
40
+ def check_deadline!
41
+ return unless @deadline
42
+
43
+ now = monotonic_time
44
+ raise LimitExceeded.new(:timeout, @timeout, now - @started) if now >= @deadline
45
+ end
46
+
47
+ # Ruby retains the original deadline through native execution and fallback.
48
+ def native_duration
49
+ check_deadline!
50
+ @deadline && [@deadline - monotonic_time, 0.0].max
51
+ end
52
+
53
+ def native_max_errors
54
+ @max_errors && [@max_errors, (1 << (0.size * 8)) - 1].min
55
+ end
56
+
57
+ def raise_native_limit(kind, attempted)
58
+ if kind == 2
59
+ raise LimitExceeded.new(:timeout, @timeout, monotonic_time - @started)
60
+ end
61
+ raise LimitExceeded.new(:max_errors, @max_errors, attempted)
62
+ end
63
+
64
+ # Check before constructing the next message and JSON Pointers.
65
+ def reserve_error
66
+ checkpoint
67
+ attempted = @errors + 1
68
+ raise LimitExceeded.new(:max_errors, @max_errors, attempted) if @max_errors && attempted > @max_errors
69
+
70
+ @errors = attempted
71
+ end
72
+
73
+ private def monotonic_time
74
+ Process.clock_gettime(Process::CLOCK_MONOTONIC)
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Schemurai
4
+ # Raised when validation cannot finish within an application-supplied limit.
5
+ # This is an incomplete evaluation, not a JSON Schema validation failure.
6
+ class LimitExceeded < Error
7
+ # Limit name, configured maximum and attempted count (or elapsed seconds).
8
+ attr_reader :limit, :maximum, :attempted
9
+
10
+ # Creates an interruption error without embedding the input or partial errors.
11
+ def initialize(limit, maximum, attempted)
12
+ @limit = limit
13
+ @maximum = maximum
14
+ @attempted = attempted
15
+ super("validation exceeded #{limit} (maximum #{maximum}, attempted #{attempted})")
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,43 @@
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 Internal
9
+ # Ruby evaluation with invocation-local limits.
10
+ class LimitedEvaluator < Evaluator
11
+ include EvaluationLimits
12
+
13
+ private def add_error(keyword, message = nil, append_keyword: true, &block)
14
+ @budget.reserve_error if @error_callback
15
+ super
16
+ end
17
+
18
+ private def decimal(value)
19
+ @budget.check_deadline!
20
+ result = super
21
+ @budget.check_deadline!
22
+ result
23
+ end
24
+
25
+ private def ecma_regexp(pattern)
26
+ @budget.check_deadline!
27
+ result = super
28
+ @budget.check_deadline!
29
+ result
30
+ end
31
+
32
+ private def json_equal?(left, right)
33
+ @budget.checkpoint
34
+ super
35
+ end
36
+
37
+ private def json_fingerprint(value)
38
+ @budget.checkpoint
39
+ super
40
+ end
41
+ end
42
+ end
43
+ end