Skip to content

Commit 88b358d

Browse files
ydahbyroot
authored andcommitted
Sort RFC 8785 keys after as_json conversion
1 parent cdcfdb8 commit 88b358d

6 files changed

Lines changed: 103 additions & 11 deletions

File tree

‎ext/json/ext/generator/generator.c‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1051,7 +1051,8 @@ static inline long increase_depth(struct generate_json_data *data)
10511051
static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
10521052
{
10531053
if (RB_UNLIKELY(data->state->rfc8785)) {
1054-
obj = rb_proc_call_with_block(rfc8785_sort_keys_proc, 1, &obj, Qnil);
1054+
VALUE args[2] = {obj, data->state->strict ? data->state->as_json : Qnil};
1055+
obj = rb_proc_call_with_block(rfc8785_sort_keys_proc, 2, args, Qnil);
10551056
Check_Type(obj, T_HASH);
10561057
} else if (RB_UNLIKELY(data->state->sort_keys)) {
10571058
obj = rb_proc_call_with_block(data->state->sort_keys, 1, &obj, Qnil);

‎java/src/json/ext/Generator.java‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -588,13 +588,11 @@ static void generateHash(ThreadContext context, Session session, RubyHash object
588588
return;
589589
}
590590

591-
RubyProc sortKeysProc = state.getSortKeysProc();
592591
if (state.rfc8785()) {
593-
sortKeysProc = (RubyProc)GeneratorState.rfc8785SortKeysProc;
594-
}
595-
596-
if (sortKeysProc != null) {
597-
object = (RubyHash)Helpers.invoke(context, sortKeysProc, "call", object);
592+
IRubyObject asJSON = state.strict() ? state.as_json_get(context) : context.nil;
593+
object = (RubyHash)Helpers.invoke(context, GeneratorState.rfc8785SortKeysProc, "call", object, asJSON);
594+
} else if (state.getSortKeysProc() != null) {
595+
object = (RubyHash)Helpers.invoke(context, state.getSortKeysProc(), "call", object);
598596
}
599597

600598
final ByteList objectNl = state.getObjectNl();

‎lib/json/ext.rb‎

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,33 @@ def shareable_lambda(block) # :nodoc:
100100
end
101101
})
102102

103-
generator::State.rfc8785_sort_keys_proc = shareable_lambda(->(hash) {
103+
generator::State.rfc8785_sort_keys_proc = shareable_lambda(->(hash, as_json) {
104+
if as_json
105+
converted = {}
106+
hash.each do |key, value|
107+
string = (String === key || Symbol === key) ? key.to_s : nil
108+
unless string && (string.ascii_only? || (string.encoding == Encoding::UTF_8 && string.valid_encoding?))
109+
key = as_json.call(key, true)
110+
unless String === key || Symbol === key
111+
raise GeneratorError.new("#{key.class} not allowed as object key in JSON", key)
112+
end
113+
begin
114+
string = key.to_s.encode(Encoding::UTF_8)
115+
rescue EncodingError => error
116+
raise GeneratorError.new(error.message, key)
117+
end
118+
unless string.valid_encoding?
119+
raise GeneratorError.new("source sequence is illegal/malformed utf-8", key)
120+
end
121+
key = string
122+
end
123+
if converted.key?(key)
124+
raise GeneratorError.new("detected duplicate key #{key.inspect}", key)
125+
end
126+
converted[key] = value
127+
end
128+
hash = converted
129+
end
104130
hash.sort_by { |k,| k.to_s.encode(Encoding::UTF_16) }.to_h
105131
})
106132

‎lib/json/truffle_ruby/generator.rb‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ def generate(obj, anIO = nil)
445445
first = true
446446
key_type = nil
447447
if @rfc8785
448-
obj = State.rfc8785_sort_keys_proc.call(obj)
448+
obj = State.rfc8785_sort_keys_proc.call(obj, @strict && @as_json)
449449
elsif @sort_keys
450450
obj = @sort_keys.call(obj)
451451
end
@@ -585,7 +585,7 @@ def json_transform(state)
585585
hash = self
586586

587587
if state.rfc8785?
588-
hash = State.rfc8785_sort_keys_proc.call(hash)
588+
hash = State.rfc8785_sort_keys_proc.call(hash, state.strict? && state.as_json)
589589
elsif state.sort_keys
590590
hash = state.sort_keys.call(hash)
591591
end
@@ -596,7 +596,7 @@ def json_transform(state)
596596
else
597597
if key_type && !state.allow_duplicate_key? && key_type != key.class
598598
key_type = nil # stop checking
599-
JSON.send(:on_mixed_keys_hash, self)
599+
JSON.send(:on_mixed_keys_hash, hash)
600600
end
601601
result << delim
602602
end

‎test/json/json_coder_test.rb‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,54 @@ def test_json_coder_hash_key
6464
assert_equal "Integer not allowed as object key in JSON", error.message
6565
end
6666

67+
def test_json_coder_rfc8785_converted_keys
68+
time = Time.utc(1999)
69+
calls = []
70+
coder = JSON::Coder.new(rfc8785: true) do |object, is_key|
71+
calls << [object, is_key]
72+
object.year.to_s
73+
end
74+
input = { time => 1, '1999!' => 2, Time.utc(2000) => 3 }
75+
assert_equal '{"1999":1,"1999!":2,"2000":3}', coder.dump(input)
76+
assert_equal [[time, true], [Time.utc(2000), true]], calls
77+
assert_equal [time, '1999!', Time.utc(2000)], input.keys
78+
end
79+
80+
def test_json_coder_rfc8785_converted_key_validation
81+
object = Object.new
82+
coder = JSON::Coder.new(rfc8785: true) { 42 }
83+
error = assert_raise(JSON::GeneratorError) { coder.dump(object => 1) }
84+
assert_equal 'Integer not allowed as object key in JSON', error.message
85+
assert_equal 42, error.invalid_object
86+
87+
coder = JSON::Coder.new(rfc8785: true) { :a }
88+
assert_equal '{"a":1,"b":2}', coder.dump('b' => 2, object => 1)
89+
90+
invalid = "\xFF"
91+
calls = 0
92+
coder = JSON::Coder.new(rfc8785: true) { calls += 1; invalid }
93+
error = assert_raise(JSON::GeneratorError) { coder.dump(object => 1) }
94+
assert_same invalid, error.invalid_object
95+
assert_equal 1, calls
96+
end
97+
98+
def test_json_coder_rfc8785_converted_key_collision
99+
coder = JSON::Coder.new(rfc8785: true) { 'a' }
100+
assert_raise(JSON::GeneratorError) { coder.dump(Object.new => 1, 'a' => 2) }
101+
assert_raise(JSON::GeneratorError) { coder.dump(Object.new => 1, Object.new => 2) }
102+
assert_raise(JSON::GeneratorError) { coder.dump(Object.new => 1, :a => 2) }
103+
end
104+
105+
def test_json_coder_rfc8785_invalid_encoding_key
106+
calls = []
107+
coder = JSON::Coder.new(rfc8785: true) do |object, is_key|
108+
calls << [object, is_key]
109+
'a'
110+
end
111+
assert_equal '{"a":1,"b":2}', coder.dump('b' => 2, "\xFF" => 1)
112+
assert_equal [["\xFF", true]], calls
113+
end
114+
67115
def test_json_coder_options
68116
coder = JSON::Coder.new(array_nl: "\n") do |object|
69117
42

‎test/json/json_generator_test.rb‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1297,6 +1297,25 @@ def test_rfc8785_mixed_key_order
12971297
assert_rfc8785 '{"a":1,"b":2}', { a: 1, 'b' => 2 }
12981298
end
12991299

1300+
def test_rfc8785_key_order_with_as_json
1301+
key = Object.new
1302+
def key.to_s
1303+
raise 'must not sort the original key'
1304+
end
1305+
as_json = ->(object, is_key) { is_key ? 'a' : object }
1306+
options = { rfc8785: true, strict: true, as_json: as_json }
1307+
object = [{ 'b' => 2, key => 1 }]
1308+
expected = '[{"a":1,"b":2}]'
1309+
assert_equal expected, JSON.generate(object, options)
1310+
assert_equal expected, object.to_json(options)
1311+
state = JSON::State.new(options)
1312+
io = StringIO.new
1313+
state.generate(object, io)
1314+
assert_equal expected, io.string
1315+
1316+
assert_equal '{"1":1,"b":2}', JSON.generate({ 'b' => 2, 1 => 1 }, options.merge(strict: false))
1317+
end
1318+
13001319
def test_rfc8785_key_order_with_default_external_encoding
13011320
# https://www.rfc-editor.org/rfc/rfc8785.html#section-3.2.3
13021321
hash = { "\u20ac" => 1, "\r" => 2, "\ufb33" => 3, '1' => 4,

0 commit comments

Comments
 (0)