rjq 0.1.0

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.
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ module JSON
5
+ class InputBuffer
6
+ DEFAULT_CHUNK_SIZE = 16_384
7
+
8
+ def initialize(io_or_string, chunk_size: DEFAULT_CHUNK_SIZE)
9
+ @io = io_or_string if io_or_string.respond_to?(:read)
10
+ @chunk_size = chunk_size
11
+ @offset = 0
12
+ @eof = !@io
13
+ @buffer = @io ? +''.b : io_or_string.to_s.dup
14
+ normalize_encoding!
15
+ end
16
+
17
+ def [](index, length = nil)
18
+ if index.is_a?(Range)
19
+ range_end = index.end
20
+ range_end -= 1 if index.exclude_end?
21
+ ensure_index(range_end)
22
+ return @buffer[local_index(index.begin)..local_index(index.end)] unless index.exclude_end?
23
+
24
+ return @buffer[local_index(index.begin)...local_index(index.end)]
25
+ end
26
+
27
+ ensure_index(index + length - 1) if length
28
+ ensure_index(index) unless length
29
+ length ? @buffer[local_index(index), length] : @buffer[local_index(index)]
30
+ end
31
+
32
+ def discard_before(index)
33
+ return if index <= @offset
34
+
35
+ local = [index - @offset, @buffer.length].min
36
+ @buffer = @buffer[local..].to_s
37
+ @offset += local
38
+ end
39
+
40
+ private
41
+
42
+ def ensure_index(index)
43
+ return if index.negative?
44
+
45
+ read_more while !@eof && index >= @offset + @buffer.length
46
+ end
47
+
48
+ def read_more
49
+ loop do
50
+ chunk = @io.read(@chunk_size)
51
+ if chunk.nil? || chunk.empty?
52
+ @eof = true
53
+ validate_encoding!
54
+ return
55
+ end
56
+
57
+ @buffer = @buffer.b << chunk.b
58
+ @buffer.force_encoding(Encoding::UTF_8)
59
+ return if @buffer.valid_encoding?
60
+
61
+ validate_encoding! unless incomplete_utf8_suffix?
62
+ end
63
+ end
64
+
65
+ def normalize_encoding!
66
+ @buffer = @buffer.encode(Encoding::UTF_8)
67
+ validate_encoding!
68
+ rescue EncodingError => e
69
+ raise JSONParseError, e.message
70
+ end
71
+
72
+ def validate_encoding!
73
+ raise JSONParseError, 'invalid UTF-8 input' unless @buffer.valid_encoding?
74
+ end
75
+
76
+ def incomplete_utf8_suffix?
77
+ bytes = @buffer.bytes
78
+ start = [bytes.length - 3, 0].max
79
+ (start...bytes.length).any? do |index|
80
+ lead = bytes[index]
81
+ expected = case lead
82
+ when 0xC2..0xDF then 2
83
+ when 0xE0..0xEF then 3
84
+ when 0xF0..0xF4 then 4
85
+ end
86
+ next false unless expected
87
+
88
+ suffix = bytes[index..]
89
+ suffix.length < expected && suffix.drop(1).all? { |byte| byte.between?(0x80, 0xBF) } &&
90
+ bytes[0...index].pack('C*').force_encoding(Encoding::UTF_8).valid_encoding?
91
+ end
92
+ end
93
+
94
+ def local_index(index)
95
+ index - @offset
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,405 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ module JSON
5
+ class Parser
6
+ ParsedValue = Struct.new(:value, :line, keyword_init: true)
7
+ DEFAULT_MAX_DEPTH = 256
8
+
9
+ class << self
10
+ def parse(io_or_string, seq: false, chunk_size: InputBuffer::DEFAULT_CHUNK_SIZE, on_error: nil,
11
+ max_depth: DEFAULT_MAX_DEPTH, max_number_digits: nil, max_string_bytes: nil)
12
+ new(io_or_string, seq: seq, chunk_size: chunk_size, on_error: on_error,
13
+ max_depth: max_depth, max_number_digits: max_number_digits,
14
+ max_string_bytes: max_string_bytes).parse_stream
15
+ end
16
+
17
+ def parse_one(io_or_string, seq: false, chunk_size: InputBuffer::DEFAULT_CHUNK_SIZE,
18
+ max_depth: DEFAULT_MAX_DEPTH, max_number_digits: nil, max_string_bytes: nil)
19
+ values = parse(io_or_string, seq: seq, chunk_size: chunk_size, max_depth: max_depth,
20
+ max_number_digits: max_number_digits, max_string_bytes: max_string_bytes).take(2)
21
+ raise JSONParseError, "expected one JSON value, got #{values.length}" unless values.length == 1
22
+
23
+ values.first
24
+ end
25
+
26
+ def parse_records(io_or_string, seq: false, chunk_size: InputBuffer::DEFAULT_CHUNK_SIZE, on_error: nil,
27
+ max_depth: DEFAULT_MAX_DEPTH, max_number_digits: nil, max_string_bytes: nil)
28
+ new(io_or_string, seq: seq, chunk_size: chunk_size, on_error: on_error,
29
+ max_depth: max_depth, max_number_digits: max_number_digits,
30
+ max_string_bytes: max_string_bytes).parse_stream(locations: true)
31
+ end
32
+ end
33
+
34
+ def initialize(input, seq: false, chunk_size: InputBuffer::DEFAULT_CHUNK_SIZE, on_error: nil,
35
+ max_depth: DEFAULT_MAX_DEPTH, max_number_digits: nil, max_string_bytes: nil)
36
+ validate_options!(chunk_size, max_depth, max_number_digits, max_string_bytes)
37
+ @input = InputBuffer.new(input, chunk_size: chunk_size)
38
+ @seq = seq
39
+ @on_error = on_error
40
+ @max_depth = max_depth
41
+ @max_number_digits = max_number_digits
42
+ @max_string_bytes = max_string_bytes
43
+ @depth = 0
44
+ @index = 0
45
+ @line = 1
46
+ @column = 1
47
+ advance if current == "\uFEFF"
48
+ end
49
+
50
+ def parse_stream(locations: false)
51
+ Enumerator.new do |yielder|
52
+ loop do
53
+ skip_separators
54
+ break if eof?
55
+
56
+ begin
57
+ line = @line
58
+ value = parse_value
59
+ yielder << (locations ? ParsedValue.new(value: value, line: line) : value)
60
+ @input.discard_before(@index)
61
+ skip_whitespace
62
+ raise_error('expected record separator') if @seq && !(eof? || current == "\x1e")
63
+ rescue JSONParseError => e
64
+ raise unless @seq && @on_error
65
+
66
+ @on_error.call(e.message)
67
+ resync_to_record_separator
68
+ end
69
+ end
70
+ end
71
+ end
72
+
73
+ private
74
+
75
+ def parse_value
76
+ skip_whitespace
77
+ raise_error('unexpected end of input') if eof?
78
+
79
+ case current
80
+ when '{'
81
+ parse_object
82
+ when '['
83
+ parse_array
84
+ when '"'
85
+ parse_string
86
+ when 't'
87
+ consume_literal('true', true)
88
+ when 'f'
89
+ consume_literal('false', false)
90
+ when 'n'
91
+ if @input[@index,
92
+ 3].to_s.casecmp('nan').zero?
93
+ parse_special_number(positive: true)
94
+ else
95
+ consume_literal('null', nil)
96
+ end
97
+ when 'I', 'N'
98
+ parse_special_number(positive: true)
99
+ when '-'
100
+ if %w[I N].include?(@input[@index + 1, 1])
101
+ advance
102
+ parse_special_number(positive: false)
103
+ else
104
+ parse_number
105
+ end
106
+ else
107
+ parse_number
108
+ end
109
+ end
110
+
111
+ def parse_object
112
+ with_container_depth { parse_object_body }
113
+ end
114
+
115
+ def parse_object_body
116
+ advance
117
+ object = {}
118
+ skip_whitespace
119
+ return object if consume?('}')
120
+
121
+ loop do
122
+ skip_whitespace
123
+ raise_error('expected object key string') unless current == '"'
124
+
125
+ key = parse_string
126
+ skip_whitespace
127
+ expect(':')
128
+ object[key] = parse_value
129
+ skip_whitespace
130
+ return object if consume?('}')
131
+
132
+ expect(',')
133
+ end
134
+ end
135
+
136
+ def parse_array
137
+ with_container_depth { parse_array_body }
138
+ end
139
+
140
+ def parse_array_body
141
+ advance
142
+ array = []
143
+ skip_whitespace
144
+ return array if consume?(']')
145
+
146
+ loop do
147
+ array << parse_value
148
+ skip_whitespace
149
+ return array if consume?(']')
150
+
151
+ expect(',')
152
+ end
153
+ end
154
+
155
+ def parse_string
156
+ expect('"')
157
+ out = +''
158
+ bytes = 0
159
+ until eof?
160
+ line = @line
161
+ column = @column
162
+ char = advance
163
+ return out.force_encoding(Encoding::UTF_8) if char == '"'
164
+
165
+ piece = if char == '\\'
166
+ parse_escape
167
+ else
168
+ raise_error('unescaped control character in string') if char.ord < 0x20
169
+
170
+ char
171
+ end
172
+ bytes += piece.bytesize
173
+ raise_error_at("string exceeds #{@max_string_bytes} byte limit", line, column) if @max_string_bytes &&
174
+ bytes > @max_string_bytes
175
+
176
+ out << piece
177
+ end
178
+ raise_error('unterminated string')
179
+ end
180
+
181
+ def parse_escape
182
+ raise_error('unterminated escape') if eof?
183
+
184
+ char = advance
185
+ case char
186
+ when '"', '\\', '/'
187
+ char
188
+ when 'b'
189
+ "\b"
190
+ when 'f'
191
+ "\f"
192
+ when 'n'
193
+ "\n"
194
+ when 'r'
195
+ "\r"
196
+ when 't'
197
+ "\t"
198
+ when 'u'
199
+ parse_unicode_escape
200
+ else
201
+ raise_error("invalid escape: \\#{char}")
202
+ end
203
+ end
204
+
205
+ def parse_unicode_escape
206
+ codepoint = read_hex4
207
+ if high_surrogate?(codepoint)
208
+ raise_error('missing low surrogate') unless @input[@index, 2] == '\\u'
209
+
210
+ 2.times { advance }
211
+ low = read_hex4
212
+ raise_error('invalid low surrogate') unless low_surrogate?(low)
213
+
214
+ codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00)
215
+ elsif low_surrogate?(codepoint)
216
+ raise_error('unexpected low surrogate')
217
+ end
218
+ [codepoint].pack('U')
219
+ end
220
+
221
+ def parse_number
222
+ start = @index
223
+ digits = 0
224
+ consume?('-')
225
+ if current == '0'
226
+ digits = consume_number_digit(digits)
227
+ raise_error('leading zero in number') if digit?(current)
228
+ else
229
+ raise_error('expected number') unless digit_1_9?(current)
230
+ digits = consume_number_digit(digits) while digit?(current)
231
+ end
232
+
233
+ if consume?('.')
234
+ raise_error('expected digit after decimal point') unless digit?(current)
235
+ digits = consume_number_digit(digits) while digit?(current)
236
+ end
237
+
238
+ if %w[e E].include?(current)
239
+ advance
240
+ advance if ['+', '-'].include?(current)
241
+ raise_error('expected digit in exponent') unless digit?(current)
242
+ digits = consume_number_digit(digits) while digit?(current)
243
+ end
244
+
245
+ literal = @input[start...@index]
246
+ raise_error('invalid number') unless number_delimiter?(current)
247
+
248
+ Number.parse(literal)
249
+ rescue ArgumentError
250
+ raise_error('invalid number')
251
+ end
252
+
253
+ def number_delimiter?(char)
254
+ char.nil? || char.match?(/[\s,\]}\x1e]/)
255
+ end
256
+
257
+ def parse_special_number(positive:)
258
+ if @input[@index, 8] == 'Infinity'
259
+ 8.times { advance }
260
+ raise_error('invalid number') if atom_char?(current)
261
+
262
+ return positive ? Float::INFINITY : -Float::INFINITY
263
+ end
264
+
265
+ raise_error('expected number') unless @input[@index, 3].to_s.casecmp('nan').zero?
266
+
267
+ 3.times { advance }
268
+ digits = 0
269
+ digits = consume_number_digit(digits) while digit?(current)
270
+ raise_error('invalid number') if atom_char?(current)
271
+
272
+ Float::NAN
273
+ end
274
+
275
+ def consume_literal(literal, value)
276
+ raise_error("expected #{literal}") unless @input[@index, literal.length] == literal
277
+
278
+ literal.length.times { advance }
279
+ raise_error("invalid literal #{literal}") if atom_char?(current)
280
+
281
+ value
282
+ end
283
+
284
+ def atom_char?(char)
285
+ char&.match?(/[0-9A-Za-z_]/)
286
+ end
287
+
288
+ def read_hex4
289
+ chars = @input[@index, 4]
290
+ raise_error('invalid unicode escape') unless chars&.match?(/\A[0-9a-fA-F]{4}\z/)
291
+
292
+ 4.times { advance }
293
+ chars.to_i(16)
294
+ end
295
+
296
+ def high_surrogate?(codepoint)
297
+ codepoint.between?(0xD800, 0xDBFF)
298
+ end
299
+
300
+ def low_surrogate?(codepoint)
301
+ codepoint.between?(0xDC00, 0xDFFF)
302
+ end
303
+
304
+ def skip_separators
305
+ loop do
306
+ skip_whitespace
307
+ break unless @seq && current == "\x1e"
308
+
309
+ advance
310
+ end
311
+ end
312
+
313
+ def resync_to_record_separator
314
+ advance until eof? || current == "\x1e"
315
+ @input.discard_before(@index)
316
+ end
317
+
318
+ def with_container_depth
319
+ @depth += 1
320
+ raise_error('exceeds depth limit for parsing') if @depth > @max_depth
321
+
322
+ yield
323
+ ensure
324
+ @depth -= 1
325
+ end
326
+
327
+ def consume_number_digit(count)
328
+ count += 1
329
+ if @max_number_digits && count > @max_number_digits
330
+ raise_error("number exceeds #{@max_number_digits} digit limit")
331
+ end
332
+
333
+ advance
334
+ count
335
+ end
336
+
337
+ def validate_options!(chunk_size, max_depth, max_number_digits, max_string_bytes)
338
+ validate_nonnegative_integer!(:max_depth, max_depth)
339
+ validate_nonnegative_integer!(:max_number_digits, max_number_digits, optional: true)
340
+ validate_nonnegative_integer!(:max_string_bytes, max_string_bytes, optional: true)
341
+ return if chunk_size.is_a?(Integer) && chunk_size.positive?
342
+
343
+ raise ArgumentError, 'chunk_size must be a positive Integer'
344
+ end
345
+
346
+ def validate_nonnegative_integer!(name, value, optional: false)
347
+ return if optional && value.nil?
348
+ return if value.is_a?(Integer) && value >= 0
349
+
350
+ raise ArgumentError, "#{name} must be a non-negative Integer#{' or nil' if optional}"
351
+ end
352
+
353
+ def skip_whitespace
354
+ advance while current&.match?(/[ \t\r\n]/)
355
+ end
356
+
357
+ def expect(char)
358
+ raise_error("expected #{char}") unless consume?(char)
359
+ end
360
+
361
+ def consume?(char)
362
+ return false unless current == char
363
+
364
+ advance
365
+ true
366
+ end
367
+
368
+ def advance
369
+ char = current
370
+ @index += char.length
371
+ if char == "\n"
372
+ @line += 1
373
+ @column = 1
374
+ else
375
+ @column += 1
376
+ end
377
+ char
378
+ end
379
+
380
+ def current
381
+ @input[@index]
382
+ end
383
+
384
+ def eof?
385
+ current.nil?
386
+ end
387
+
388
+ def digit?(char)
389
+ !char.nil? && char >= '0' && char <= '9'
390
+ end
391
+
392
+ def digit_1_9?(char)
393
+ !char.nil? && char >= '1' && char <= '9'
394
+ end
395
+
396
+ def raise_error(message)
397
+ raise JSONParseError, "#{message} at line #{@line}, column #{@column}"
398
+ end
399
+
400
+ def raise_error_at(message, line, column)
401
+ raise JSONParseError, "#{message} at line #{line}, column #{column}"
402
+ end
403
+ end
404
+ end
405
+ end