Skip to content

Commit f103c89

Browse files
committed
Implement rfc8785
Fix: #1038 Directly lift https://github.com/dryruby/json-canonicalization At least for now, implementing most of the logic in Ruby make it shareable between the 3 generator implementations, largely reducing the maintenance cost. I don't particularly expect canonicalization to be a hotspot, but if it happens to be, I'd be open to specialize the implementation, within reason, at least in the C generator.
1 parent 590dd54 commit f103c89

20 files changed

Lines changed: 292 additions & 22 deletions

File tree

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

Lines changed: 74 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,14 +27,16 @@ typedef struct JSON_Generator_StateStruct {
2727
bool ascii_only;
2828
bool script_safe;
2929
bool strict;
30+
bool rfc8785;
3031
VALUE sort_keys;
3132
} JSON_Generator_State;
3233

33-
static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc;
34+
static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc,
35+
rfc8785_number_formater_proc, rfc8785_sort_keys_proc;
3436

3537
static ID i_to_s, i_to_json, i_new, i_encode;
3638
static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan, sym_allow_duplicate_key,
37-
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_strict, sym_as_json, sym_sort_keys;
39+
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_strict, sym_as_json, sym_sort_keys, sym_rfc8785;
3840

3941

4042
#define GET_STATE_TO(self, state) \
@@ -1052,7 +1054,10 @@ static inline long increase_depth(struct generate_json_data *data)
10521054

10531055
static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
10541056
{
1055-
if (RB_UNLIKELY(data->state->sort_keys)) {
1057+
if (RB_UNLIKELY(data->state->rfc8785)) {
1058+
obj = rb_proc_call_with_block(rfc8785_sort_keys_proc, 1, &obj, Qnil);
1059+
Check_Type(obj, T_HASH);
1060+
} else if (RB_UNLIKELY(data->state->sort_keys)) {
10561061
obj = rb_proc_call_with_block(data->state->sort_keys, 1, &obj, Qnil);
10571062
Check_Type(obj, T_HASH);
10581063
}
@@ -1180,6 +1185,13 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data
11801185
return;
11811186
}
11821187

1188+
if (RB_UNLIKELY(data->state->rfc8785)) {
1189+
VALUE str = rb_proc_call_with_block(rfc8785_number_formater_proc, 1, &obj, Qnil);
1190+
Check_Type(str, T_STRING);
1191+
fbuffer_append_str(buffer, str);
1192+
return;
1193+
}
1194+
11831195
/* This implementation writes directly into the buffer. We reserve
11841196
* the 32 characters that fpconv_dtoa states as its maximum.
11851197
*/
@@ -1757,6 +1769,28 @@ static VALUE cState_set_default_sort_keys_proc(VALUE self, VALUE proc)
17571769
return default_sort_keys_proc = proc;
17581770
}
17591771

1772+
static VALUE cState_set_rfc8785_number_formater_proc(VALUE self, VALUE proc)
1773+
{
1774+
if (!rb_obj_is_proc(proc)) {
1775+
rb_raise(rb_eTypeError, "rfc8785_number_formater_proc must be a Proc");
1776+
}
1777+
if (rfc8785_number_formater_proc) {
1778+
rb_raise(rb_eArgError, "rfc8785_number_formater_proc can only be set once");
1779+
}
1780+
return rfc8785_number_formater_proc = proc;
1781+
}
1782+
1783+
static VALUE cState_set_rfc8785_sort_keys_proc(VALUE self, VALUE proc)
1784+
{
1785+
if (!rb_obj_is_proc(proc)) {
1786+
rb_raise(rb_eTypeError, "rfc8785_sort_keys_proc must be a Proc");
1787+
}
1788+
if (rfc8785_sort_keys_proc) {
1789+
rb_raise(rb_eArgError, "rfc8785_sort_keys_proc can only be set once");
1790+
}
1791+
return rfc8785_sort_keys_proc = proc;
1792+
}
1793+
17601794
static VALUE normalize_sort_keys(VALUE value)
17611795
{
17621796
if (rb_obj_is_proc(value)) {
@@ -1775,7 +1809,7 @@ static VALUE normalize_sort_keys(VALUE value)
17751809
*
17761810
* Get the value of sort_keys.
17771811
*/
1778-
static VALUE cState_sort_keys_p(VALUE self)
1812+
static VALUE cState_sort_keys(VALUE self)
17791813
{
17801814
GET_STATE(self);
17811815
return state->sort_keys;
@@ -1795,7 +1829,33 @@ static VALUE cState_sort_keys_set(VALUE self, VALUE value)
17951829
rb_check_frozen(self);
17961830
GET_STATE(self);
17971831
RB_OBJ_WRITE(self, &state->sort_keys, normalize_sort_keys(value));
1798-
return Qnil;
1832+
return state->sort_keys;
1833+
}
1834+
1835+
1836+
/*
1837+
* call-seq: rfc8785?
1838+
*
1839+
* Get whether RFC8785 (canonicalization) is enabled.
1840+
*/
1841+
static VALUE cState_rfc8785_p(VALUE self)
1842+
{
1843+
GET_STATE(self);
1844+
return state->rfc8785 ? Qtrue : Qfalse;
1845+
}
1846+
1847+
/*
1848+
* call-seq: rfc8785=
1849+
*
1850+
* Sets whether RFC8785 (canonicalization) is enabled.
1851+
*/
1852+
1853+
static VALUE cState_rfc8785_set(VALUE self, VALUE value)
1854+
{
1855+
rb_check_frozen(self);
1856+
GET_STATE(self);
1857+
state->rfc8785 = RTEST(value);
1858+
return state->rfc8785 ? Qtrue : Qfalse;
17991859
}
18001860

18011861
static VALUE cState_allow_duplicate_key_p(VALUE self)
@@ -1895,6 +1955,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
18951955
else if (key == sym_script_safe) { state->script_safe = RTEST(val); }
18961956
else if (key == sym_strict) { state->strict = RTEST(val); }
18971957
else if (key == sym_allow_duplicate_key) { state->allow_duplicate_key = RTEST(val); }
1958+
else if (key == sym_rfc8785) { state->rfc8785 = RTEST(val); }
18981959
else if (key == sym_as_json) {
18991960
VALUE proc = RTEST(val) ? as_json_config(val) : Qfalse;
19001961
state->as_json_single_arg = proc && rb_proc_arity(proc) == 1;
@@ -1990,6 +2051,8 @@ void Init_generator(void)
19902051
VALUE mGenerator = rb_define_module_under(mExt, "Generator");
19912052

19922053
rb_global_variable(&default_sort_keys_proc);
2054+
rb_global_variable(&rfc8785_number_formater_proc);
2055+
rb_global_variable(&rfc8785_sort_keys_proc);
19932056

19942057
rb_global_variable(&eGeneratorError);
19952058
eGeneratorError = rb_path2class("JSON::GeneratorError");
@@ -2001,6 +2064,8 @@ void Init_generator(void)
20012064
rb_define_alloc_func(cState, cState_s_allocate);
20022065
rb_define_singleton_method(cState, "from_state", cState_from_state_s, 1);
20032066
rb_define_singleton_method(cState, "default_sort_keys_proc=", cState_set_default_sort_keys_proc, 1);
2067+
rb_define_singleton_method(cState, "rfc8785_number_formater_proc=", cState_set_rfc8785_number_formater_proc, 1);
2068+
rb_define_singleton_method(cState, "rfc8785_sort_keys_proc=", cState_set_rfc8785_sort_keys_proc, 1);
20042069

20052070
rb_define_method(cState, "initialize", cState_initialize, -1);
20062071
rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings
@@ -2038,8 +2103,10 @@ void Init_generator(void)
20382103
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
20392104
rb_define_method(cState, "generate", cState_generate, -1);
20402105
rb_define_method(cState, "_generate_no_fallback", cState_generate_no_fallback, -1);
2041-
rb_define_method(cState, "sort_keys", cState_sort_keys_p, 0);
2106+
rb_define_method(cState, "sort_keys", cState_sort_keys, 0);
20422107
rb_define_method(cState, "sort_keys=", cState_sort_keys_set, 1);
2108+
rb_define_method(cState, "rfc8785?", cState_rfc8785_p, 0);
2109+
rb_define_method(cState, "rfc8785=", cState_rfc8785_set, 1);
20432110

20442111
rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0);
20452112

@@ -2069,6 +2136,7 @@ void Init_generator(void)
20692136
sym_as_json = ID2SYM(rb_intern("as_json"));
20702137
sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key"));
20712138
sym_sort_keys = ID2SYM(rb_intern("sort_keys"));
2139+
sym_rfc8785 = ID2SYM(rb_intern("rfc8785"));
20722140

20732141
usascii_encindex = rb_usascii_encindex();
20742142
utf8_encindex = rb_utf8_encindex();

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

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -403,11 +403,10 @@ void generate(ThreadContext context, Session session, RubyFloat object, OutputSt
403403
}
404404

405405
static void generateFloat(ThreadContext context, Session session, RubyFloat object, OutputStream buffer) throws IOException {
406+
final GeneratorState state = session.getState(context);
406407
double value = object.getValue();
407408

408409
if (Double.isInfinite(value) || Double.isNaN(value)) {
409-
GeneratorState state = session.getState(context);
410-
411410
if (!state.allowNaN()) {
412411
if (state.strict() && state.getAsJSON() != null) {
413412
IRubyObject castedValue = state.getAsJSON().call(context, object, context.getRuntime().getFalse());
@@ -421,7 +420,13 @@ static void generateFloat(ThreadContext context, Session session, RubyFloat obje
421420
}
422421
}
423422

424-
buffer.write(Double.toString(value).getBytes(UTF_8));
423+
if (state.rfc8785()) {
424+
RubyProc numberProc = (RubyProc)GeneratorState.rfc8785NumberFormaterProc;
425+
RubyString numberString = (RubyString)Helpers.invoke(context, numberProc, "call", object);
426+
buffer.write(numberString.toString().getBytes(UTF_8));
427+
} else {
428+
buffer.write(Double.toString(value).getBytes(UTF_8));
429+
}
425430
}
426431

427432
private static final byte[] EMPTY_ARRAY_BYTES = "[]".getBytes();
@@ -570,8 +575,12 @@ static void generateHash(ThreadContext context, Session session, RubyHash object
570575
}
571576

572577
RubyProc sortKeysProc = state.getSortKeysProc();
578+
if (state.rfc8785()) {
579+
sortKeysProc = (RubyProc)GeneratorState.rfc8785SortKeysProc;
580+
}
581+
573582
if (sortKeysProc != null) {
574-
object = (RubyHash) Helpers.invoke(context, sortKeysProc, "call", object);
583+
object = (RubyHash)Helpers.invoke(context, sortKeysProc, "call", object);
575584
}
576585

577586
final ByteList objectNl = state.getObjectNl();

‎java/src/json/ext/GeneratorState.java‎

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class GeneratorState extends RubyObject {
3737
private boolean allowDuplicateKey = false;
3838

3939
private static IRubyObject defaultSortKeyProc;
40+
public static IRubyObject rfc8785NumberFormaterProc;
41+
public static IRubyObject rfc8785SortKeysProc;
4042

4143
/**
4244
* The indenting unit string. Will be repeated several times for larger
@@ -112,6 +114,8 @@ public class GeneratorState extends RubyObject {
112114
*/
113115
private IRubyObject sortKeys;
114116

117+
private boolean rfc8785 = false;
118+
115119
/**
116120
* The current depth (inside a #to_json call)
117121
*/
@@ -172,6 +176,18 @@ public static IRubyObject setDefaultSortKeyProc(IRubyObject klass, IRubyObject p
172176
return proc;
173177
}
174178

179+
@JRubyMethod(meta=true, name="rfc8785_number_formater_proc=")
180+
public static IRubyObject setRfc8785NumberFormaterProc(IRubyObject klass, IRubyObject proc) {
181+
rfc8785NumberFormaterProc = proc;
182+
return proc;
183+
}
184+
185+
@JRubyMethod(meta=true, name="rfc8785_sort_keys_proc=")
186+
public static IRubyObject setRfc8785SortKeysProc(IRubyObject klass, IRubyObject proc) {
187+
rfc8785SortKeysProc = proc;
188+
return proc;
189+
}
190+
175191
/**
176192
* <code>State#initialize(opts = {})</code>
177193
* <p>
@@ -493,6 +509,22 @@ public IRubyObject sort_keys_set(ThreadContext context, IRubyObject sortKeys) {
493509
return sortKeys;
494510
}
495511

512+
@JRubyMethod(name="rfc8785?")
513+
public IRubyObject rfc8785_p(ThreadContext context) {
514+
return RubyBoolean.newBoolean(context, rfc8785);
515+
}
516+
517+
@JRubyMethod(name="rfc8785=")
518+
public IRubyObject rfc8785_set(ThreadContext context, IRubyObject rfc8785) {
519+
checkFrozen();
520+
this.rfc8785 = rfc8785.isTrue();
521+
return rfc8785;
522+
}
523+
524+
public boolean rfc8785() {
525+
return this.rfc8785;
526+
}
527+
496528
public int getDepth() {
497529
return depth;
498530
}
@@ -568,6 +600,8 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) {
568600

569601
if (opts.hasKey("sort_keys")) sortKeys = normalizeSortKeys(context, opts.get("sort_keys"));
570602

603+
rfc8785 = opts.getBool("rfc8785", rfc8785);
604+
571605
opts.ensureEmpty();
572606

573607
return this;

‎lib/json.rb‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,8 @@
414414
# When +true+, keys are sorted lexicographically. When a \Proc, it receives
415415
# the entire \Hash and must return a \Hash with its pairs in the desired
416416
# order, allowing for arbitrary sort orders.
417+
# - Option +rfc8785+ (boolean) controls whether the generated JSON will be canonicalized
418+
# as defined in RFC8785.
417419
#
418420
# In this example, +obj+ is used first to generate the shortest
419421
# \JSON data (no whitespace), then again with all formatting options

‎lib/json/ext.rb‎

Lines changed: 56 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,18 @@ def parse
2828
end
2929
end
3030

31+
class << self
32+
if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda)
33+
def shareable_lambda(block) # :nodoc:
34+
Ractor.shareable_lambda(&block)
35+
end
36+
else
37+
def shareable_lambda(block) # :nodoc:
38+
block
39+
end
40+
end
41+
end
42+
3143
require 'json/ext/parser'
3244
Ext::Parser::Config = Ext::ParserConfig
3345
JSON.parser = Ext::Parser
@@ -42,11 +54,50 @@ def parse
4254

4355
# The default proc used when the +sort_keys+ generation option is +true+.
4456
# It returns a new hash with the entries sorted by their keys.
45-
sort_keys_proc = ->(hash) { hash.sort.to_h }
46-
if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda)
47-
sort_keys_proc = Ractor.shareable_lambda(&sort_keys_proc)
48-
end
49-
generator::State.default_sort_keys_proc = sort_keys_proc
57+
generator::State.default_sort_keys_proc = shareable_lambda(->(hash) {
58+
hash.sort.to_h
59+
})
60+
61+
# Directly lifted from Gregg Kellogg's json-canonicalization
62+
generator::State.rfc8785_number_formater_proc = shareable_lambda(->(num) {
63+
if num.zero?
64+
"0"
65+
else
66+
if num < 0
67+
num, sign = -num, '-'
68+
end
69+
native_rep = "%.15E" % num
70+
decimal, exponential = native_rep.split('E')
71+
exp_val = exponential.to_i
72+
exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s
73+
74+
integral, fractional = decimal.split('.')
75+
fractional = fractional.sub(/0+$/, '') # Remove trailing zeros
76+
77+
if exp_val > 0 && exp_val < 21
78+
while exp_val > 0
79+
integral += fractional.to_s[0] || '0'
80+
fractional = fractional.to_s[1..-1]
81+
exp_val -= 1
82+
end
83+
exponential = nil
84+
elsif exp_val == 0
85+
exponential = nil
86+
elsif exp_val < 0 && exp_val > -7
87+
# Small numbers are shown as 0.etc with e-6 as lower limit
88+
fractional, integral, exponential = integral + fractional.to_s, '0', nil
89+
fractional = ("0" * (-exp_val - 1)) + fractional
90+
end
91+
92+
fractional = nil if fractional.to_s.empty?
93+
sign.to_s + integral + (fractional ? ".#{fractional}" : '') + (exponential ? "e#{exponential}" : '')
94+
end
95+
})
96+
97+
generator::State.rfc8785_sort_keys_proc = shareable_lambda(->(hash) {
98+
hash.sort_by { |k| k.to_s.encode(Encoding::UTF_16) }.to_h
99+
})
100+
50101
JSON.generator = generator
51102
end
52103

‎lib/json/ext/generator/state.rb‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,8 @@ def to_h
5454
strict: strict?,
5555
depth: depth,
5656
buffer_initial_length: buffer_initial_length,
57-
sort_keys: sort_keys
57+
sort_keys: sort_keys,
58+
rfc8785: rfc8785?,
5859
}
5960

6061
allow_duplicate_key = allow_duplicate_key?

0 commit comments

Comments
 (0)