From 8618ecfe6a6fd59157b1b2b65db1ffbd828c4616 Mon Sep 17 00:00:00 2001 From: Shaisolaris <66793913+Shaisolaris@users.noreply.github.com> Date: Mon, 12 Oct 2026 04:06:44 -0700 Subject: [PATCH] Detect RFC 8785 duplicate keys that differ only in encoding --- lib/json/ext.rb | 16 +++++++++++++--- test/json/json_generator_test.rb | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/lib/json/ext.rb b/lib/json/ext.rb index 7d7b09b27..c7a1c1731 100644 --- a/lib/json/ext.rb +++ b/lib/json/ext.rb @@ -101,12 +101,22 @@ def shareable_lambda(block) # :nodoc: }) generator::State.rfc8785_sort_keys_proc = shareable_lambda(->(hash, as_json) { - if as_json + # Keys are converted when an as_json hook is given, or when a String or + # Symbol key is not UTF-8, since it can then match another key once both + # are written as UTF-8. + needs_conversion = as_json || hash.each_key.any? do |key| + next false unless String === key || Symbol === key + string = key.to_s + !(string.ascii_only? || (string.encoding == Encoding::UTF_8 && string.valid_encoding?)) + end + if needs_conversion converted = {} hash.each do |key, value| string = (String === key || Symbol === key) ? key.to_s : nil - unless string && (string.ascii_only? || (string.encoding == Encoding::UTF_8 && string.valid_encoding?)) - key = as_json.call(key, true) + if !as_json && !string + # Other keys are converted with #to_s by the generator. + elsif !string || !(string.ascii_only? || (string.encoding == Encoding::UTF_8 && string.valid_encoding?)) + key = as_json ? as_json.call(key, true) : string unless String === key || Symbol === key raise GeneratorError.new("#{key.class} not allowed as object key in JSON", key) end diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 7cf76c958..f80ea8ed6 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -1316,6 +1316,20 @@ def key.to_s assert_equal '{"1":1,"b":2}', JSON.generate({ 'b' => 2, 1 => 1 }, options.merge(strict: false)) end + def test_rfc8785_duplicate_keys_in_different_encodings + latin1 = "\u00e9".encode(Encoding::ISO_8859_1) + hash = { latin1 => 1, "\u00e9" => 2 } + assert_equal 2, hash.size + + error = assert_raise JSON::GeneratorError do + JSON.generate(hash, rfc8785: true) + end + assert_includes error.message, "detected duplicate key" + + assert_rfc8785 "{\"\u00e9\":1}", { latin1 => 1 } + assert_rfc8785 "{\"a\":1,\"\u00e9\":2}", { latin1 => 2, "a" => 1 } + end + def test_rfc8785_key_order_with_default_external_encoding # https://www.rfc-editor.org/rfc/rfc8785.html#section-3.2.3 hash = { "\u20ac" => 1, "\r" => 2, "\ufb33" => 3, '1' => 4,