From 255064b7fd129b99ecb2016ddfe91b24c7d49998 Mon Sep 17 00:00:00 2001 From: Hasan Date: Fri, 6 Nov 2026 13:37:42 +0500 Subject: [PATCH] Java parser: quote the same error snippet as the C parser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The C parser's build_parse_error_message quotes the source up to the next whitespace, at most 32 bytes, and backs off when the limit splits a multibyte character. The Java parser quoted 32 raw bytes, so JSON.parse("あ" * 23) raised "unexpected token at 'ああああああああああ\xE3\x81'", a message with invalid UTF-8, and 'Infinity something' was quoted whole. Build the fragment the same way, show EOF at the end of the input, and use the C parser's "unexpected character:" and "unexpected token at end of stream" messages for an invalid value and for trailing garbage. JRuby errors have carried positions for a while, so this lets test_parse_error_snippet and test_parse_error_snippet_multibyte_boundaries run on JRuby, along with three assertions in test_error_messages. Part of https://github.com/ruby/json/issues/1123 --- CHANGES.md | 1 + java/src/json/ext/Parser.java | 35 ++++++++++++++++++++++++------- test/json/json_ext_parser_test.rb | 12 +++-------- test/json/json_parser_test.rb | 4 ---- 4 files changed, 32 insertions(+), 20 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2bad52aed..732df3220 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,6 +5,7 @@ * Fix `JSON::ParserError#json_path` raising `NoMethodError` when the document was parsed with `symbolize_names: true`. * Fix `JSON.generate` not detecting duplicate keys such as `{ "1" => 1, 1 => 2 }`. * JRuby: raise `TypeError` when a String subclass used as a Hash key has a `to_s` that doesn't return a String, like the C extension. +* JRuby: parse error messages quote the same snippet as the C extension, stopping at whitespace and never splitting a multibyte character. ### 2026-10-13 (3.1.0) diff --git a/java/src/json/ext/Parser.java b/java/src/json/ext/Parser.java index eb5ce3a9a..cc0666655 100644 --- a/java/src/json/ext/Parser.java +++ b/java/src/json/ext/Parser.java @@ -195,6 +195,7 @@ private IRubyObject createCustomDecimal(final ThreadContext context, final ByteL private static final class ParserSession { private static final int INITIAL_FRAME_CAPACITY = 32; private static final int INITIAL_VALUE_CAPACITY = 64; + private static final int PARSE_ERROR_FRAGMENT_LEN = 32; // Integers with fewer than this many digits are built directly from a // long accumulated during the digit scan; longer ones fall back to @@ -304,7 +305,7 @@ public IRubyObject parse(ThreadContext context) { // Only trailing whitespace (and comments) may follow the document. eatWhitespace(); if (cursor < end) { - throw unexpectedToken(cursor, end); + throw parsingError("unexpected token at end of stream ", cursor, end); } return result; } @@ -392,7 +393,7 @@ private IRubyObject run() { case 0: throw parseError("unexpected end of input"); default: - throw unexpectedToken(cursor, end); + throw parsingError("unexpected character: ", cursor, end); } pushValue(value); @@ -958,16 +959,36 @@ private RaiseException parseError(RubyString message, IRubyObject duplicateKey) return error; } - private RaiseException parsingError(int absStart, int absEnd) { + private RaiseException parsingError(String prefix, int absStart, int absEnd) { cursor = absStart; - RubyString msg = context.runtime.newString("unexpected token at '") - .cat(data, absStart, Math.min(absEnd - absStart, 32)) - .cat((byte)'\''); + RubyString msg = context.runtime.newString(prefix); + if (absStart >= absEnd) { + msg.catString("EOF"); + return parseError(msg, null); + } + + // Like the C parser, quote the source up to the next whitespace, + // at most PARSE_ERROR_FRAGMENT_LEN bytes. + int len = 0; + while (len < PARSE_ERROR_FRAGMENT_LEN && absStart + len < absEnd) { + byte b = data[absStart + len]; + if (b == 0 || b == '\n' || b == ' ' || b == '\t' || b == '\r') { + break; + } + len++; + } + if (len == PARSE_ERROR_FRAGMENT_LEN) { + // Only trim when the byte limit splits a multibyte character. + while (len > 0 && absStart + len < absEnd && (data[absStart + len] & 0xC0) == 0x80) { + len--; + } + } + msg.cat((byte)'\'').cat(data, absStart, len).cat((byte)'\''); return parseError(msg, null); } private RaiseException unexpectedToken(int absStart, int absEnd) { - return parsingError(absStart, absEnd); + return parsingError("unexpected token ", absStart, absEnd); } private RaiseException newException(String className, String message) { diff --git a/test/json/json_ext_parser_test.rb b/test/json/json_ext_parser_test.rb index 8b10146ad..30e2b8fdf 100644 --- a/test/json/json_ext_parser_test.rb +++ b/test/json/json_ext_parser_test.rb @@ -16,14 +16,10 @@ def test_allocate def test_error_messages ex = assert_raise(ParserError) { parse('Infinity something') } - unless RUBY_PLATFORM =~ /java/ - assert_equal "unexpected token 'Infinity' at line 1 column 1", ex.message - end + assert_equal "unexpected token 'Infinity' at line 1 column 1", ex.message ex = assert_raise(ParserError) { parse('foo bar') } - unless RUBY_PLATFORM =~ /java/ - assert_equal "unexpected token 'foo' at line 1 column 1", ex.message - end + assert_equal "unexpected token 'foo' at line 1 column 1", ex.message ex = assert_raise(ParserError) { parse('-Infinity something') } unless RUBY_PLATFORM =~ /java/ @@ -31,9 +27,7 @@ def test_error_messages end ex = assert_raise(ParserError) { parse('NaN something') } - unless RUBY_PLATFORM =~ /java/ - assert_equal "unexpected token 'NaN' at line 1 column 1", ex.message - end + assert_equal "unexpected token 'NaN' at line 1 column 1", ex.message ex = assert_raise(ParserError) { parse(' ') } unless RUBY_PLATFORM =~ /java/ diff --git a/test/json/json_parser_test.rb b/test/json/json_parser_test.rb index 0549f6f4c..448e6eee5 100644 --- a/test/json/json_parser_test.rb +++ b/test/json/json_parser_test.rb @@ -867,8 +867,6 @@ def test_parse_error_incomplete_hash end def test_parse_error_snippet - omit "JRuby errors don't contain positions" if RUBY_ENGINE == "jruby" - error = assert_raise(JSON::ParserError) { JSON.parse("あああああああああああああああああああああああ") } assert_equal "unexpected character: 'ああああああああああ' at line 1 column 1", error.message @@ -898,8 +896,6 @@ def test_parse_error_snippet end def test_parse_error_snippet_multibyte_boundaries - omit "JRuby errors don't contain positions" if RUBY_ENGINE == "jruby" - ['é', '日', '😀'].each do |char| ['', ' ', "\n", "\t", "\r"].each do |suffix| error = assert_raise(JSON::ParserError) { JSON.parse(char + suffix) }