json5-ruby 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.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/NOTICE +6 -0
- data/README.md +161 -0
- data/SECURITY.md +9 -0
- data/docs/conformance-matrix.md +90 -0
- data/docs/conformance-report.md +29 -0
- data/grammar/json5.y +74 -0
- data/lib/json5/ascii_classification.rb +31 -0
- data/lib/json5/diagnostic.rb +70 -0
- data/lib/json5/document.rb +20 -0
- data/lib/json5/document_nodes.rb +99 -0
- data/lib/json5/document_parser.rb +291 -0
- data/lib/json5/ecma_string.rb +106 -0
- data/lib/json5/errors.rb +67 -0
- data/lib/json5/generated_parser.ibex.json +196 -0
- data/lib/json5/generated_parser.rb +10066 -0
- data/lib/json5/input.rb +283 -0
- data/lib/json5/lexer.rb +599 -0
- data/lib/json5/limits.rb +47 -0
- data/lib/json5/line_map.rb +92 -0
- data/lib/json5/number_value.rb +119 -0
- data/lib/json5/object_value.rb +28 -0
- data/lib/json5/parser.rb +301 -0
- data/lib/json5/public_token_names.rb +43 -0
- data/lib/json5/ruby_value_builder.rb +236 -0
- data/lib/json5/token.rb +26 -0
- data/lib/json5/unicode_tables.rb +42 -0
- data/lib/json5/version.rb +7 -0
- data/lib/json5.rb +47 -0
- data/tool/sources.yml +21 -0
- data/tool/unicode/README.md +19 -0
- data/tool/unicode/UNICODE-LICENSE.txt +39 -0
- data/tool/unicode/checksums.txt +1 -0
- metadata +91 -0
data/lib/json5/lexer.rb
ADDED
|
@@ -0,0 +1,599 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class Lexer
|
|
5
|
+
SIMPLE_ESCAPES = {
|
|
6
|
+
0x27 => 0x27,
|
|
7
|
+
0x22 => 0x22,
|
|
8
|
+
0x5c => 0x5c,
|
|
9
|
+
0x62 => 0x08,
|
|
10
|
+
0x66 => 0x0c,
|
|
11
|
+
0x6e => 0x0a,
|
|
12
|
+
0x72 => 0x0d,
|
|
13
|
+
0x74 => 0x09,
|
|
14
|
+
0x76 => 0x0b
|
|
15
|
+
}.freeze
|
|
16
|
+
KEYWORDS = {
|
|
17
|
+
"true" => :TRUE,
|
|
18
|
+
"false" => :FALSE,
|
|
19
|
+
"null" => :NULL,
|
|
20
|
+
"Infinity" => :INFINITY,
|
|
21
|
+
"NaN" => :NAN
|
|
22
|
+
}.freeze
|
|
23
|
+
PUNCTUATORS = {
|
|
24
|
+
0x7b => "{",
|
|
25
|
+
0x7d => "}",
|
|
26
|
+
0x5b => "[",
|
|
27
|
+
0x5d => "]",
|
|
28
|
+
0x3a => ":",
|
|
29
|
+
0x2c => ","
|
|
30
|
+
}.freeze
|
|
31
|
+
ASCII = ASCIIClassification::TABLE
|
|
32
|
+
ASCII_WHITESPACE = ASCIIClassification::WHITESPACE
|
|
33
|
+
ASCII_DIGIT = ASCIIClassification::DIGIT
|
|
34
|
+
ASCII_HEX_DIGIT = ASCIIClassification::HEX_DIGIT
|
|
35
|
+
ASCII_IDENTIFIER_START = ASCIIClassification::IDENTIFIER_START
|
|
36
|
+
ASCII_IDENTIFIER_PART = ASCIIClassification::IDENTIFIER_PART
|
|
37
|
+
ASCII_QUOTE = ASCIIClassification::QUOTE
|
|
38
|
+
ASCII_PUNCTUATOR = ASCIIClassification::PUNCTUATOR
|
|
39
|
+
ASCII_SLASH = ASCIIClassification::SLASH
|
|
40
|
+
ASCII_BACKSLASH = ASCIIClassification::BACKSLASH
|
|
41
|
+
ASCII_LINE_TERMINATOR = ASCIIClassification::LINE_TERMINATOR
|
|
42
|
+
ASCII_STRING_STOP = ASCII_BACKSLASH | ASCII_LINE_TERMINATOR
|
|
43
|
+
EMPTY_TRIVIA = [].freeze
|
|
44
|
+
|
|
45
|
+
attr_reader :input, :last_token, :nesting_depth,
|
|
46
|
+
:last_token_container, :last_token_nesting_depth
|
|
47
|
+
|
|
48
|
+
def initialize(input, limits: Limits.default, diagnostics: nil, filename: nil, preserve_trivia: false)
|
|
49
|
+
@input = input
|
|
50
|
+
@limits = limits
|
|
51
|
+
@diagnostics = DiagnosticSink.build(diagnostics, filename: filename)
|
|
52
|
+
@filename = filename
|
|
53
|
+
@preserve_trivia = preserve_trivia
|
|
54
|
+
@leading_trivia = []
|
|
55
|
+
@container_stack = []
|
|
56
|
+
@nesting_depth = 0
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def next_token
|
|
60
|
+
skip_trivia
|
|
61
|
+
@last_token_container = current_container
|
|
62
|
+
@last_token_nesting_depth = @nesting_depth
|
|
63
|
+
start_byte = input.index
|
|
64
|
+
start_line = input.line
|
|
65
|
+
start_column = input.column
|
|
66
|
+
kind, value = scan_token
|
|
67
|
+
finish_token(kind, value, start_byte, start_line, start_column)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def scan_token
|
|
73
|
+
return [:EOF, nil] if input.eof?
|
|
74
|
+
byte = input.peek_byte
|
|
75
|
+
flags = ASCII.fetch(byte)
|
|
76
|
+
return [PUNCTUATORS.fetch(byte), consume_punctuation] unless (flags & ASCII_PUNCTUATOR).zero?
|
|
77
|
+
return [:STRING, scan_string] unless (flags & ASCII_QUOTE).zero?
|
|
78
|
+
return [:NUMBER, scan_number] if number_start?(byte)
|
|
79
|
+
return scan_identifier if identifier_start_byte?(byte) || !(flags & ASCII_BACKSLASH).zero?
|
|
80
|
+
|
|
81
|
+
raise_error("unexpected_character", "unexpected character", input.mark, LexError)
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def finish_token(kind, value, start_byte, start_line, start_column)
|
|
85
|
+
token = Token.new(
|
|
86
|
+
kind,
|
|
87
|
+
value,
|
|
88
|
+
start_byte,
|
|
89
|
+
input.index,
|
|
90
|
+
start_line,
|
|
91
|
+
start_column,
|
|
92
|
+
nil,
|
|
93
|
+
(@preserve_trivia ? @leading_trivia.freeze : EMPTY_TRIVIA),
|
|
94
|
+
source: input.source
|
|
95
|
+
)
|
|
96
|
+
@last_token = token
|
|
97
|
+
@leading_trivia = []
|
|
98
|
+
token
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def consume_punctuation
|
|
102
|
+
byte = input.peek_byte
|
|
103
|
+
if byte == 0x7b || byte == 0x5b
|
|
104
|
+
@container_stack << byte
|
|
105
|
+
@nesting_depth = @container_stack.length
|
|
106
|
+
if @limits.max_depth && @nesting_depth > @limits.max_depth
|
|
107
|
+
raise_error("maximum_depth_exceeded", "nested value exceeds max_depth", input.mark, DepthError)
|
|
108
|
+
end
|
|
109
|
+
elsif byte == 0x7d || byte == 0x5d
|
|
110
|
+
matching_open = byte == 0x7d ? 0x7b : 0x5b
|
|
111
|
+
@container_stack.pop if @container_stack.last == matching_open
|
|
112
|
+
@nesting_depth = @container_stack.length
|
|
113
|
+
end
|
|
114
|
+
input.consume_byte
|
|
115
|
+
nil
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
public
|
|
119
|
+
|
|
120
|
+
def current_container
|
|
121
|
+
case @container_stack.last
|
|
122
|
+
when 0x7b then "{"
|
|
123
|
+
when 0x5b then "["
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def skip_trivia
|
|
130
|
+
loop do
|
|
131
|
+
if whitespace?
|
|
132
|
+
start = input.mark
|
|
133
|
+
input.consume_codepoint
|
|
134
|
+
record_trivia(:whitespace, start)
|
|
135
|
+
next
|
|
136
|
+
end
|
|
137
|
+
unless (ascii_flags(input.peek_byte) & ASCII_SLASH).zero?
|
|
138
|
+
if input.peek_byte(1) == 0x2f
|
|
139
|
+
start = input.mark
|
|
140
|
+
scan_line_comment(start)
|
|
141
|
+
next
|
|
142
|
+
end
|
|
143
|
+
if input.peek_byte(1) == 0x2a
|
|
144
|
+
start = input.mark
|
|
145
|
+
scan_block_comment(start)
|
|
146
|
+
next
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
break
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def whitespace?
|
|
154
|
+
byte = input.peek_byte
|
|
155
|
+
return false unless byte
|
|
156
|
+
return !(ASCII.fetch(byte) & ASCII_WHITESPACE).zero? if byte < 0x80
|
|
157
|
+
|
|
158
|
+
codepoint, = input.peek_codepoint
|
|
159
|
+
codepoint == 0xa0 || codepoint == 0x2028 || codepoint == 0x2029 || codepoint == 0xfeff ||
|
|
160
|
+
UnicodeTables.space_separator?(codepoint)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def scan_line_comment(start)
|
|
164
|
+
input.consume_ascii(0x2f)
|
|
165
|
+
input.consume_ascii(0x2f)
|
|
166
|
+
enforce_comment_limit(start)
|
|
167
|
+
while !input.eof?
|
|
168
|
+
codepoint, = input.peek_codepoint
|
|
169
|
+
break if input.line_terminator?(codepoint)
|
|
170
|
+
input.consume_codepoint
|
|
171
|
+
enforce_comment_limit(start)
|
|
172
|
+
end
|
|
173
|
+
record_trivia(:line_comment, start)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def scan_block_comment(start)
|
|
177
|
+
input.consume_ascii(0x2f)
|
|
178
|
+
input.consume_ascii(0x2a)
|
|
179
|
+
enforce_comment_limit(start)
|
|
180
|
+
until input.eof?
|
|
181
|
+
if input.peek_byte == 0x2a && input.peek_byte(1) == 0x2f
|
|
182
|
+
input.consume_byte
|
|
183
|
+
input.consume_byte
|
|
184
|
+
enforce_comment_limit(start)
|
|
185
|
+
record_trivia(:block_comment, start)
|
|
186
|
+
return
|
|
187
|
+
end
|
|
188
|
+
input.consume_codepoint
|
|
189
|
+
enforce_comment_limit(start)
|
|
190
|
+
end
|
|
191
|
+
raise_error("unterminated_comment", "unterminated block comment", start, LexError)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def enforce_comment_limit(start)
|
|
195
|
+
return unless @limits.max_comment_bytes && input.index - start.byte_offset > @limits.max_comment_bytes
|
|
196
|
+
raise_error("maximum_comment_size_exceeded", "comment exceeds max_comment_bytes", start, LimitError)
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def record_trivia(kind, start)
|
|
200
|
+
return unless @preserve_trivia
|
|
201
|
+
raw = input.byteslice(start.byte_offset).freeze
|
|
202
|
+
@leading_trivia << Trivia.new(kind, start.byte_offset, input.index, raw)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def scan_identifier
|
|
206
|
+
start = input.mark
|
|
207
|
+
units = []
|
|
208
|
+
escaped = consume_identifier_character(units, first: true)
|
|
209
|
+
while identifier_continuation?
|
|
210
|
+
escaped = consume_identifier_character(units, first: false) || escaped
|
|
211
|
+
end
|
|
212
|
+
name = ECMAString.new(units)
|
|
213
|
+
kind = keyword_kind(start.byte_offset, input.index) unless escaped
|
|
214
|
+
if kind
|
|
215
|
+
enforce_number_limit(start) if %i[INFINITY NAN].include?(kind)
|
|
216
|
+
value = if %i[INFINITY NAN].include?(kind)
|
|
217
|
+
NumberValue.new(input.byteslice(start.byte_offset))
|
|
218
|
+
else
|
|
219
|
+
name
|
|
220
|
+
end
|
|
221
|
+
return [kind, value]
|
|
222
|
+
end
|
|
223
|
+
[:IDENTIFIER_NAME, name]
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def consume_identifier_character(units, first:)
|
|
227
|
+
start = input.mark
|
|
228
|
+
if input.peek_byte == 0x5c
|
|
229
|
+
unit = consume_identifier_escape
|
|
230
|
+
valid = first ? UnicodeTables.identifier_start?(unit) : UnicodeTables.identifier_part?(unit)
|
|
231
|
+
raise_error("invalid_identifier_character", "escaped character is not valid in IdentifierName", start, LexError) unless valid
|
|
232
|
+
units << unit
|
|
233
|
+
return true
|
|
234
|
+
end
|
|
235
|
+
codepoint, = input.peek_codepoint
|
|
236
|
+
input.consume_codepoint
|
|
237
|
+
if codepoint > 0xffff
|
|
238
|
+
raise_error("invalid_identifier_character", "non-BMP identifier characters are not supported", start, LexError)
|
|
239
|
+
end
|
|
240
|
+
valid = first ? identifier_start_codepoint?(codepoint) : identifier_part_codepoint?(codepoint)
|
|
241
|
+
raise_error("invalid_identifier_character", "character is not valid in IdentifierName", start, LexError) unless valid
|
|
242
|
+
units << codepoint
|
|
243
|
+
false
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def keyword_kind(start_byte, end_byte)
|
|
247
|
+
length = end_byte - start_byte
|
|
248
|
+
KEYWORDS.each do |word, kind|
|
|
249
|
+
next unless word.bytesize == length
|
|
250
|
+
return kind if word.bytes.each_with_index.all? { |byte, index| input.source.getbyte(start_byte + index) == byte }
|
|
251
|
+
end
|
|
252
|
+
nil
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def consume_identifier_escape
|
|
256
|
+
start = input.mark
|
|
257
|
+
input.consume_ascii(0x5c)
|
|
258
|
+
unless input.peek_byte == 0x75
|
|
259
|
+
raise_error("invalid_identifier_escape", "IdentifierName escape must use \\uHHHH", start, LexError)
|
|
260
|
+
end
|
|
261
|
+
input.consume_byte
|
|
262
|
+
value = 0
|
|
263
|
+
4.times do
|
|
264
|
+
byte = input.peek_byte
|
|
265
|
+
unless hex_digit_byte?(byte)
|
|
266
|
+
raise_error("invalid_identifier_escape", "IdentifierName escape requires four hex digits", start, LexError)
|
|
267
|
+
end
|
|
268
|
+
input.consume_byte
|
|
269
|
+
value = (value << 4) | hex_digit_value(byte)
|
|
270
|
+
end
|
|
271
|
+
value
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def identifier_continuation?
|
|
275
|
+
return false if input.eof?
|
|
276
|
+
return true if input.peek_byte == 0x5c
|
|
277
|
+
byte = input.peek_byte
|
|
278
|
+
return !(ASCII.fetch(byte) & ASCII_IDENTIFIER_PART).zero? if byte < 0x80
|
|
279
|
+
|
|
280
|
+
codepoint, = input.peek_codepoint
|
|
281
|
+
identifier_part_codepoint?(codepoint)
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def identifier_start_byte?(byte)
|
|
285
|
+
byte >= 0x80 || !(ASCII.fetch(byte) & ASCII_IDENTIFIER_START).zero?
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def identifier_start_codepoint?(codepoint)
|
|
289
|
+
return false unless codepoint
|
|
290
|
+
return !(ASCII.fetch(codepoint) & ASCII_IDENTIFIER_START).zero? if codepoint < 0x80
|
|
291
|
+
|
|
292
|
+
UnicodeTables.identifier_start?(codepoint)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def identifier_part_codepoint?(codepoint)
|
|
296
|
+
return false unless codepoint
|
|
297
|
+
return !(ASCII.fetch(codepoint) & ASCII_IDENTIFIER_PART).zero? if codepoint < 0x80
|
|
298
|
+
|
|
299
|
+
UnicodeTables.identifier_part?(codepoint)
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def scan_string
|
|
303
|
+
quote = input.consume_byte
|
|
304
|
+
content_start = input.index
|
|
305
|
+
stop = ascii_string_stop(content_start, quote)
|
|
306
|
+
units = consume_ascii_string_prefix(content_start, stop)
|
|
307
|
+
|
|
308
|
+
if input.peek_byte == quote
|
|
309
|
+
input.consume_byte
|
|
310
|
+
return ECMAString.new(units)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
loop do
|
|
314
|
+
raise_error("unterminated_string", "unterminated string", input.mark, LexError) if input.eof?
|
|
315
|
+
codepoint, = input.peek_codepoint
|
|
316
|
+
if codepoint == quote
|
|
317
|
+
input.consume_byte
|
|
318
|
+
return ECMAString.new(units)
|
|
319
|
+
end
|
|
320
|
+
if codepoint == 0x5c
|
|
321
|
+
scan_escape(units)
|
|
322
|
+
next
|
|
323
|
+
end
|
|
324
|
+
if codepoint == 0x0a || codepoint == 0x0d
|
|
325
|
+
raise_error("unterminated_string", "line terminator in string", input.mark, LexError)
|
|
326
|
+
end
|
|
327
|
+
if codepoint == 0x2028 || codepoint == 0x2029
|
|
328
|
+
position = input.mark
|
|
329
|
+
@diagnostics.emit(
|
|
330
|
+
code: codepoint == 0x2028 ? "unescaped_line_separator_in_string" : "unescaped_paragraph_separator_in_string",
|
|
331
|
+
message: "unescaped Unicode line separator in string",
|
|
332
|
+
byte_offset: input.index,
|
|
333
|
+
end_byte_offset: input.index + 3,
|
|
334
|
+
line: position.line,
|
|
335
|
+
column: position.column
|
|
336
|
+
)
|
|
337
|
+
end
|
|
338
|
+
input.consume_codepoint
|
|
339
|
+
append_codepoint(units, codepoint)
|
|
340
|
+
enforce_string_limit(units)
|
|
341
|
+
end
|
|
342
|
+
end
|
|
343
|
+
|
|
344
|
+
def ascii_string_stop(start, quote)
|
|
345
|
+
index = start
|
|
346
|
+
while (byte = input.source.getbyte(index))
|
|
347
|
+
return index if byte >= 0x80 || byte == quote || !(ASCII.fetch(byte) & ASCII_STRING_STOP).zero?
|
|
348
|
+
index += 1
|
|
349
|
+
end
|
|
350
|
+
index
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
def consume_ascii_string_prefix(start, finish)
|
|
354
|
+
length = finish - start
|
|
355
|
+
limit = @limits.max_string_code_units
|
|
356
|
+
if limit && length > limit
|
|
357
|
+
input.consume_ascii_bytes(limit + 1)
|
|
358
|
+
raise_string_limit
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
input.consume_ascii_bytes(length)
|
|
362
|
+
input.source.byteslice(start, length).bytes
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
def scan_escape(units)
|
|
366
|
+
start = input.mark
|
|
367
|
+
input.consume_byte
|
|
368
|
+
raise_error("invalid_escape", "backslash at end of string", start, LexError) if input.eof?
|
|
369
|
+
codepoint, = input.peek_codepoint
|
|
370
|
+
if input.line_terminator?(codepoint)
|
|
371
|
+
input.consume_codepoint
|
|
372
|
+
return
|
|
373
|
+
end
|
|
374
|
+
if SIMPLE_ESCAPES.key?(codepoint)
|
|
375
|
+
input.consume_byte
|
|
376
|
+
units << SIMPLE_ESCAPES.fetch(codepoint)
|
|
377
|
+
elsif codepoint == 0x30
|
|
378
|
+
input.consume_byte
|
|
379
|
+
if decimal_digit_byte?(input.peek_byte)
|
|
380
|
+
raise_error("invalid_escape", "\\0 cannot be followed by a decimal digit", start, LexError)
|
|
381
|
+
end
|
|
382
|
+
units << 0
|
|
383
|
+
elsif codepoint.between?(0x31, 0x39)
|
|
384
|
+
raise_error("invalid_escape", "decimal and octal escapes are not allowed", start, LexError)
|
|
385
|
+
elsif codepoint == 0x78
|
|
386
|
+
units << read_hex_escape(2, start, "x")
|
|
387
|
+
elsif codepoint == 0x75
|
|
388
|
+
units << read_hex_escape(4, start, "u")
|
|
389
|
+
else
|
|
390
|
+
input.consume_codepoint
|
|
391
|
+
append_codepoint(units, codepoint)
|
|
392
|
+
end
|
|
393
|
+
enforce_string_limit(units)
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
def read_hex_escape(length, start, marker)
|
|
397
|
+
input.consume_byte
|
|
398
|
+
value = 0
|
|
399
|
+
length.times do
|
|
400
|
+
byte = input.peek_byte
|
|
401
|
+
unless hex_digit_byte?(byte)
|
|
402
|
+
raise_error("invalid_escape", "\\#{marker} escape requires #{length} hex digits", start, LexError)
|
|
403
|
+
end
|
|
404
|
+
input.consume_byte
|
|
405
|
+
value = (value << 4) | hex_digit_value(byte)
|
|
406
|
+
end
|
|
407
|
+
value
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def append_codepoint(units, codepoint)
|
|
411
|
+
if codepoint <= 0xffff
|
|
412
|
+
units << codepoint
|
|
413
|
+
else
|
|
414
|
+
codepoint -= 0x10000
|
|
415
|
+
units << (0xd800 + (codepoint >> 10))
|
|
416
|
+
units << (0xdc00 + (codepoint & 0x3ff))
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def enforce_string_limit(units)
|
|
421
|
+
return unless @limits.max_string_code_units && units.length > @limits.max_string_code_units
|
|
422
|
+
raise_string_limit
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def raise_string_limit
|
|
426
|
+
raise_error("maximum_string_size_exceeded", "string exceeds max_string_code_units", input.mark, LimitError)
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def scan_number
|
|
430
|
+
start = input.mark
|
|
431
|
+
if [0x2b, 0x2d].include?(input.peek_byte)
|
|
432
|
+
consume_number_byte(start)
|
|
433
|
+
raw = scan_signed_number_body(start)
|
|
434
|
+
return raw
|
|
435
|
+
end
|
|
436
|
+
if input.peek_byte == 0x2e
|
|
437
|
+
consume_number_byte(start)
|
|
438
|
+
require_decimal_digits(start)
|
|
439
|
+
consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
|
|
440
|
+
scan_decimal_tail(start)
|
|
441
|
+
check_number_boundary(start)
|
|
442
|
+
enforce_number_limit(start)
|
|
443
|
+
return input.byteslice(start.byte_offset)
|
|
444
|
+
end
|
|
445
|
+
scan_unsigned_number(start)
|
|
446
|
+
end
|
|
447
|
+
|
|
448
|
+
def scan_signed_number_body(start)
|
|
449
|
+
if match_ascii_word?("Infinity") || match_ascii_word?("NaN")
|
|
450
|
+
consume_number_bytes(input.peek_byte == 0x49 ? 8 : 3, start)
|
|
451
|
+
check_number_boundary(start)
|
|
452
|
+
enforce_number_limit(start)
|
|
453
|
+
return input.byteslice(start.byte_offset)
|
|
454
|
+
end
|
|
455
|
+
if input.peek_byte == 0x30 && [0x78, 0x58].include?(input.peek_byte(1))
|
|
456
|
+
scan_hex_body(start)
|
|
457
|
+
elsif input.peek_byte == 0x2e
|
|
458
|
+
consume_number_byte(start)
|
|
459
|
+
require_decimal_digits(start)
|
|
460
|
+
consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
|
|
461
|
+
scan_decimal_tail(start)
|
|
462
|
+
check_number_boundary(start)
|
|
463
|
+
enforce_number_limit(start)
|
|
464
|
+
input.byteslice(start.byte_offset)
|
|
465
|
+
elsif input.peek_byte == 0x30
|
|
466
|
+
consume_number_byte(start)
|
|
467
|
+
raise_error("leading_zero", "leading zero is not allowed", start, LexError) if decimal_digit_byte?(input.peek_byte)
|
|
468
|
+
scan_decimal_tail(start)
|
|
469
|
+
raw = input.byteslice(start.byte_offset)
|
|
470
|
+
check_number_boundary(start)
|
|
471
|
+
enforce_number_limit(start)
|
|
472
|
+
raw
|
|
473
|
+
elsif decimal_digit_byte?(input.peek_byte)
|
|
474
|
+
scan_decimal_body(start)
|
|
475
|
+
else
|
|
476
|
+
raise_error("invalid_number", "sign must be followed by a JSON5 number", start, LexError)
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def scan_unsigned_number(start)
|
|
481
|
+
if input.peek_byte == 0x30 && [0x78, 0x58].include?(input.peek_byte(1))
|
|
482
|
+
scan_hex_body(start)
|
|
483
|
+
elsif input.peek_byte == 0x30
|
|
484
|
+
consume_number_byte(start)
|
|
485
|
+
raise_error("leading_zero", "leading zero is not allowed", start, LexError) if decimal_digit_byte?(input.peek_byte)
|
|
486
|
+
scan_decimal_tail(start)
|
|
487
|
+
raw = input.byteslice(start.byte_offset)
|
|
488
|
+
check_number_boundary(start)
|
|
489
|
+
enforce_number_limit(start)
|
|
490
|
+
raw
|
|
491
|
+
elsif input.peek_byte&.between?(0x31, 0x39)
|
|
492
|
+
scan_decimal_body(start)
|
|
493
|
+
else
|
|
494
|
+
raise_error("invalid_number", "invalid number", start, LexError)
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def scan_decimal_body(start)
|
|
499
|
+
consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
|
|
500
|
+
scan_decimal_tail(start)
|
|
501
|
+
raw = input.byteslice(start.byte_offset)
|
|
502
|
+
check_number_boundary(start)
|
|
503
|
+
enforce_number_limit(start)
|
|
504
|
+
raw
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def scan_decimal_tail(start)
|
|
508
|
+
if input.peek_byte == 0x2e
|
|
509
|
+
consume_number_byte(start)
|
|
510
|
+
consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
|
|
511
|
+
end
|
|
512
|
+
if [0x65, 0x45].include?(input.peek_byte)
|
|
513
|
+
consume_number_byte(start)
|
|
514
|
+
consume_number_byte(start) if [0x2b, 0x2d].include?(input.peek_byte)
|
|
515
|
+
require_decimal_digits(input.mark)
|
|
516
|
+
consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
|
|
517
|
+
end
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def scan_hex_body(start)
|
|
521
|
+
consume_number_byte(start)
|
|
522
|
+
consume_number_byte(start)
|
|
523
|
+
unless hex_digit_byte?(input.peek_byte)
|
|
524
|
+
raise_error("invalid_number", "hexadecimal number requires at least one digit", start, LexError)
|
|
525
|
+
end
|
|
526
|
+
consume_number_byte(start) while hex_digit_byte?(input.peek_byte)
|
|
527
|
+
raw = input.byteslice(start.byte_offset)
|
|
528
|
+
check_number_boundary(start)
|
|
529
|
+
enforce_number_limit(start)
|
|
530
|
+
raw
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
def require_decimal_digits(start)
|
|
534
|
+
raise_error("invalid_number", "number requires a decimal digit", start, LexError) unless decimal_digit_byte?(input.peek_byte)
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def check_number_boundary(start)
|
|
538
|
+
return if input.eof?
|
|
539
|
+
codepoint, = input.peek_codepoint
|
|
540
|
+
return unless identifier_start_codepoint?(codepoint) || decimal_digit_byte?(codepoint) || input.peek_byte == 0x5c
|
|
541
|
+
raise_error("invalid_number", "number is followed by an identifier character", start, LexError)
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def enforce_number_limit(start)
|
|
545
|
+
return unless @limits.max_number_bytes && input.index - start.byte_offset > @limits.max_number_bytes
|
|
546
|
+
raise_error("maximum_number_size_exceeded", "number exceeds max_number_bytes", start, LimitError)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
def consume_number_byte(start)
|
|
550
|
+
input.consume_byte
|
|
551
|
+
enforce_number_limit(start)
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def consume_number_bytes(count, start)
|
|
555
|
+
count.times { consume_number_byte(start) }
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def match_ascii_word?(word)
|
|
559
|
+
input.source.byteslice(input.index, word.bytesize) == word
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def number_start?(byte)
|
|
563
|
+
byte == 0x2b || byte == 0x2d || byte == 0x2e || decimal_digit_byte?(byte)
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
def decimal_digit_byte?(byte)
|
|
567
|
+
!(ascii_flags(byte) & ASCII_DIGIT).zero?
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def hex_digit_byte?(byte)
|
|
571
|
+
!(ascii_flags(byte) & ASCII_HEX_DIGIT).zero?
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
def hex_digit_value(byte)
|
|
575
|
+
return byte - 0x30 if byte <= 0x39
|
|
576
|
+
return byte - 0x37 if byte <= 0x46
|
|
577
|
+
|
|
578
|
+
byte - 0x57
|
|
579
|
+
end
|
|
580
|
+
|
|
581
|
+
def ascii_flags(byte)
|
|
582
|
+
byte && byte < ASCII.length ? ASCII.fetch(byte) : 0
|
|
583
|
+
end
|
|
584
|
+
|
|
585
|
+
def raise_error(code, message, position, klass)
|
|
586
|
+
offset = position.byte_offset
|
|
587
|
+
raise klass.new(
|
|
588
|
+
message,
|
|
589
|
+
code: code,
|
|
590
|
+
filename: @filename,
|
|
591
|
+
byte_offset: offset,
|
|
592
|
+
end_byte_offset: [[input.index, offset + 1].max, input.source.bytesize].min,
|
|
593
|
+
line: position.line,
|
|
594
|
+
column: position.column,
|
|
595
|
+
excerpt: input.excerpt_at(offset)
|
|
596
|
+
)
|
|
597
|
+
end
|
|
598
|
+
end
|
|
599
|
+
end
|
data/lib/json5/limits.rb
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class Limits
|
|
5
|
+
ATTRIBUTES = %i[
|
|
6
|
+
max_input_bytes
|
|
7
|
+
max_depth
|
|
8
|
+
max_string_code_units
|
|
9
|
+
max_number_bytes
|
|
10
|
+
max_comment_bytes
|
|
11
|
+
max_container_entries
|
|
12
|
+
].freeze
|
|
13
|
+
|
|
14
|
+
attr_reader(*ATTRIBUTES)
|
|
15
|
+
|
|
16
|
+
def initialize(max_input_bytes: 64 * 1024 * 1024, max_depth: 512,
|
|
17
|
+
max_string_code_units: 16 * 1024 * 1024,
|
|
18
|
+
max_number_bytes: 1 * 1024 * 1024,
|
|
19
|
+
max_comment_bytes: 16 * 1024 * 1024,
|
|
20
|
+
max_container_entries: 10_000_000)
|
|
21
|
+
values = {
|
|
22
|
+
max_input_bytes: max_input_bytes,
|
|
23
|
+
max_depth: max_depth,
|
|
24
|
+
max_string_code_units: max_string_code_units,
|
|
25
|
+
max_number_bytes: max_number_bytes,
|
|
26
|
+
max_comment_bytes: max_comment_bytes,
|
|
27
|
+
max_container_entries: max_container_entries
|
|
28
|
+
}
|
|
29
|
+
values.each do |name, value|
|
|
30
|
+
next if value.nil?
|
|
31
|
+
unless value.is_a?(Integer) && value >= 0
|
|
32
|
+
raise ArgumentError, "#{name} must be non-negative"
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
values.each { |name, value| instance_variable_set("@#{name}", value) }
|
|
36
|
+
freeze
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.default
|
|
40
|
+
@default ||= new
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.unbounded
|
|
44
|
+
@unbounded ||= new(**ATTRIBUTES.to_h { |name| [name, nil] })
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|