Skip to content

Commit a720619

Browse files
committed
Fixed majority of error cases
1 parent de1846d commit a720619

File tree

3 files changed

+101
-27
lines changed

3 files changed

+101
-27
lines changed

lib/src/main/kotlin/io/github/json5/kotlin/JSON5Exception.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ class JSON5Exception(
1515
*/
1616
fun formatChar(c: Char): String {
1717
return when {
18-
c.isISOControl() -> "\\x" + c.code.toString(16).padStart(2, '0')
18+
c.code < 0x20 || c == '\u007F' -> "\\x" + c.code.toString(16).padStart(2, '0')
1919
else -> c.toString()
2020
}
2121
}

lib/src/main/kotlin/io/github/json5/kotlin/JSON5Lexer.kt

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import kotlin.math.pow
99
class JSON5Lexer(private val source: String) {
1010
private var pos: Int = 0
1111
private var line: Int = 1
12-
private var column: Int = 0
12+
private var column: Int = 1 // Starting column at 1 to match JavaScript implementation
1313
private var currentChar: Char? = null
1414

1515
init {
@@ -44,18 +44,25 @@ class JSON5Lexer(private val source: String) {
4444
if (peek() == 'I') {
4545
// Handle -Infinity
4646
val sign = currentChar
47+
val startColumn = column
4748
advance()
4849
if (source.substring(pos, minOf(pos + 8, source.length)) == "Infinity") {
4950
repeat(8) { advance() }
50-
return Token.NumericToken(if (sign == '-') Double.NEGATIVE_INFINITY else Double.POSITIVE_INFINITY, line, column)
51+
return Token.NumericToken(if (sign == '-') Double.NEGATIVE_INFINITY else Double.POSITIVE_INFINITY, line, startColumn)
5152
} else if (source.substring(pos, minOf(pos + 3, source.length)) == "NaN") {
5253
// Handle -NaN (technically the same as NaN)
5354
repeat(3) { advance() }
54-
return Token.NumericToken(Double.NaN, line, column)
55+
return Token.NumericToken(Double.NaN, line, startColumn)
5556
}
5657
// Not Infinity/NaN, revert and continue with normal number parsing
5758
pos -= 1
58-
column -= 1
59+
if (currentChar == '\n') {
60+
line -= 1
61+
// Need to find last column position - but in this case we're just handling Infinity/NaN
62+
column = 1
63+
} else {
64+
column -= 1
65+
}
5966
currentChar = sign
6067
}
6168
readNumber()
@@ -70,7 +77,7 @@ class JSON5Lexer(private val source: String) {
7077
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
7178
}
7279

73-
advance() // Skip the 'u'
80+
advance() // Skip 'u'
7481
val hexDigits = StringBuilder()
7582
repeat(4) {
7683
if (currentChar == null || !currentChar!!.isHexDigit()) {
@@ -90,6 +97,7 @@ class JSON5Lexer(private val source: String) {
9097
// Continue reading the rest of the identifier
9198
while (true) {
9299
if (currentChar == '\\') {
100+
val continueColumn = column
93101
advance() // Skip backslash
94102
if (currentChar != 'u') {
95103
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
@@ -105,7 +113,12 @@ class JSON5Lexer(private val source: String) {
105113
advance()
106114
}
107115

108-
buffer.append(identHexDigits.toString().toInt(16).toChar())
116+
val continueChar = identHexDigits.toString().toInt(16).toChar()
117+
if (!isIdentifierPart(continueChar)) {
118+
throw JSON5Exception.invalidIdentifierChar(line, continueColumn)
119+
}
120+
121+
buffer.append(continueChar)
109122
} else if (currentChar != null && isIdentifierPart(currentChar)) {
110123
buffer.append(currentChar)
111124
advance()
@@ -118,16 +131,26 @@ class JSON5Lexer(private val source: String) {
118131
}
119132
'/' -> {
120133
// Handle incomplete comments
121-
if (peek() == null) {
122-
throw JSON5Exception.invalidChar('/', line, column)
134+
val startColumn = column
135+
val lookAhead = peek()
136+
if (lookAhead == null) {
137+
advance()
138+
throw JSON5Exception.invalidChar('/', line, startColumn)
139+
}
140+
if (lookAhead != '/' && lookAhead != '*') {
141+
advance()
142+
throw JSON5Exception.invalidChar('/', line, startColumn)
123143
}
124-
throw JSON5Exception.invalidChar(peek() ?: ' ', line, column + 1)
144+
throw JSON5Exception.invalidChar(lookAhead, line, column + 1)
125145
}
126146
else -> {
127147
if (isIdentifierStart(currentChar)) {
128148
readIdentifier()
129149
} else {
130-
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
150+
val c = currentChar ?: ' '
151+
val startColumn = column
152+
advance()
153+
throw JSON5Exception.invalidChar(c, line, startColumn)
131154
}
132155
}
133156
}
@@ -149,7 +172,7 @@ class JSON5Lexer(private val source: String) {
149172
currentChar = source[pos]
150173
if (currentChar == '\n') {
151174
line++
152-
column = 0
175+
column = 1 // Reset to 1 when we encounter a newline
153176
} else {
154177
column++
155178
}
@@ -201,6 +224,9 @@ class JSON5Lexer(private val source: String) {
201224
}
202225
advance()
203226
}
227+
if (currentChar == null) {
228+
throw JSON5Exception.invalidEndOfInput(line, column)
229+
}
204230
skipWhitespace()
205231
}
206232
}
@@ -233,13 +259,15 @@ class JSON5Lexer(private val source: String) {
233259
}
234260

235261
if (!done) {
236-
throw JSON5Exception.invalidEndOfInput(startLine, startColumn)
262+
throw JSON5Exception.invalidEndOfInput(startLine, startColumn + 1)
237263
}
238264

239-
return Token.StringToken(buffer.toString(), line, startColumn)
265+
return Token.StringToken(buffer.toString(), startLine, startColumn)
240266
}
241267

242268
private fun readEscapeSequence(): Char {
269+
val escapeCol = column
270+
243271
when (currentChar) {
244272
'b' -> {
245273
advance()
@@ -303,30 +331,24 @@ class JSON5Lexer(private val source: String) {
303331
try {
304332
return readHexEscape(2)
305333
} catch (e: Exception) {
306-
if (currentChar != null) {
307-
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
308-
}
309-
throw JSON5Exception.invalidEndOfInput(line, column)
334+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
310335
}
311336
}
312337
'u' -> {
313338
advance()
314339
try {
315340
return readHexEscape(4)
316341
} catch (e: Exception) {
317-
if (currentChar != null) {
318-
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
319-
}
320-
throw JSON5Exception.invalidEndOfInput(line, column)
342+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
321343
}
322344
}
323-
null -> throw JSON5Exception.invalidEndOfInput(line, column)
345+
null -> throw JSON5Exception.invalidEndOfInput(line, escapeCol)
324346
in '1'..'9' -> throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
325347
else -> {
326348
// Just return the character after the backslash (e.g. for \', \", etc.)
327349
val c = currentChar
328350
advance()
329-
return c ?: throw JSON5Exception.invalidEndOfInput(line, column)
351+
return c ?: throw JSON5Exception.invalidEndOfInput(line, escapeCol)
330352
}
331353
}
332354
}
@@ -352,6 +374,11 @@ class JSON5Lexer(private val source: String) {
352374
if (source.substring(pos, minOf(pos + 4, source.length)) == "true") {
353375
repeat(4) { advance() }
354376
return Token.BooleanToken(true, startLine, startColumn)
377+
} else {
378+
val c = source.getOrNull(pos + 3)
379+
if (c != null && pos + 3 < source.length) {
380+
throw JSON5Exception.invalidChar(c, line, column + 3)
381+
}
355382
}
356383

357384
throw JSON5Exception("Unexpected identifier", startLine, startColumn)
@@ -404,13 +431,19 @@ class JSON5Lexer(private val source: String) {
404431

405432
// Handle sign
406433
if (currentChar == '+') {
434+
buffer.append('+')
407435
advance() // Skip '+'
408436
} else if (currentChar == '-') {
409437
isNegative = true
410438
buffer.append('-')
411439
advance() // Skip '-'
412440
}
413441

442+
// Handle number following sign
443+
if (currentChar == null || (!currentChar!!.isDigit() && currentChar != '.')) {
444+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
445+
}
446+
414447
// Handle hexadecimal notation
415448
if (currentChar == '0' && (peek() == 'x' || peek() == 'X')) {
416449
buffer.append('0')
@@ -473,11 +506,24 @@ class JSON5Lexer(private val source: String) {
473506
var hasExponentPart = false
474507
if (currentChar == 'e' || currentChar == 'E') {
475508
buffer.append(currentChar)
509+
510+
// Save position for error reporting
511+
val eColumn = column
476512
advance()
477513

478514
if (currentChar == '+' || currentChar == '-') {
479515
buffer.append(currentChar)
516+
517+
// Save position for error reporting
518+
val signColumn = column
480519
advance()
520+
521+
// Check for invalid character after exponent sign
522+
if (currentChar == null || !currentChar!!.isDigit()) {
523+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
524+
}
525+
} else if (currentChar == null || !currentChar!!.isDigit()) {
526+
throw JSON5Exception.invalidChar(currentChar ?: ' ', line, column)
481527
}
482528

483529
var hasExponentDigits = false

lib/src/main/kotlin/io/github/json5/kotlin/JSON5Parser.kt

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,24 @@ internal object JSON5Parser {
2525
* @throws JSON5Exception if the input is invalid JSON5
2626
*/
2727
fun parse(text: String, reviver: ((key: String, value: Any?) -> Any?)? = null): Any? {
28+
if (text.isEmpty()) {
29+
throw JSON5Exception.invalidEndOfInput(1, 1)
30+
}
31+
2832
val lexer = JSON5Lexer(text)
2933
var token = lexer.nextToken()
3034

31-
// Handle empty input
35+
// Handle empty input or only comments
3236
if (token.type == TokenType.EOF) {
33-
throw JSON5Exception("Empty JSON5 input", token.line, token.column)
37+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
3438
}
3539

3640
val result = parseValue(token, lexer)
3741

3842
// Check that there's no extra content after the JSON5 value
3943
token = lexer.nextToken()
4044
if (token.type != TokenType.EOF) {
41-
throw JSON5Exception("Unexpected additional content after JSON5 value", token.line, token.column)
45+
throw JSON5Exception.invalidChar(text[token.column - 1], token.line, token.column)
4246
}
4347

4448
// Apply the reviver function if provided
@@ -124,6 +128,10 @@ internal object JSON5Parser {
124128
}
125129

126130
while (true) {
131+
if (token.type == TokenType.EOF) {
132+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
133+
}
134+
127135
// Parse the property name
128136
val key = when {
129137
token.type == TokenType.STRING -> (token as Token.StringToken).stringValue
@@ -135,19 +143,31 @@ internal object JSON5Parser {
135143

136144
// Expect a colon
137145
token = lexer.nextToken()
146+
if (token.type == TokenType.EOF) {
147+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
148+
}
149+
138150
if (token.type != TokenType.PUNCTUATOR || (token as Token.PunctuatorToken).punctuator != ":") {
139151
throw JSON5Exception("Expected ':' after property name", token.line, token.column)
140152
}
141153

142154
// Parse the property value
143155
token = lexer.nextToken()
156+
if (token.type == TokenType.EOF) {
157+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
158+
}
159+
144160
val value = parseValue(token, lexer)
145161

146162
// Add the property to the object
147163
result[key] = value
148164

149165
// Expect a comma or closing brace
150166
token = lexer.nextToken()
167+
if (token.type == TokenType.EOF) {
168+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
169+
}
170+
151171
if (token.type == TokenType.PUNCTUATOR) {
152172
token as Token.PunctuatorToken
153173
if (token.punctuator == "}") {
@@ -179,6 +199,10 @@ internal object JSON5Parser {
179199
}
180200

181201
while (true) {
202+
if (token.type == TokenType.EOF) {
203+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
204+
}
205+
182206
// Parse the element value
183207
val value = parseValue(token, lexer)
184208

@@ -187,6 +211,10 @@ internal object JSON5Parser {
187211

188212
// Expect a comma or closing bracket
189213
token = lexer.nextToken()
214+
if (token.type == TokenType.EOF) {
215+
throw JSON5Exception.invalidEndOfInput(token.line, token.column)
216+
}
217+
190218
if (token.type == TokenType.PUNCTUATOR) {
191219
token as Token.PunctuatorToken
192220
if (token.punctuator == "]") {

0 commit comments

Comments
 (0)