ruby/test/json/json_parser_test.rb
Jean Boussier 342caec27b Upgrade json gem to 1.8.2
Merge of the following upstream commits:

commit 6d3266dc0edbbdb1dae513f4f3be16198c432af3
Author: Jean Boussier <byroot@ruby-lang.org>
Date:   Mon Nov 2 15:51:58 2026 +0100

    Release 2.18.2

commit dcfbbcd36f240c18c3b89e5399105715f70162e3
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Wed Mar 25 11:45:17 2026 +0100

    Fix handling of unescaped control characters preceeded by a backslash

    GHSA-cjpr-jw2g-fw99

commit 6ec6e7baff137e67831a23090061e84bf028dbfc
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Tue Feb 3 08:35:11 2026 +0100

    Release 2.18.1

commit fff25c9f4b9c590a59ed4a2ec3c545121db844d3
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Mon Feb 2 19:44:02 2026 +0100

    fbuffer_append_str: assume string

commit 79b6e168ba02f6ac7a0dac378968b1c918af91db
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Mon Feb 2 22:06:44 2026 +0100

    Ensure `Generator::State` is kept on the stack

    Fix: https://github.com/ruby/json/issues/929

    When calling `cState_partial_generate` from `mHash_to_json` or other
    `to_json` funcs, `VState` becomes unreachable very quickly, hence the
    compiler may optimize it out of the stack, and make it invisible to
    the GC stack scanning. This is particularly liekly given how aggressively
    we inline.

    Repro:

    ```ruby
    require 'json'

    test_data = {
      "flag" => true,
      "data" => 10000.times.map { [1.0] },
      :flag => false,
    }

    10.times do
      test_data.to_json
    end
    ```

    But in practice the cause was just that the issued warning calls
    Hash#inspect on a big hash, which triggers GC.

    So it can be triggered even more reliably with:

    ```ruby
    require 'json'

    module JSON
      module Common
        def self.on_mixed_keys_hash(...)
          GC.start
        end
      end
    end

    test_data = {
      "flag" => true,
      "data" => 10000.times.map { [1.0] },
      :flag => false,
    }

    test_data.to_json
    ```

commit 062fcdd22818d226f0e4b49647eb2aabbfaf002b
Author: BurdetteLamar <burdettelamar@yahoo.com>
Date:   Thu Jan 22 22:08:15 2026 +0000

    Improve class JSON intro

commit 1b276c8623da0ff9bec62396d9a7172365bc1784
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Fri Jan 16 18:43:08 2026 +0100

    Extract json_fast_memcpy16 for readability

commit 643ee11fed6898b7e6f75f266daa6e4b2a07bb07
Author: Scott Myron <samyron@gmail.com>
Date:   Thu Jan 15 19:12:41 2026 -0600

    Use __builtin_memcpy, if available, to copy overlapping byte ranges in copy_remaining_bytes to avoid a branch to MEMCPY. Additionally use a space as padding byte instead of an 'X' so it can be represented diretly on AArch64 with a single instruction.

commit 15eb40dbdfb299813bbbb72f70e12d1614b26200
Author: Nobuyoshi Nakada <nobu@ruby-lang.org>
Date:   Wed Jan 14 21:28:37 2026 +0900

    Remove trailing spaces [ci skip]

commit 73818b80c8fe4394153002a60ecd285e8688b4cd
Author: Scott Myron <samyron@gmail.com>
Date:   Tue Jan 13 22:17:16 2026 -0600

    initialize search.chunk_end to silence a warning about it being potentially uninitialized

commit 86a5cce405102eca15d639e2dae2f539fb3e7c26
Author: Scott Myron <samyron@gmail.com>
Date:   Mon Jan 12 18:57:09 2026 -0600

    use a conditional to select SIMD implementation rather than pointer

commit a51317c94900ec5ea44ef0fd9327cca4d601bb5c
Author: Scott Myron <samyron@gmail.com>
Date:   Fri Jan 2 20:47:59 2026 -0600

    Directly write to the output buffer when converting UTF32 to UTF8.

commit 3e23e57568d29aa96f73d5f0c27b8c3676af4ee5
Merge: 976ba366 8ad744c5
Author: Nobuyoshi Nakada <nobu@ruby-lang.org>
Date:   Wed Dec 31 21:43:48 2025 +0900

    Merge pull request #921 from nobu/nonportable-code

    Fix non-portable code

commit 8ad744c53269330cc827bf4ae213991ae11712ce
Author: Nobuyoshi Nakada <nobu@ruby-lang.org>
Date:   Wed Dec 31 21:40:16 2025 +0900

    Fix non-portable code

    A plain `char` may be `signed` or `unsigned` depending on the
    implementation.  Also, bitwise ORing of `signed` values ​​is not
    guaranteed to be `signed`.  To ensure portability, should logical-OR
    each comparison, but casting to `signed char` is usually sufficient.

commit 976ba36629b2d20fa4de3da12ed405ab178deeba
Author: Scott Myron <samyron@gmail.com>
Date:   Tue Dec 30 21:32:03 2025 -0600

    Simplify unescape_unicode

    Co-Authored-By: Jean Boussier <jean.boussier@gmail.com>

commit d21d9362fa36bd1b34c53e28b04ba6e91115a3be
Author: Scott Myron <samyron@gmail.com>
Date:   Tue Dec 30 20:48:18 2025 -0600

    Keep track of the the number of additional backslashes to avoid an extra memchr searching the remaining characters when no more backslashes exist.

commit a5c160f37285cab4406017990a6a02e82e283055
Author: Jean Boussier <jean.boussier@gmail.com>
Date:   Mon Dec 22 09:16:09 2025 +0100

    Add missing documentation for `allow_control_characters` parsing option

commit 4808fee9affb801bc9bc63ea2d69d1cb28deda2a
Author: 久我山菜々 <nna@nna774.net>
Date:   Fri Dec 19 01:30:37 2025 +0900

    Update `fpconv_dtoa` definition to use `dest[32]`

commit 5793694ee6632245f345e3b923af5ebfd7db68a9
Author: Étienne Barrié <etienne.barrie@gmail.com>
Date:   Thu Dec 11 16:12:35 2025 +0100

    Revert "Skip test failing with JRuby in CI"

    This reverts commit b7e1734d9ca090e95b1923b05536f5fb6f437060.
2026-11-02 10:05:05 -08:00

867 lines
30 KiB
Ruby

# frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
require 'tempfile'
begin
require 'ostruct'
rescue LoadError
end
begin
require 'bigdecimal'
rescue LoadError
end
class JSONParserTest < Test::Unit::TestCase
include JSON
def test_construction
parser = JSON::Parser.new('test')
assert_equal 'test', parser.source
end
def test_argument_encoding_unmodified
source = "{}".encode(Encoding::UTF_16)
JSON::Parser.new(source)
assert_equal Encoding::UTF_16, source.encoding
end
def test_argument_encoding_for_binary_unmodified
source = "{}".b
JSON::Parser.new(source)
assert_equal Encoding::ASCII_8BIT, source.encoding
end
def test_error_message_encoding
bug10705 = '[ruby-core:67386] [Bug #10705]'
json = ".\"\xE2\x88\x9A\""
assert_equal(Encoding::UTF_8, json.encoding)
e = assert_raise(JSON::ParserError) {
JSON::Ext::Parser.new(json).parse
}
assert_equal(Encoding::UTF_8, e.message.encoding, bug10705)
assert_include(e.message, json, bug10705)
end
def test_parsing
parser = JSON::Parser.new('"test"')
assert_equal 'test', parser.parse
end
def test_parser_reset
parser = Parser.new('{"a":"b"}')
assert_equal({ 'a' => 'b' }, parser.parse)
assert_equal({ 'a' => 'b' }, parser.parse)
end
def test_parse_values
assert_equal(nil, parse('null'))
assert_equal(false, parse('false'))
assert_equal(true, parse('true'))
assert_equal(-23, parse('-23'))
assert_equal(23, parse('23'))
assert_in_delta(0.23, parse('0.23'), 1e-2)
assert_in_delta(0.0, parse('0e0'), 1e-2)
assert_equal("", parse('""'))
assert_equal("foobar", parse('"foobar"'))
end
def test_parse_simple_arrays
assert_equal([], parse('[]'))
assert_equal([], parse(' [ ] '))
assert_equal([ nil ], parse('[null]'))
assert_equal([ false ], parse('[false]'))
assert_equal([ true ], parse('[true]'))
assert_equal([ -23 ], parse('[-23]'))
assert_equal([ 23 ], parse('[23]'))
assert_equal_float([ 0.23 ], parse('[0.23]'))
assert_equal_float([ 0.0 ], parse('[0e0]'))
assert_equal([""], parse('[""]'))
assert_equal(["foobar"], parse('["foobar"]'))
assert_equal([{}], parse('[{}]'))
end
def test_parse_simple_objects
assert_equal({}, parse('{}'))
assert_equal({}, parse(' { } '))
assert_equal({ "a" => nil }, parse('{ "a" : null}'))
assert_equal({ "a" => nil }, parse('{"a":null}'))
assert_equal({ "a" => false }, parse('{ "a" : false } '))
assert_equal({ "a" => false }, parse('{"a":false}'))
assert_raise(JSON::ParserError) { parse('{false}') }
assert_equal({ "a" => true }, parse('{"a":true}'))
assert_equal({ "a" => true }, parse(' { "a" : true } '))
assert_equal({ "a" => -23 }, parse(' { "a" : -23 } '))
assert_equal({ "a" => -23 }, parse(' { "a" : -23 } '))
assert_equal({ "a" => 23 }, parse('{"a":23 } '))
assert_equal({ "a" => 23 }, parse(' { "a" : 23 } '))
assert_equal({ "a" => 0.23 }, parse(' { "a" : 0.23 } '))
assert_equal({ "a" => 0.23 }, parse(' { "a" : 0.23 } '))
assert_equal({ "" => 123 }, parse('{"":123}'))
end
def test_parse_numbers
assert_raise(JSON::ParserError) { parse('+23.2') }
assert_raise(JSON::ParserError) { parse('+23') }
assert_raise(JSON::ParserError) { parse('.23') }
assert_raise(JSON::ParserError) { parse('023') }
assert_raise(JSON::ParserError) { parse('-023') }
assert_raise(JSON::ParserError) { parse('023.12') }
assert_raise(JSON::ParserError) { parse('-023.12') }
assert_raise(JSON::ParserError) { parse('023e12') }
assert_raise(JSON::ParserError) { parse('-023e12') }
assert_raise(JSON::ParserError) { parse('-') }
assert_raise(JSON::ParserError) { parse('-.1') }
assert_raise(JSON::ParserError) { parse('-e0') }
assert_equal(23, parse('23'))
assert_equal(-23, parse('-23'))
assert_equal_float(3.141, parse('3.141'))
assert_equal_float(-3.141, parse('-3.141'))
assert_equal_float(3.141, parse('3141e-3'))
assert_equal_float(3.141, parse('3141.1e-3'))
assert_equal_float(3.141, parse('3141E-3'))
assert_equal_float(3.141, parse('3141.0E-3'))
assert_equal_float(-3.141, parse('-3141.0e-3'))
assert_equal_float(-3.141, parse('-3141e-3'))
assert_raise(ParserError) { parse('NaN') }
assert parse('NaN', :allow_nan => true).nan?
assert_raise(ParserError) { parse('Infinity') }
assert_equal(1.0/0, parse('Infinity', :allow_nan => true))
assert_raise(ParserError) { parse('-Infinity') }
assert_equal(-1.0/0, parse('-Infinity', :allow_nan => true))
capture_output { assert_equal(Float::INFINITY, parse("23456789012E666")) }
end
def test_parse_bignum
bignum = Integer('1234567890' * 10)
assert_equal(bignum, JSON.parse(bignum.to_s))
assert_equal(bignum.to_f, JSON.parse(bignum.to_s + ".0"))
end
def test_parse_bigdecimals
assert_equal(BigDecimal, JSON.parse('{"foo": 9.01234567890123456789}', decimal_class: BigDecimal)["foo"].class)
assert_equal(BigDecimal("0.901234567890123456789E1"),JSON.parse('{"foo": 9.01234567890123456789}', decimal_class: BigDecimal)["foo"] )
end if defined?(::BigDecimal)
def test_parse_string_mixed_unicode
assert_equal(["éé"], JSON.parse("[\"\\u00e9é\"]"))
end
def test_parse_more_complex_arrays
a = [ nil, false, true, "foßbar", [ "n€st€d", true ], { "nested" => true, "n€ßt€ð2" => {} }]
a.permutation.each do |perm|
json = pretty_generate(perm)
assert_equal perm, parse(json)
end
end
def test_parse_complex_objects
a = [ nil, false, true, "foßbar", [ "n€st€d", true ], { "nested" => true, "n€ßt€ð2" => {} }]
a.permutation.each do |perm|
s = "a"
orig_obj = perm.inject({}) { |h, x| h[s.dup] = x; s = s.succ; h }
json = pretty_generate(orig_obj)
assert_equal orig_obj, parse(json)
end
end
def test_parse_control_chars_in_string
0.upto(31) do |ord|
assert_raise JSON::ParserError do
parse(%("#{ord.chr}"))
end
end
end
def test_parse_allowed_control_chars_in_string
0.upto(31) do |ord|
assert_equal ord.chr, parse(%("#{ord.chr}"), allow_control_characters: true)
end
end
def test_parsse_control_char_and_backslash
backslash_and_control_char = "\\\t"
assert_raise JSON::ParserError do
JSON.parse(%("#{'a' * 30}#{backslash_and_control_char}"), allow_control_characters: true)
end
end
def test_parse_arrays
assert_equal([1,2,3], parse('[1,2,3]'))
assert_equal([1.2,2,3], parse('[1.2,2,3]'))
assert_equal([[],[[],[]]], parse('[[],[[],[]]]'))
assert_equal([], parse('[]'))
assert_equal([], parse(' [ ] '))
assert_equal([1], parse('[1]'))
assert_equal([1], parse(' [ 1 ] '))
ary = [[1], ["foo"], [3.14], [4711.0], [2.718], [nil],
[[1, -2, 3]], [false], [true]]
assert_equal(ary,
parse('[[1],["foo"],[3.14],[47.11e+2],[2718.0E-3],[null],[[1,-2,3]],[false],[true]]'))
assert_equal(ary, parse(%Q{ [ [1] , ["foo"] , [3.14] \t , [47.11e+2]\s
, [2718.0E-3 ],\r[ null] , [[1, -2, 3 ]], [false ],[ true]\n ] }))
end
def test_parse_json_primitive_values
assert_raise(JSON::ParserError) { parse('') }
assert_raise(TypeError) { parse(nil) }
assert_raise(JSON::ParserError) { parse(' /* foo */ ') }
assert_equal nil, parse('null')
assert_equal false, parse('false')
assert_equal true, parse('true')
assert_equal 23, parse('23')
assert_equal 1, parse('1')
assert_equal_float 3.141, parse('3.141'), 1E-3
assert_equal 2 ** 64, parse('18446744073709551616')
assert_equal 'foo', parse('"foo"')
assert parse('NaN', :allow_nan => true).nan?
assert parse('Infinity', :allow_nan => true).infinite?
assert parse('-Infinity', :allow_nan => true).infinite?
end
def test_parse_arrays_with_allow_trailing_comma
assert_equal([], parse('[]', allow_trailing_comma: true))
assert_equal([], parse('[]', allow_trailing_comma: false))
assert_raise(JSON::ParserError) { parse('[,]', allow_trailing_comma: true) }
assert_raise(JSON::ParserError) { parse('[,]', allow_trailing_comma: false) }
assert_equal([1], parse('[1]', allow_trailing_comma: true))
assert_equal([1], parse('[1]', allow_trailing_comma: false))
assert_equal([1], parse('[1,]', allow_trailing_comma: true))
assert_raise(JSON::ParserError) { parse('[1,]', allow_trailing_comma: false) }
assert_equal([1, 2, 3], parse('[1,2,3]', allow_trailing_comma: true))
assert_equal([1, 2, 3], parse('[1,2,3]', allow_trailing_comma: false))
assert_equal([1, 2, 3], parse('[1,2,3,]', allow_trailing_comma: true))
assert_raise(JSON::ParserError) { parse('[1,2,3,]', allow_trailing_comma: false) }
assert_equal([1, 2, 3], parse('[ 1 , 2 , 3 ]', allow_trailing_comma: true))
assert_equal([1, 2, 3], parse('[ 1 , 2 , 3 ]', allow_trailing_comma: false))
assert_equal([1, 2, 3], parse('[ 1 , 2 , 3 , ]', allow_trailing_comma: true))
assert_raise(JSON::ParserError) { parse('[ 1 , 2 , 3 , ]', allow_trailing_comma: false) }
assert_equal({'foo' => [1, 2, 3]}, parse('{ "foo": [1,2,3] }', allow_trailing_comma: true))
assert_equal({'foo' => [1, 2, 3]}, parse('{ "foo": [1,2,3] }', allow_trailing_comma: false))
assert_equal({'foo' => [1, 2, 3]}, parse('{ "foo": [1,2,3,] }', allow_trailing_comma: true))
assert_raise(JSON::ParserError) { parse('{ "foo": [1,2,3,] }', allow_trailing_comma: false) }
end
def test_parse_object_with_allow_trailing_comma
assert_equal({}, parse('{}', allow_trailing_comma: true))
assert_equal({}, parse('{}', allow_trailing_comma: false))
assert_raise(JSON::ParserError) { parse('{,}', allow_trailing_comma: true) }
assert_raise(JSON::ParserError) { parse('{,}', allow_trailing_comma: false) }
assert_equal({'foo'=>'bar'}, parse('{"foo":"bar"}', allow_trailing_comma: true))
assert_equal({'foo'=>'bar'}, parse('{"foo":"bar"}', allow_trailing_comma: false))
assert_equal({'foo'=>'bar'}, parse('{"foo":"bar",}', allow_trailing_comma: true))
assert_raise(JSON::ParserError) { parse('{"foo":"bar",}', allow_trailing_comma: false) }
assert_equal(
{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'},
parse('{"foo":"bar","baz":"qux","quux":"garply"}', allow_trailing_comma: true)
)
assert_equal(
{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'},
parse('{"foo":"bar","baz":"qux","quux":"garply"}', allow_trailing_comma: false)
)
assert_equal(
{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'},
parse('{"foo":"bar","baz":"qux","quux":"garply",}', allow_trailing_comma: true)
)
assert_raise(JSON::ParserError) {
parse('{"foo":"bar","baz":"qux","quux":"garply",}', allow_trailing_comma: false)
}
assert_equal(
{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'},
parse('{ "foo":"bar" , "baz":"qux" , "quux":"garply" }', allow_trailing_comma: true)
)
assert_equal(
{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'},
parse('{ "foo":"bar" , "baz":"qux" , "quux":"garply" }', allow_trailing_comma: false)
)
assert_equal(
{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'},
parse('{ "foo":"bar" , "baz":"qux" , "quux":"garply" , }', allow_trailing_comma: true)
)
assert_raise(JSON::ParserError) {
parse('{ "foo":"bar" , "baz":"qux" , "quux":"garply" , }', allow_trailing_comma: false)
}
assert_equal(
[{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'}],
parse('[{"foo":"bar","baz":"qux","quux":"garply"}]', allow_trailing_comma: true)
)
assert_equal(
[{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'}],
parse('[{"foo":"bar","baz":"qux","quux":"garply"}]', allow_trailing_comma: false)
)
assert_equal(
[{'foo'=>'bar', 'baz'=>'qux', 'quux'=>'garply'}],
parse('[{"foo":"bar","baz":"qux","quux":"garply",}]', allow_trailing_comma: true)
)
assert_raise(JSON::ParserError) {
parse('[{"foo":"bar","baz":"qux","quux":"garply",}]', allow_trailing_comma: false)
}
end
def test_parse_some_strings
assert_equal([""], parse('[""]'))
assert_equal(["\\"], parse('["\\\\"]'))
assert_equal(['"'], parse('["\""]'))
assert_equal(['\\"\\'], parse('["\\\\\\"\\\\"]'))
assert_equal(
["\"\b\n\r\t\0\037"],
parse('["\"\b\n\r\t\u0000\u001f"]')
)
end
if RUBY_ENGINE != "jruby" # https://github.com/ruby/json/issues/138
def test_parse_broken_string
s = parse(%{["\x80"]})[0]
assert_equal("\x80", s)
assert_equal Encoding::UTF_8, s.encoding
assert_equal false, s.valid_encoding?
s = parse(%{["\x80"]}.b)[0]
assert_equal("\x80", s)
assert_equal Encoding::UTF_8, s.encoding
assert_equal false, s.valid_encoding?
input = %{["\x80"]}.dup.force_encoding(Encoding::US_ASCII)
assert_raise(Encoding::InvalidByteSequenceError) { parse(input) }
end
end
def test_invalid_unicode_escape
assert_raise(JSON::ParserError) { parse('"\u"') }
assert_raise(JSON::ParserError) { parse('"\ua"') }
assert_raise(JSON::ParserError) { parse('"\uaa"') }
assert_raise(JSON::ParserError) { parse('"\uaaa"') }
assert_equal "\uaaaa", parse('"\uaaaa"')
assert_raise(JSON::ParserError) { parse('"\u______"') }
assert_raise(JSON::ParserError) { parse('"\u1_____"') }
assert_raise(JSON::ParserError) { parse('"\u11____"') }
assert_raise(JSON::ParserError) { parse('"\u111___"') }
end
def test_unicode_followed_by_newline
# Ref: https://github.com/ruby/json/issues/912
assert_equal "🌌\n".bytes, JSON.parse('"\ud83c\udf0c\n"').bytes
assert_equal "🌌\n", JSON.parse('"\ud83c\udf0c\n"')
assert_predicate JSON.parse('"\ud83c\udf0c\n"'), :valid_encoding?
end
def test_invalid_surogates
assert_raise(JSON::ParserError) { parse('"\\uD800"') }
assert_raise(JSON::ParserError) { parse('"\\uD800_________________"') }
assert_raise(JSON::ParserError) { parse('"\\uD800\\u0041"') }
assert_raise(JSON::ParserError) { parse('"\\uD800\\u004') }
end
def test_parse_big_integers
json1 = JSON(orig = (1 << 31) - 1)
assert_equal orig, parse(json1)
json2 = JSON(orig = 1 << 31)
assert_equal orig, parse(json2)
json3 = JSON(orig = (1 << 62) - 1)
assert_equal orig, parse(json3)
json4 = JSON(orig = 1 << 62)
assert_equal orig, parse(json4)
json5 = JSON(orig = 1 << 64)
assert_equal orig, parse(json5)
end
def test_parse_escaped_key
doc = {
"test\r1" => 1,
"entries" => [
"test\t2" => 2,
"test\n3" => 3,
]
}
assert_equal doc, parse(JSON.generate(doc))
end
def test_parse_duplicate_key
expected = {"a" => 2}
expected_sym = {a: 2}
assert_equal expected, parse('{"a": 1, "a": 2}', allow_duplicate_key: true)
assert_raise(ParserError) { parse('{"a": 1, "a": 2}', allow_duplicate_key: false) }
assert_raise(ParserError) { parse('{"a": 1, "a": 2}', allow_duplicate_key: false, symbolize_names: true) }
assert_deprecated_warning(/duplicate key "a"/) do
assert_equal expected, parse('{"a": 1, "a": 2}')
end
assert_deprecated_warning(/duplicate key "a"/) do
assert_equal expected_sym, parse('{"a": 1, "a": 2}', symbolize_names: true)
end
if RUBY_ENGINE == 'ruby'
assert_deprecated_warning(/#{File.basename(__FILE__)}\:#{__LINE__ + 1}/) do
assert_equal expected, parse('{"a": 1, "a": 2}')
end
end
unless RUBY_ENGINE == 'jruby'
assert_raise(ParserError) do
fake_key = Object.new
JSON.load('{"a": 1, "a": 2}', -> (obj) { obj == "a" ? fake_key : obj }, allow_duplicate_key: false)
end
assert_deprecated_warning(/duplicate key #<Object:0x/) do
fake_key = Object.new
JSON.load('{"a": 1, "a": 2}', -> (obj) { obj == "a" ? fake_key : obj })
end
end
end
def test_some_wrong_inputs
assert_raise(ParserError) { parse('[] bla') }
assert_raise(ParserError) { parse('[] 1') }
assert_raise(ParserError) { parse('[] []') }
assert_raise(ParserError) { parse('[] {}') }
assert_raise(ParserError) { parse('{} []') }
assert_raise(ParserError) { parse('{} {}') }
assert_raise(ParserError) { parse('[NULL]') }
assert_raise(ParserError) { parse('[FALSE]') }
assert_raise(ParserError) { parse('[TRUE]') }
assert_raise(ParserError) { parse('[07] ') }
assert_raise(ParserError) { parse('[0a]') }
assert_raise(ParserError) { parse('[1.]') }
assert_raise(ParserError) { parse(' ') }
end
def test_symbolize_names
assert_equal({ "foo" => "bar", "baz" => "quux" },
parse('{"foo":"bar", "baz":"quux"}'))
assert_equal({ :foo => "bar", :baz => "quux" },
parse('{"foo":"bar", "baz":"quux"}', :symbolize_names => true))
assert_raise(ArgumentError) do
parse('{}', :symbolize_names => true, :create_additions => true)
end
end
def test_freeze
assert_predicate parse('{}', :freeze => true), :frozen?
assert_predicate parse('[]', :freeze => true), :frozen?
assert_predicate parse('"foo"', :freeze => true), :frozen?
assert_same(-'foo', parse('"foo"', :freeze => true))
assert_same(-'foo', parse('{"foo": 1}', :freeze => true).keys.first)
end
def test_parse_comments
json = <<~JSON
{
"key1":"value1", // eol comment
"key2":"value2" /* multi line
* comment */,
"key3":"value3" /* multi line
// nested eol comment
* comment */
}
JSON
assert_equal(
{ "key1" => "value1", "key2" => "value2", "key3" => "value3" },
parse(json))
json = <<~JSON
{
"key1":"value1" /* multi line
// nested eol comment
/* illegal nested multi line comment */
* comment */
}
JSON
assert_raise(ParserError) { parse(json) }
json = <<~JSON
{
"key1":"value1" /* multi line
// nested eol comment
/* legal nested multi line comment start sequence */
}
JSON
assert_equal({ "key1" => "value1" }, parse(json))
json = <<~JSON
{
"key1":"value1" /* multi line
// nested eol comment
closed multi comment */
and again, throw an Error */
}
JSON
assert_raise(ParserError) { parse(json) }
json = <<~JSON
{
"key1":"value1" /*/*/
}
JSON
assert_equal({ "key1" => "value1" }, parse(json))
assert_equal({}, parse('{} /**/'))
assert_raise(ParserError) { parse('{} /* comment not closed') }
assert_raise(ParserError) { parse('{} /*/') }
assert_raise(ParserError) { parse('{} /x wrong comment') }
assert_raise(ParserError) { parse('{} /') }
end
def test_nesting
too_deep = '[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[["Too deep"]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]'
too_deep_ary = eval too_deep
assert_raise(JSON::NestingError) { parse too_deep }
assert_raise(JSON::NestingError) { parse too_deep, :max_nesting => 100 }
ok = parse too_deep, :max_nesting => 101
assert_equal too_deep_ary, ok
ok = parse too_deep, :max_nesting => nil
assert_equal too_deep_ary, ok
ok = parse too_deep, :max_nesting => false
assert_equal too_deep_ary, ok
ok = parse too_deep, :max_nesting => 0
assert_equal too_deep_ary, ok
end
def test_backslash
data = [ '\\.(?i:gif|jpe?g|png)$' ]
json = '["\\\\.(?i:gif|jpe?g|png)$"]'
assert_equal data, parse(json)
#
data = [ '\\"' ]
json = '["\\\\\""]'
assert_equal data, parse(json)
#
json = '["/"]'
data = [ '/' ]
assert_equal data, parse(json)
#
json = '["\""]'
data = ['"']
assert_equal data, parse(json)
#
json = '["\\/"]'
data = ["/"]
assert_equal data, parse(json)
json = '["\/"]'
data = [ '/' ]
assert_equal data, parse(json)
data = ['"""""""""""""""""""""""""']
json = '["\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\"\""]'
assert_equal data, parse(json)
data = '["This is a "test" of the emergency broadcast system."]'
json = "\"[\\\"This is a \\\"test\\\" of the emergency broadcast system.\\\"]\""
assert_equal data, parse(json)
data = '\tThis is a test of the emergency broadcast system.'
json = "\"\\\\tThis is a test of the emergency broadcast system.\""
assert_equal data, parse(json)
data = 'This\tis a test of the emergency broadcast system.'
json = "\"This\\\\tis a test of the emergency broadcast system.\""
assert_equal data, parse(json)
data = 'This is\ta test of the emergency broadcast system.'
json = "\"This is\\\\ta test of the emergency broadcast system.\""
assert_equal data, parse(json)
data = 'This is a test of the emergency broadcast\tsystem.'
json = "\"This is a test of the emergency broadcast\\\\tsystem.\""
assert_equal data, parse(json)
data = 'This is a test of the emergency broadcast\tsystem.\n'
json = "\"This is a test of the emergency broadcast\\\\tsystem.\\\\n\""
assert_equal data, parse(json)
data = '"' * 15
json = "\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\""
assert_equal data, parse(json)
data = "\"\"\"\"\"\"\"\"\"\"\"\"\"\"a"
json = "\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"\\\"a\""
assert_equal data, parse(json)
data = "\u0001\u0001\u0001\u0001"
json = "\"\\u0001\\u0001\\u0001\\u0001\""
assert_equal data, parse(json)
data = "\u0001a\u0001a\u0001a\u0001a"
json = "\"\\u0001a\\u0001a\\u0001a\\u0001a\""
assert_equal data, parse(json)
data = "\u0001aa\u0001aa"
json = "\"\\u0001aa\\u0001aa\""
assert_equal data, parse(json)
data = "\u0001aa\u0001aa\u0001aa"
json = "\"\\u0001aa\\u0001aa\\u0001aa\""
assert_equal data, parse(json)
data = "\u0001aa\u0001aa\u0001aa\u0001aa\u0001aa\u0001aa"
json = "\"\\u0001aa\\u0001aa\\u0001aa\\u0001aa\\u0001aa\\u0001aa\""
assert_equal data, parse(json)
data = "\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002\u0001a\u0002"
json = "\"\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\\u0001a\\u0002\""
assert_equal data, parse(json)
data = "ab\u0002c"
json = "\"ab\\u0002c\""
assert_equal data, parse(json)
data = "ab\u0002cab\u0002cab\u0002cab\u0002c"
json = "\"ab\\u0002cab\\u0002cab\\u0002cab\\u0002c\""
assert_equal data, parse(json)
data = "ab\u0002cab\u0002cab\u0002cab\u0002cab\u0002cab\u0002c"
json = "\"ab\\u0002cab\\u0002cab\\u0002cab\\u0002cab\\u0002cab\\u0002c\""
assert_equal data, parse(json)
data = "\n\t\f\b\n\t\f\b\n\t\f\b\n\t\f"
json = "\"\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\""
assert_equal data, parse(json)
data = "\n\t\f\b\n\t\f\b\n\t\f\b\n\t\f\b"
json = "\"\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\""
assert_equal data, parse(json)
data = "a\n\t\f\b\n\t\f\b\n\t\f\b\n\t"
json = "\"a\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\\f\\b\\n\\t\""
assert_equal data, parse(json)
end
class SubArray < Array
def <<(v)
@shifted = true
super
end
def shifted?
@shifted
end
end
class SubArray2 < Array
def to_json(*a)
{
JSON.create_id => self.class.name,
'ary' => to_a,
}.to_json(*a)
end
def self.json_create(o)
o.delete JSON.create_id
o['ary']
end
end
class SubArrayWrapper
def initialize
@data = []
end
attr_reader :data
def [](index)
@data[index]
end
def <<(value)
@data << value
@shifted = true
end
def shifted?
@shifted
end
end
def test_parse_array_custom_array_derived_class
res = parse('[1,2]', :array_class => SubArray)
assert_equal([1,2], res)
assert_equal(SubArray, res.class)
assert res.shifted?
end
def test_parse_array_custom_non_array_derived_class
res = parse('[1,2]', :array_class => SubArrayWrapper)
assert_equal([1,2], res.data)
assert_equal(1, res[0])
assert_equal(SubArrayWrapper, res.class)
assert res.shifted?
end
def test_parse_object
assert_equal({}, parse('{}'))
assert_equal({}, parse(' { } '))
assert_equal({'foo'=>'bar'}, parse('{"foo":"bar"}'))
assert_equal({'foo'=>'bar'}, parse(' { "foo" : "bar" } '))
end
class SubHash < Hash
def []=(k, v)
@item_set = true
super
end
def item_set?
@item_set
end
end
class SubHash2 < Hash
def to_json(*a)
{
JSON.create_id => self.class.name,
}.merge(self).to_json(*a)
end
def self.json_create(o)
o.delete JSON.create_id
self[o]
end
end
def test_parse_object_custom_hash_derived_class
res = parse('{"foo":"bar"}', :object_class => SubHash)
assert_equal({"foo" => "bar"}, res)
assert_equal(SubHash, res.class)
assert res.item_set?
end
if defined?(::OpenStruct)
class SubOpenStruct < OpenStruct
def [](k)
__send__(k)
end
def []=(k, v)
@item_set = true
__send__("#{k}=", v)
end
def item_set?
@item_set
end
end
def test_parse_object_custom_non_hash_derived_class
res = parse('{"foo":"bar"}', :object_class => SubOpenStruct)
assert_equal "bar", res.foo
assert_equal "bar", res[:foo]
assert_equal(SubOpenStruct, res.class)
assert res.item_set?
end
def test_parse_generic_object
res = parse(
'{"foo":"bar", "baz":{}}',
:object_class => JSON::GenericObject
)
assert_equal(JSON::GenericObject, res.class)
assert_equal "bar", res.foo
assert_equal "bar", res["foo"]
assert_equal "bar", res[:foo]
assert_equal "bar", res.to_hash[:foo]
assert_equal(JSON::GenericObject, res.baz.class)
end
end
def test_generate_core_subclasses_with_new_to_json
obj = SubHash2["foo" => SubHash2["bar" => true]]
obj_json = JSON(obj)
obj_again = parse(obj_json, :create_additions => true)
assert_kind_of SubHash2, obj_again
assert_kind_of SubHash2, obj_again['foo']
assert obj_again['foo']['bar']
assert_equal obj, obj_again
assert_equal ["foo"],
JSON(JSON(SubArray2["foo"]), :create_additions => true)
end
def test_generate_core_subclasses_with_default_to_json
assert_equal '{"foo":"bar"}', JSON(SubHash["foo" => "bar"])
assert_equal '["foo"]', JSON(SubArray["foo"])
end
def test_generate_of_core_subclasses
obj = SubHash["foo" => SubHash["bar" => true]]
obj_json = JSON(obj)
obj_again = JSON(obj_json)
assert_kind_of Hash, obj_again
assert_kind_of Hash, obj_again['foo']
assert obj_again['foo']['bar']
assert_equal obj, obj_again
end
def test_parsing_frozen_ascii8bit_string
assert_equal(
{ 'foo' => 'bar' },
JSON('{ "foo": "bar" }'.b.freeze)
)
end
def test_parse_error_message_length
# Error messages aren't consistent across backends, but we can at least
# enforce that if they include fragments of the source it should be of
# reasonable size.
error = assert_raise(JSON::ParserError) do
JSON.parse('{"foo": ' + ('A' * 500) + '}')
end
assert_operator 80, :>, error.message.bytesize
end
def test_parse_error_incomplete_hash
error = assert_raise(JSON::ParserError) do
JSON.parse('{"input":{"firstName":"Bob","lastName":"Mob","email":"bob@example.com"}')
end
if RUBY_ENGINE == "ruby"
assert_equal %(expected ',' or '}' after object value, got: EOF at line 1 column 72), error.message
end
end
def test_parse_error_snippet
omit "C ext only test" unless RUBY_ENGINE == "ruby"
error = assert_raise(JSON::ParserError) { JSON.parse("あああああああああああああああああああああああ") }
assert_equal "unexpected character: 'ああああああああああ' at line 1 column 1", error.message
error = assert_raise(JSON::ParserError) { JSON.parse("aあああああああああああああああああああああああ") }
assert_equal "unexpected character: 'aああああああああああ' at line 1 column 1", error.message
error = assert_raise(JSON::ParserError) { JSON.parse("abあああああああああああああああああああああああ") }
assert_equal "unexpected character: 'abあああああああああ' at line 1 column 1", error.message
error = assert_raise(JSON::ParserError) { JSON.parse("abcあああああああああああああああああああああああ") }
assert_equal "unexpected character: 'abcあああああああああ' at line 1 column 1", error.message
end
def test_parse_leading_slash
# ref: https://github.com/ruby/ruby/pull/12598
assert_raise(JSON::ParserError) do
JSON.parse("/foo/bar")
end
end
def test_parse_whitespace_after_newline
assert_equal [], JSON.parse("[\n#{' ' * (8 + 8 + 4 + 3)}]")
end
def test_frozen
parser_config = JSON::Parser::Config.new({}).freeze
assert_raise FrozenError do
parser_config.send(:initialize, {})
end
end
private
def assert_equal_float(expected, actual, delta = 1e-2)
Array === expected and expected = expected.first
Array === actual and actual = actual.first
assert_in_delta(expected, actual, delta)
end
end