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.
@@ -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