diff --git a/src/JSON.jl b/src/JSON.jl index 3764a6f0..cd73c1da 100644 --- a/src/JSON.jl +++ b/src/JSON.jl @@ -50,10 +50,15 @@ end @noinline function _invalid(error, buf, pos::Int, typename::String) # compute which line the error falls on by counting “\n” bytes up to pos cus = buf isa AbstractString ? codeunits(buf) : buf - line_no = count(b -> b == UInt8('\n'), view(cus, 1:pos)) + 1 - - li = pos > 20 ? pos - 9 : 1 - ri = min(sizeof(cus), pos + 20) + # `pos` can point one byte past the end: UnexpectedEOF is reported at the + # position we wanted to read, so every input ending mid-token lands here + # with pos == sizeof(cus) + 1. Clamp before slicing, or building the error + # message throws BoundsError instead of the ArgumentError we mean to raise. + n = sizeof(cus) + line_no = count(b -> b == UInt8('\n'), view(cus, 1:min(pos, n))) + 1 + + li = pos > 20 ? min(pos - 9, n) : 1 + ri = min(n, pos + 20) snippet_bytes = cus[li:ri] snippet_pos = pos - li + 1 snippet = String(copy(snippet_bytes)) diff --git a/test/parse.jl b/test/parse.jl index a7dd2566..bbea56ba 100644 --- a/test/parse.jl +++ b/test/parse.jl @@ -276,6 +276,35 @@ JSON.lift(::DateMaterializedObjectStyle, ::Type{Date}, x::JSON.Object) = Date(x[ @test x.url == "http://www.example.com/#\\\ud8000\\好" end # @testset "errors" + @testset "truncated input reports UnexpectedEOF" begin + # UnexpectedEOF is raised at the byte we wanted to read, i.e. one past + # the end, so building the error message used to slice out of bounds and + # throw BoundsError from the error path itself. + for str in ("{", "[", "[1", "[1,", "\"a", "{\"a\"", "{\"a\":", "{\"a\":1,", + " ", "\n", "\t", "{\"a\":[1,2", "-") + @test_throws ArgumentError JSON.parse(str) + end + # Same for the typed and lazy entry points. + @test_throws ArgumentError JSON.parse("{\"a\":", @NamedTuple{a::Int}) + @test_throws ArgumentError JSON.lazy("{\"a\":")[] + # ...and when the buffer is bytes rather than a string. + @test_throws ArgumentError JSON.parse(Vector{UInt8}("{\"a\":")) + + # The message still carries a correct position, line number and snippet. + err = try + JSON.parse("{\n \"a\": 1,\n \"b\":") + nothing + catch e + e + end + @test err isa ArgumentError + msg = err.msg + @test occursin("UnexpectedEOF", msg) + @test occursin("byte position 18", msg) || occursin("byte position 19", msg) + @test occursin("line 3", msg) + @test occursin("", msg) + end # @testset "truncated input reports UnexpectedEOF" + # JSON.jl pre-1.0 compat x = JSON.parse("{}") @test isempty(x) && typeof(x) == JSON.Object{String, Any}