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/input.rb
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JSON5
|
|
4
|
+
class Input
|
|
5
|
+
Position = Data.define(:byte_offset, :line, :column)
|
|
6
|
+
|
|
7
|
+
attr_reader :source, :index, :line, :column, :line_start_byte
|
|
8
|
+
|
|
9
|
+
def initialize(source, limits: Limits.default, filename: nil)
|
|
10
|
+
unless source.is_a?(String)
|
|
11
|
+
raise TypeError, "source must be a String"
|
|
12
|
+
end
|
|
13
|
+
unless limits.is_a?(Limits)
|
|
14
|
+
raise ArgumentError, "limits must be a JSON5::Limits"
|
|
15
|
+
end
|
|
16
|
+
@filename = filename
|
|
17
|
+
unless [Encoding::UTF_8, Encoding::US_ASCII].include?(source.encoding)
|
|
18
|
+
raise EncodingError.new(
|
|
19
|
+
"source must be UTF-8 or US-ASCII",
|
|
20
|
+
code: "invalid_encoding",
|
|
21
|
+
filename: filename,
|
|
22
|
+
byte_offset: 0,
|
|
23
|
+
end_byte_offset: [source.bytesize, 1].min,
|
|
24
|
+
line: 1,
|
|
25
|
+
column: 1,
|
|
26
|
+
excerpt: excerpt_for(source, 0)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
if limits.max_input_bytes && source.bytesize > limits.max_input_bytes
|
|
30
|
+
line, column = position_for_limit(source, limits.max_input_bytes)
|
|
31
|
+
raise LimitError.new(
|
|
32
|
+
"input exceeds max_input_bytes",
|
|
33
|
+
code: "maximum_input_size_exceeded",
|
|
34
|
+
filename: filename,
|
|
35
|
+
byte_offset: limits.max_input_bytes,
|
|
36
|
+
end_byte_offset: limits.max_input_bytes + 1,
|
|
37
|
+
line: line,
|
|
38
|
+
column: column,
|
|
39
|
+
excerpt: limit_excerpt_for(source, limits.max_input_bytes)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
unless source.valid_encoding?
|
|
43
|
+
offset = first_invalid_utf8_offset(source)
|
|
44
|
+
line, column = position_at(source, offset)
|
|
45
|
+
raise EncodingError.new(
|
|
46
|
+
"source is not valid UTF-8",
|
|
47
|
+
code: "invalid_encoding",
|
|
48
|
+
filename: filename,
|
|
49
|
+
byte_offset: offset,
|
|
50
|
+
end_byte_offset: [offset + 1, source.bytesize].min,
|
|
51
|
+
line: line,
|
|
52
|
+
column: column,
|
|
53
|
+
excerpt: excerpt_for(source, offset)
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@source = source
|
|
58
|
+
@limits = limits
|
|
59
|
+
@index = 0
|
|
60
|
+
@line = 1
|
|
61
|
+
@column = 1
|
|
62
|
+
@line_start_byte = 0
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def eof?
|
|
66
|
+
index >= source.bytesize
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def peek_byte(offset = 0)
|
|
70
|
+
source.getbyte(index + offset)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def mark
|
|
74
|
+
Position.new(index, line, column)
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def position
|
|
78
|
+
mark
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def self.position_for(source, byte_offset)
|
|
82
|
+
return [nil, nil] unless source.is_a?(String)
|
|
83
|
+
|
|
84
|
+
normalized = source.dup
|
|
85
|
+
normalized.force_encoding(Encoding::UTF_8) unless normalized.encoding == Encoding::UTF_8
|
|
86
|
+
input = allocate
|
|
87
|
+
input.send(:position_at, normalized, [[byte_offset, 0].max, normalized.bytesize].min)
|
|
88
|
+
rescue ArgumentError, TypeError
|
|
89
|
+
[nil, nil]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def byteslice(start_byte, end_byte = index)
|
|
93
|
+
source.byteslice(start_byte...end_byte)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def remaining
|
|
97
|
+
source.bytesize - index
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def consume_byte
|
|
101
|
+
byte = peek_byte
|
|
102
|
+
return nil unless byte
|
|
103
|
+
@index += 1
|
|
104
|
+
@column += 1
|
|
105
|
+
byte
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def consume_ascii(expected = nil)
|
|
109
|
+
byte = peek_byte
|
|
110
|
+
if expected && byte != expected
|
|
111
|
+
raise ArgumentError, "expected byte #{expected}, got #{byte.inspect}"
|
|
112
|
+
end
|
|
113
|
+
consume_byte
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def consume_bytes(count)
|
|
117
|
+
count.times { consume_byte }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def consume_ascii_bytes(count)
|
|
121
|
+
@index += count
|
|
122
|
+
@column += count
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def peek_codepoint
|
|
126
|
+
return [nil, 0] if eof?
|
|
127
|
+
byte = peek_byte
|
|
128
|
+
return [byte, 1] if byte < 0x80
|
|
129
|
+
decode_utf8(index)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def consume_codepoint
|
|
133
|
+
codepoint, width = peek_codepoint
|
|
134
|
+
return [nil, 0] unless codepoint
|
|
135
|
+
if codepoint == 0x0d
|
|
136
|
+
width += 1 if peek_byte(width) == 0x0a
|
|
137
|
+
@index += width
|
|
138
|
+
advance_line
|
|
139
|
+
elsif line_terminator?(codepoint)
|
|
140
|
+
@index += width
|
|
141
|
+
advance_line
|
|
142
|
+
else
|
|
143
|
+
@index += width
|
|
144
|
+
@column += 1
|
|
145
|
+
end
|
|
146
|
+
[codepoint, width]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def line_terminator?(codepoint)
|
|
150
|
+
codepoint == 0x0a || codepoint == 0x0d || codepoint == 0x2028 || codepoint == 0x2029
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def excerpt_at(byte_offset, radius: 40)
|
|
154
|
+
start_byte = [byte_offset - radius, 0].max
|
|
155
|
+
finish_byte = [byte_offset + radius, source.bytesize].min
|
|
156
|
+
source.byteslice(start_byte...finish_byte)&.scrub
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
private
|
|
160
|
+
|
|
161
|
+
def advance_line
|
|
162
|
+
@line += 1
|
|
163
|
+
@column = 1
|
|
164
|
+
@line_start_byte = index
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def decode_utf8(offset)
|
|
168
|
+
first = source.getbyte(offset)
|
|
169
|
+
width, codepoint, minimum = utf8_shape(first)
|
|
170
|
+
bytes = (1...width).map { |delta| source.getbyte(offset + delta) }
|
|
171
|
+
unless bytes.all? { |byte| byte && byte.between?(0x80, 0xbf) }
|
|
172
|
+
invalid_utf8!(offset)
|
|
173
|
+
end
|
|
174
|
+
codepoint = bytes.reduce(codepoint) { |value, byte| (value << 6) | (byte & 0x3f) }
|
|
175
|
+
invalid_utf8!(offset) if codepoint < minimum || codepoint > 0x10ffff || codepoint.between?(0xd800, 0xdfff)
|
|
176
|
+
[codepoint, width]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def utf8_shape(first)
|
|
180
|
+
case first
|
|
181
|
+
when 0xc2..0xdf then [2, first & 0x1f, 0x80]
|
|
182
|
+
when 0xe0..0xef then [3, first & 0x0f, 0x800]
|
|
183
|
+
when 0xf0..0xf4 then [4, first & 0x07, 0x10000]
|
|
184
|
+
else invalid_utf8!(index)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def invalid_utf8!(offset)
|
|
189
|
+
raise EncodingError.new(
|
|
190
|
+
"invalid UTF-8 sequence",
|
|
191
|
+
code: "invalid_encoding",
|
|
192
|
+
filename: @filename,
|
|
193
|
+
byte_offset: offset,
|
|
194
|
+
end_byte_offset: [offset + 1, source.bytesize].min,
|
|
195
|
+
line: line,
|
|
196
|
+
column: column,
|
|
197
|
+
excerpt: excerpt_at(offset)
|
|
198
|
+
)
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def excerpt_for(source, byte_offset, radius: 40)
|
|
202
|
+
start_byte = [byte_offset - radius, 0].max
|
|
203
|
+
finish_byte = [byte_offset + radius, source.bytesize].min
|
|
204
|
+
source.byteslice(start_byte...finish_byte)&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def limit_excerpt_for(source, byte_offset, radius: 40)
|
|
208
|
+
start_byte = [byte_offset - radius, 0].max
|
|
209
|
+
source.byteslice(start_byte...byte_offset + 1)&.dup&.force_encoding(Encoding::UTF_8)&.scrub
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def first_invalid_utf8_offset(source)
|
|
213
|
+
offset = 0
|
|
214
|
+
while offset < source.bytesize
|
|
215
|
+
first = source.getbyte(offset)
|
|
216
|
+
if first < 0x80
|
|
217
|
+
offset += 1
|
|
218
|
+
next
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
width, codepoint, minimum = case first
|
|
222
|
+
when 0xc2..0xdf then [2, first & 0x1f, 0x80]
|
|
223
|
+
when 0xe0..0xef then [3, first & 0x0f, 0x800]
|
|
224
|
+
when 0xf0..0xf4 then [4, first & 0x07, 0x10000]
|
|
225
|
+
else return offset
|
|
226
|
+
end
|
|
227
|
+
bytes = (1...width).map { |delta| source.getbyte(offset + delta) }
|
|
228
|
+
return offset unless bytes.all? { |byte| byte && byte.between?(0x80, 0xbf) }
|
|
229
|
+
|
|
230
|
+
codepoint = bytes.reduce(codepoint) { |value, byte| (value << 6) | (byte & 0x3f) }
|
|
231
|
+
return offset if codepoint < minimum || codepoint > 0x10ffff || codepoint.between?(0xd800, 0xdfff)
|
|
232
|
+
|
|
233
|
+
offset += width
|
|
234
|
+
end
|
|
235
|
+
source.bytesize
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def position_at(source, byte_offset)
|
|
239
|
+
line = 1
|
|
240
|
+
column = 1
|
|
241
|
+
index = 0
|
|
242
|
+
while index < byte_offset
|
|
243
|
+
packed = valid_codepoint_at(source, index)
|
|
244
|
+
width = packed & 0x07
|
|
245
|
+
codepoint = packed >> 3
|
|
246
|
+
break if index + width > byte_offset
|
|
247
|
+
|
|
248
|
+
if codepoint == 0x0d
|
|
249
|
+
width += 1 if index + width < byte_offset && source.getbyte(index + width) == 0x0a
|
|
250
|
+
line += 1
|
|
251
|
+
column = 1
|
|
252
|
+
elsif line_terminator?(codepoint)
|
|
253
|
+
line += 1
|
|
254
|
+
column = 1
|
|
255
|
+
else
|
|
256
|
+
column += 1
|
|
257
|
+
end
|
|
258
|
+
index += width
|
|
259
|
+
end
|
|
260
|
+
[line, column]
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def valid_codepoint_at(source, offset)
|
|
264
|
+
first = source.getbyte(offset)
|
|
265
|
+
return (first << 3) | 1 if first < 0x80
|
|
266
|
+
|
|
267
|
+
width, codepoint = case first
|
|
268
|
+
when 0xc2..0xdf then [2, first & 0x1f]
|
|
269
|
+
when 0xe0..0xef then [3, first & 0x0f]
|
|
270
|
+
when 0xf0..0xf4 then [4, first & 0x07]
|
|
271
|
+
else raise ArgumentError, "invalid UTF-8 before requested position"
|
|
272
|
+
end
|
|
273
|
+
(1...width).each { |delta| codepoint = (codepoint << 6) | (source.getbyte(offset + delta) & 0x3f) }
|
|
274
|
+
(codepoint << 3) | width
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def position_for_limit(source, byte_offset)
|
|
278
|
+
position_at(source, byte_offset)
|
|
279
|
+
rescue ArgumentError
|
|
280
|
+
[nil, nil]
|
|
281
|
+
end
|
|
282
|
+
end
|
|
283
|
+
end
|