json 2.20.0 → 2.21.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +16 -0
- data/README.md +5 -5
- data/ext/json/ext/generator/generator.c +72 -3
- data/ext/json/ext/json.h +4 -0
- data/ext/json/ext/parser/extconf.rb +6 -16
- data/ext/json/ext/parser/parser.c +140 -43
- data/lib/json/common.rb +9 -0
- data/lib/json/ext/generator/state.rb +1 -0
- data/lib/json/ext.rb +26 -0
- data/lib/json/truffle_ruby/generator.rb +36 -1
- data/lib/json/version.rb +1 -1
- data/lib/json.rb +18 -3
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ea6afdf75f49e474e408c24daae4856891310979fbb10aa34ead66ad2fbf3321
|
|
4
|
+
data.tar.gz: 19eb129df23cde0ac607fa6ddf909d6ea5eaef0035b2180eea4c2c8a7a5c5ff0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c2212cdac41569ebd41e83e6d1ea3324e944f6a8db0070a2805f2213da7341a05cc8eab4dc6d77852f30d31a8951f36d435fdb0aff647776eb8f42cee25113ef
|
|
7
|
+
data.tar.gz: b16fdb8a1f5384aa6e9064c7884c0f7ec96632bc2c1c6324cc2e902459e9ac94559ec691cf45dea30fc374fab74ce4e3a82f8bb9d0ec22e0ddd5357764076942
|
data/CHANGES.md
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
### Unreleased
|
|
4
4
|
|
|
5
|
+
### 2026-07-31 (2.21.2)
|
|
6
|
+
|
|
7
|
+
* Fix a use-after-free bug in `JSON::ResumableParser`. [GHSA-9hj4-r449-hfvc].
|
|
8
|
+
|
|
9
|
+
### 2026-07-13 (2.21.1)
|
|
10
|
+
|
|
11
|
+
* Fix a compilation issue on Window and Microsoft Visual C++.
|
|
12
|
+
|
|
13
|
+
### 2026-07-12 (2.21.0)
|
|
14
|
+
|
|
15
|
+
* `JSON.generate` now accept a `sort_keys` option, which takes either a boolean or a block.
|
|
16
|
+
* Added `#empty?` and `#partial_value?` methods on `JSON::ResumableParser`.
|
|
17
|
+
* Numerous correctness and performance fixes for `JSON::ResumableParser`.
|
|
18
|
+
* Avoid triggering Ruby's `float out of range` warning when parsing out of range numbers.
|
|
19
|
+
* Declare C types with Ruby 4.1 `RUBY_TYPED_THREAD_SAFE_FREE`.
|
|
20
|
+
|
|
5
21
|
### 2026-06-23 (2.20.0)
|
|
6
22
|
|
|
7
23
|
* Both C and Java parsers are no longer recursive, so parsing very deep documents with `max_nesting: false` will no longer
|
data/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Both of these behavior can be disabled using the `strict: true` option:
|
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
87
|
JSON.generate(Object.new, strict: true) # => Object not allowed in JSON (JSON::GeneratorError)
|
|
88
|
-
JSON.generate(Position.new(1, 2)) # => Position not allowed in JSON (JSON::GeneratorError)
|
|
88
|
+
JSON.generate(Position.new(1, 2), strict: true) # => Position not allowed in JSON (JSON::GeneratorError)
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
## JSON::Coder
|
|
@@ -117,13 +117,13 @@ It is also called for objects that do have a JSON equivalent, but are used as Ha
|
|
|
117
117
|
as well as for strings that aren't valid UTF-8:
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
|
-
coder = JSON::
|
|
120
|
+
coder = JSON::Coder.new do |object, is_object_key|
|
|
121
121
|
case object
|
|
122
122
|
when String
|
|
123
|
-
if !
|
|
124
|
-
Base64.encode64(
|
|
123
|
+
if !object.valid_encoding? || object.encoding != Encoding::UTF_8
|
|
124
|
+
Base64.encode64(object)
|
|
125
125
|
else
|
|
126
|
-
|
|
126
|
+
object
|
|
127
127
|
end
|
|
128
128
|
else
|
|
129
129
|
object
|
|
@@ -34,13 +34,14 @@ typedef struct JSON_Generator_StateStruct {
|
|
|
34
34
|
bool ascii_only;
|
|
35
35
|
bool script_safe;
|
|
36
36
|
bool strict;
|
|
37
|
+
VALUE sort_keys;
|
|
37
38
|
} JSON_Generator_State;
|
|
38
39
|
|
|
39
|
-
static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8;
|
|
40
|
+
static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc;
|
|
40
41
|
|
|
41
42
|
static ID i_to_s, i_to_json, i_new, i_encode;
|
|
42
43
|
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,
|
|
43
|
-
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json;
|
|
44
|
+
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json, sym_sort_keys;
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
#define GET_STATE_TO(self, state) \
|
|
@@ -709,6 +710,7 @@ static void State_mark(void *ptr)
|
|
|
709
710
|
rb_gc_mark_movable(state->object_nl);
|
|
710
711
|
rb_gc_mark_movable(state->array_nl);
|
|
711
712
|
rb_gc_mark_movable(state->as_json);
|
|
713
|
+
rb_gc_mark_movable(state->sort_keys);
|
|
712
714
|
}
|
|
713
715
|
|
|
714
716
|
static void State_compact(void *ptr)
|
|
@@ -720,6 +722,7 @@ static void State_compact(void *ptr)
|
|
|
720
722
|
state->object_nl = rb_gc_location(state->object_nl);
|
|
721
723
|
state->array_nl = rb_gc_location(state->array_nl);
|
|
722
724
|
state->as_json = rb_gc_location(state->as_json);
|
|
725
|
+
state->sort_keys = rb_gc_location(state->sort_keys);
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
static size_t State_memsize(const void *ptr)
|
|
@@ -739,7 +742,7 @@ static const rb_data_type_t JSON_Generator_State_type = {
|
|
|
739
742
|
.dsize = State_memsize,
|
|
740
743
|
.dcompact = State_compact,
|
|
741
744
|
},
|
|
742
|
-
.flags = RUBY_TYPED_WB_PROTECTED |
|
|
745
|
+
.flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
|
|
743
746
|
};
|
|
744
747
|
|
|
745
748
|
static void state_init(JSON_Generator_State *state)
|
|
@@ -769,6 +772,7 @@ static void vstate_spill(struct generate_json_data *data)
|
|
|
769
772
|
RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl);
|
|
770
773
|
RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl);
|
|
771
774
|
RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
|
|
775
|
+
RB_OBJ_WRITTEN(vstate, Qundef, state->sort_keys);
|
|
772
776
|
}
|
|
773
777
|
|
|
774
778
|
static inline VALUE json_call_to_json(struct generate_json_data *data, VALUE obj)
|
|
@@ -1050,6 +1054,11 @@ static inline long increase_depth(struct generate_json_data *data)
|
|
|
1050
1054
|
|
|
1051
1055
|
static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
|
|
1052
1056
|
{
|
|
1057
|
+
if (RB_UNLIKELY(data->state->sort_keys)) {
|
|
1058
|
+
obj = rb_proc_call_with_block(data->state->sort_keys, 1, &obj, Qnil);
|
|
1059
|
+
Check_Type(obj, T_HASH);
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1053
1062
|
long depth = increase_depth(data);
|
|
1054
1063
|
|
|
1055
1064
|
if (RHASH_SIZE(obj) == 0) {
|
|
@@ -1376,6 +1385,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig)
|
|
|
1376
1385
|
RB_OBJ_WRITTEN(obj, Qundef, objState->object_nl);
|
|
1377
1386
|
RB_OBJ_WRITTEN(obj, Qundef, objState->array_nl);
|
|
1378
1387
|
RB_OBJ_WRITTEN(obj, Qundef, objState->as_json);
|
|
1388
|
+
RB_OBJ_WRITTEN(obj, Qundef, objState->sort_keys);
|
|
1379
1389
|
|
|
1380
1390
|
return obj;
|
|
1381
1391
|
}
|
|
@@ -1722,6 +1732,55 @@ static VALUE cState_ascii_only_set(VALUE self, VALUE enable)
|
|
|
1722
1732
|
return Qnil;
|
|
1723
1733
|
}
|
|
1724
1734
|
|
|
1735
|
+
static VALUE cState_set_default_sort_keys_proc(VALUE self, VALUE proc)
|
|
1736
|
+
{
|
|
1737
|
+
if (!rb_obj_is_proc(proc)) {
|
|
1738
|
+
rb_raise(rb_eTypeError, "sort_key_proc must be a Proc");
|
|
1739
|
+
}
|
|
1740
|
+
return default_sort_keys_proc = proc;
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
static VALUE normalize_sort_keys(VALUE value)
|
|
1744
|
+
{
|
|
1745
|
+
if (rb_obj_is_proc(value)) {
|
|
1746
|
+
return value;
|
|
1747
|
+
} else if (value == Qtrue) {
|
|
1748
|
+
return default_sort_keys_proc;
|
|
1749
|
+
} else if (RTEST(value)) {
|
|
1750
|
+
rb_raise(rb_eTypeError, "The `sort_keys` argument must be a boolean or a Proc");
|
|
1751
|
+
} else {
|
|
1752
|
+
return Qfalse;
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
|
|
1756
|
+
/*
|
|
1757
|
+
* call-seq: sort_keys
|
|
1758
|
+
*
|
|
1759
|
+
* Get the value of sort_keys.
|
|
1760
|
+
*/
|
|
1761
|
+
static VALUE cState_sort_keys_p(VALUE self)
|
|
1762
|
+
{
|
|
1763
|
+
GET_STATE(self);
|
|
1764
|
+
return state->sort_keys;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
/*
|
|
1768
|
+
* call-seq: sort_keys=(value)
|
|
1769
|
+
*
|
|
1770
|
+
* value is a boolean or a proc. If the value is the boolean true, object keys
|
|
1771
|
+
* will be sorted lexicographically in ascending order.
|
|
1772
|
+
*
|
|
1773
|
+
* If the value is a proc, it receives the entire Hash and must return a Hash
|
|
1774
|
+
* with its pairs in the desired order, allowing for arbitrary sorting.
|
|
1775
|
+
*/
|
|
1776
|
+
static VALUE cState_sort_keys_set(VALUE self, VALUE value)
|
|
1777
|
+
{
|
|
1778
|
+
rb_check_frozen(self);
|
|
1779
|
+
GET_STATE(self);
|
|
1780
|
+
RB_OBJ_WRITE(self, &state->sort_keys, normalize_sort_keys(value));
|
|
1781
|
+
return Qnil;
|
|
1782
|
+
}
|
|
1783
|
+
|
|
1725
1784
|
static VALUE cState_allow_duplicate_key_p(VALUE self)
|
|
1726
1785
|
{
|
|
1727
1786
|
GET_STATE(self);
|
|
@@ -1832,6 +1891,9 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
|
|
|
1832
1891
|
state->as_json_single_arg = proc && rb_proc_arity(proc) == 1;
|
|
1833
1892
|
state_write_value(data, &state->as_json, proc);
|
|
1834
1893
|
}
|
|
1894
|
+
else if (key == sym_sort_keys) {
|
|
1895
|
+
state_write_value(data, &state->sort_keys, normalize_sort_keys(val));
|
|
1896
|
+
}
|
|
1835
1897
|
return ST_CONTINUE;
|
|
1836
1898
|
}
|
|
1837
1899
|
|
|
@@ -1909,6 +1971,8 @@ void Init_generator(void)
|
|
|
1909
1971
|
VALUE mExt = rb_define_module_under(mJSON, "Ext");
|
|
1910
1972
|
VALUE mGenerator = rb_define_module_under(mExt, "Generator");
|
|
1911
1973
|
|
|
1974
|
+
rb_global_variable(&default_sort_keys_proc);
|
|
1975
|
+
|
|
1912
1976
|
rb_global_variable(&eGeneratorError);
|
|
1913
1977
|
eGeneratorError = rb_path2class("JSON::GeneratorError");
|
|
1914
1978
|
|
|
@@ -1918,6 +1982,8 @@ void Init_generator(void)
|
|
|
1918
1982
|
cState = rb_define_class_under(mGenerator, "State", rb_cObject);
|
|
1919
1983
|
rb_define_alloc_func(cState, cState_s_allocate);
|
|
1920
1984
|
rb_define_singleton_method(cState, "from_state", cState_from_state_s, 1);
|
|
1985
|
+
rb_define_singleton_method(cState, "default_sort_keys_proc=", cState_set_default_sort_keys_proc, 1);
|
|
1986
|
+
|
|
1921
1987
|
rb_define_method(cState, "initialize", cState_initialize, -1);
|
|
1922
1988
|
rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings
|
|
1923
1989
|
rb_define_private_method(cState, "_configure", cState_configure, 1);
|
|
@@ -1957,6 +2023,8 @@ void Init_generator(void)
|
|
|
1957
2023
|
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
|
|
1958
2024
|
rb_define_method(cState, "generate", cState_generate, -1);
|
|
1959
2025
|
rb_define_method(cState, "_generate_no_fallback", cState_generate_no_fallback, -1);
|
|
2026
|
+
rb_define_method(cState, "sort_keys", cState_sort_keys_p, 0);
|
|
2027
|
+
rb_define_method(cState, "sort_keys=", cState_sort_keys_set, 1);
|
|
1960
2028
|
|
|
1961
2029
|
rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0);
|
|
1962
2030
|
|
|
@@ -1986,6 +2054,7 @@ void Init_generator(void)
|
|
|
1986
2054
|
sym_strict = ID2SYM(rb_intern("strict"));
|
|
1987
2055
|
sym_as_json = ID2SYM(rb_intern("as_json"));
|
|
1988
2056
|
sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key"));
|
|
2057
|
+
sym_sort_keys = ID2SYM(rb_intern("sort_keys"));
|
|
1989
2058
|
|
|
1990
2059
|
usascii_encindex = rb_usascii_encindex();
|
|
1991
2060
|
utf8_encindex = rb_utf8_encindex();
|
data/ext/json/ext/json.h
CHANGED
|
@@ -15,22 +15,6 @@ have_func("rb_hash_new_capa", "ruby.h") # RUBY_VERSION >= 3.2
|
|
|
15
15
|
have_func("rb_hash_bulk_insert", "ruby.h") # Missing on TruffleRuby
|
|
16
16
|
have_func("ruby_xfree_sized", "ruby.h") # RUBY_VERSION >= 4.1
|
|
17
17
|
|
|
18
|
-
def have_builtin_func(name, check_expr, opt = "", &b)
|
|
19
|
-
checking_for checking_message(name.funcall_style, nil, opt) do
|
|
20
|
-
if try_compile(<<SRC, opt, &b)
|
|
21
|
-
int foo;
|
|
22
|
-
int main() { #{check_expr}; return 0; }
|
|
23
|
-
SRC
|
|
24
|
-
$defs.push(format("-DHAVE_BUILTIN_%s", name.tr_cpp))
|
|
25
|
-
true
|
|
26
|
-
else
|
|
27
|
-
false
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
have_builtin_func("__builtin_clzll", "__builtin_clzll(0)")
|
|
33
|
-
|
|
34
18
|
if have_header("x86intrin.h")
|
|
35
19
|
have_func("_lzcnt_u64", "x86intrin.h")
|
|
36
20
|
end
|
|
@@ -46,6 +30,12 @@ end
|
|
|
46
30
|
|
|
47
31
|
append_cflags("-std=c99")
|
|
48
32
|
|
|
33
|
+
# Disable function outlining on clang to prevent some of the repeated instructions
|
|
34
|
+
# in json_eat_whitespace being outlined into function calls.
|
|
35
|
+
# Note: This verifies '-mno-outline' is accepted as a valid compiler flag
|
|
36
|
+
# and will not pass it if unsupported.
|
|
37
|
+
append_cflags("-mno-outline")
|
|
38
|
+
|
|
49
39
|
if enable_config('parser-use-simd', default=!ENV["JSON_DISABLE_SIMD"])
|
|
50
40
|
load __dir__ + "/../simd/conf.rb"
|
|
51
41
|
end
|
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
static VALUE mJSON, eNestingError, eParserError, Encoding_UTF_8;
|
|
6
6
|
static VALUE CNaN, CInfinity, CMinusInfinity, JSON_empty_string;
|
|
7
7
|
|
|
8
|
-
static ID i_new, i_try_convert,
|
|
8
|
+
static ID i_new, i_try_convert, i_encode, i_at_line, i_at_column;
|
|
9
|
+
#ifndef HAVE_RB_STR_TO_INTERNED_STR
|
|
10
|
+
static ID i_uminus;
|
|
11
|
+
#endif
|
|
9
12
|
|
|
10
13
|
static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_comments,
|
|
11
14
|
sym_allow_control_characters, sym_allow_invalid_escape, sym_symbolize_names,
|
|
@@ -314,7 +317,7 @@ static const rb_data_type_t JSON_Parser_rvalue_stack_type = {
|
|
|
314
317
|
},
|
|
315
318
|
// We deliberately don't declare rvalue_stack as RUBY_TYPED_WB_PROTECTED
|
|
316
319
|
// because it churns a lot of values so trigering write barriers every time is very costly.
|
|
317
|
-
.flags =
|
|
320
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_EMBEDDABLE,
|
|
318
321
|
};
|
|
319
322
|
|
|
320
323
|
static rvalue_stack *rvalue_stack_spill(rvalue_stack *old_stack, VALUE *handle, rvalue_stack **stack_ref)
|
|
@@ -511,7 +514,7 @@ static const rb_data_type_t JSON_Parser_frame_stack_type = {
|
|
|
511
514
|
.dfree = json_frame_stack_free,
|
|
512
515
|
.dsize = json_frame_stack_memsize,
|
|
513
516
|
},
|
|
514
|
-
.flags =
|
|
517
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE,
|
|
515
518
|
};
|
|
516
519
|
|
|
517
520
|
static json_frame_stack *json_frame_stack_spill(json_frame_stack *old_stack, VALUE *handle, json_frame_stack **stack_ref)
|
|
@@ -586,6 +589,8 @@ static inline char peek(JSON_ParserState *state)
|
|
|
586
589
|
|
|
587
590
|
static void cursor_position(JSON_ParserState *state, long *line_out, long *column_out)
|
|
588
591
|
{
|
|
592
|
+
JSON_ASSERT(!state->parser);
|
|
593
|
+
JSON_ASSERT(state->cursor);
|
|
589
594
|
JSON_ASSERT(state->cursor <= state->end);
|
|
590
595
|
|
|
591
596
|
// Redundant but helpful for hardening
|
|
@@ -618,10 +623,14 @@ static const unsigned int MAX_DEPRECATIONS = 5;
|
|
|
618
623
|
|
|
619
624
|
static void emit_parse_warning(const char *message, JSON_ParserState *state)
|
|
620
625
|
{
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
626
|
+
VALUE warning;
|
|
627
|
+
if (state->parser) { // line and columns can't be accurate in resumable
|
|
628
|
+
warning = rb_utf8_str_new_cstr(message);
|
|
629
|
+
} else {
|
|
630
|
+
long line, column;
|
|
631
|
+
cursor_position(state, &line, &column);
|
|
632
|
+
warning = rb_sprintf("%s at line %ld column %ld", message, line, column);
|
|
633
|
+
}
|
|
625
634
|
rb_funcall(mJSON, rb_intern("deprecation_warning"), 1, warning);
|
|
626
635
|
}
|
|
627
636
|
|
|
@@ -765,22 +774,34 @@ static const rb_data_type_t JSON_ParserConfig_type;
|
|
|
765
774
|
|
|
766
775
|
const char *COMMENT_DEPRECATION_MESSAGE = "Encountered comment in JSON. This will raise an error in json 3.0 unless enabled via `allow_comments: true`";
|
|
767
776
|
NOINLINE(static) void
|
|
768
|
-
json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
777
|
+
json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config, const char *resume_pos)
|
|
769
778
|
{
|
|
770
779
|
if (config->on_comment == JSON_RAISE) {
|
|
771
780
|
raise_syntax_error("unexpected token %s", state);
|
|
772
781
|
}
|
|
773
782
|
|
|
774
783
|
const char *start = state->cursor;
|
|
784
|
+
// An incomplete comment suspends a resumable parse by rewinding the cursor
|
|
785
|
+
// and throwing. Callers that already consumed a token not yet committed to
|
|
786
|
+
// the frame stack pass resume_pos so the rewind re-reads that token too.
|
|
787
|
+
// Non-resumable error positions keep pointing at the comment either way.
|
|
788
|
+
const char *rewind_pos = (state->parser && resume_pos) ? resume_pos : start;
|
|
775
789
|
state->cursor++;
|
|
776
790
|
|
|
777
791
|
switch (peek(state)) {
|
|
778
792
|
case '/': {
|
|
779
|
-
|
|
780
|
-
if (!
|
|
793
|
+
const char *newline = memchr(state->cursor, '\n', state->end - state->cursor);
|
|
794
|
+
if (!newline) {
|
|
795
|
+
// state->parser marks resumable mode, where the buffer end is only a
|
|
796
|
+
// chunk boundary: the terminating newline may still arrive, so leave
|
|
797
|
+
// the comment unterminated instead of consuming to end as a one-shot
|
|
798
|
+
// parse would.
|
|
799
|
+
if (state->parser) {
|
|
800
|
+
raise_eos_error_at("unterminated comment, expected end of line", state, rewind_pos);
|
|
801
|
+
}
|
|
781
802
|
state->cursor = state->end;
|
|
782
803
|
} else {
|
|
783
|
-
state->cursor
|
|
804
|
+
state->cursor = newline + 1;
|
|
784
805
|
}
|
|
785
806
|
break;
|
|
786
807
|
}
|
|
@@ -790,7 +811,7 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
|
790
811
|
while (true) {
|
|
791
812
|
const char *next_match = memchr(state->cursor, '*', state->end - state->cursor);
|
|
792
813
|
if (!next_match) {
|
|
793
|
-
raise_eos_error_at("unterminated comment, expected closing '*/'", state,
|
|
814
|
+
raise_eos_error_at("unterminated comment, expected closing '*/'", state, rewind_pos);
|
|
794
815
|
}
|
|
795
816
|
|
|
796
817
|
state->cursor = next_match + 1;
|
|
@@ -802,7 +823,7 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
|
802
823
|
break;
|
|
803
824
|
}
|
|
804
825
|
default:
|
|
805
|
-
raise_parse_error_at("unexpected token %s", state, start, eos(state));
|
|
826
|
+
raise_parse_error_at("unexpected token %s", state, eos(state) ? rewind_pos : start, eos(state));
|
|
806
827
|
break;
|
|
807
828
|
}
|
|
808
829
|
|
|
@@ -813,7 +834,7 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
|
813
834
|
}
|
|
814
835
|
|
|
815
836
|
ALWAYS_INLINE(static) void
|
|
816
|
-
|
|
837
|
+
json_eat_whitespace_resume_at(JSON_ParserState *state, JSON_ParserConfig *config, bool include_comments, const char *resume_pos)
|
|
817
838
|
{
|
|
818
839
|
while (true) {
|
|
819
840
|
switch (peek(state)) {
|
|
@@ -848,7 +869,7 @@ json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool inc
|
|
|
848
869
|
return;
|
|
849
870
|
}
|
|
850
871
|
|
|
851
|
-
json_eat_comments(state, config);
|
|
872
|
+
json_eat_comments(state, config, resume_pos);
|
|
852
873
|
break;
|
|
853
874
|
|
|
854
875
|
default:
|
|
@@ -857,6 +878,12 @@ json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool inc
|
|
|
857
878
|
}
|
|
858
879
|
}
|
|
859
880
|
|
|
881
|
+
ALWAYS_INLINE(static) void
|
|
882
|
+
json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool include_comments)
|
|
883
|
+
{
|
|
884
|
+
json_eat_whitespace_resume_at(state, config, include_comments, NULL);
|
|
885
|
+
}
|
|
886
|
+
|
|
860
887
|
static inline VALUE build_string(const char *start, const char *end, bool intern, bool symbolize)
|
|
861
888
|
{
|
|
862
889
|
if (symbolize) {
|
|
@@ -1130,6 +1157,13 @@ static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantis
|
|
|
1130
1157
|
}
|
|
1131
1158
|
|
|
1132
1159
|
if (RB_UNLIKELY(mantissa_digits > 18 || mantissa_digits + exponent < -307)) {
|
|
1160
|
+
// If the value is so small that it definitely underflows to 0.0, return early
|
|
1161
|
+
// to avoid triggering a "Float out of range" warning from rb_cstr_to_dbl.
|
|
1162
|
+
// When mantissa_digits + exponent < -324, value < 10^(-324) < DBL_TRUE_MIN/2,
|
|
1163
|
+
// so it rounds to 0 in IEEE 754 round-to-nearest.
|
|
1164
|
+
if (RB_UNLIKELY(mantissa_digits + exponent < -324)) {
|
|
1165
|
+
return rb_float_new(negative ? -0.0 : 0.0);
|
|
1166
|
+
}
|
|
1133
1167
|
return json_decode_large_float(start, end - start);
|
|
1134
1168
|
}
|
|
1135
1169
|
|
|
@@ -1429,7 +1463,7 @@ static inline int json_parse_digits(JSON_ParserState *state, uint64_t *accumulat
|
|
|
1429
1463
|
return (int)(state->cursor - start);
|
|
1430
1464
|
}
|
|
1431
1465
|
|
|
1432
|
-
static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig *config, bool negative, const char *start)
|
|
1466
|
+
static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig *config, bool negative, const char *start, bool resumable)
|
|
1433
1467
|
{
|
|
1434
1468
|
bool integer = true;
|
|
1435
1469
|
const char first_digit = *state->cursor;
|
|
@@ -1486,6 +1520,16 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
|
|
|
1486
1520
|
}
|
|
1487
1521
|
}
|
|
1488
1522
|
|
|
1523
|
+
// A number touching the end of the buffer may still grow in a later chunk,
|
|
1524
|
+
// so the caller will rewind and wait. Decoding it now would build a value
|
|
1525
|
+
// -- for a long run of digits, an expensive bignum -- only to discard it,
|
|
1526
|
+
// and repeating that on every resumed chunk is quadratic in the number's
|
|
1527
|
+
// length. The digit scan above already advanced the cursor, which is all
|
|
1528
|
+
// the caller needs to detect the incomplete number.
|
|
1529
|
+
if (RB_UNLIKELY(resumable && eos(state))) {
|
|
1530
|
+
return Qundef;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1489
1533
|
if (integer) {
|
|
1490
1534
|
return json_decode_integer(mantissa, mantissa_digits, negative, start, state->cursor);
|
|
1491
1535
|
}
|
|
@@ -1563,6 +1607,13 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1563
1607
|
JSON_PHASE_VALUE: {
|
|
1564
1608
|
json_eat_whitespace(state, config, true);
|
|
1565
1609
|
|
|
1610
|
+
// A trailing comma lands us here expecting an element but finding the
|
|
1611
|
+
// closing bracket; hand off to ARRAY_COMMA to close. An empty array
|
|
1612
|
+
// closes inline at '[', so this position is only reached after a ','.
|
|
1613
|
+
if (config->allow_trailing_comma && frame->type == JSON_FRAME_ARRAY && peek(state) == ']') {
|
|
1614
|
+
goto JSON_PHASE_ARRAY_COMMA;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1566
1617
|
VALUE value;
|
|
1567
1618
|
const char *value_start = state->cursor;
|
|
1568
1619
|
|
|
@@ -1603,7 +1654,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1603
1654
|
case '-': {
|
|
1604
1655
|
state->cursor++;
|
|
1605
1656
|
|
|
1606
|
-
value = json_parse_number(state, config, true, value_start);
|
|
1657
|
+
value = json_parse_number(state, config, true, value_start, resumable);
|
|
1607
1658
|
|
|
1608
1659
|
if (RB_UNLIKELY(UNDEF_P(value) && config->allow_nan && peek(state) == 'I')) {
|
|
1609
1660
|
state->cursor = value_start;
|
|
@@ -1626,7 +1677,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1626
1677
|
}
|
|
1627
1678
|
|
|
1628
1679
|
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': {
|
|
1629
|
-
value = json_parse_number(state, config, false, value_start);
|
|
1680
|
+
value = json_parse_number(state, config, false, value_start, resumable);
|
|
1630
1681
|
|
|
1631
1682
|
// Top level numbers are ambiguous when parsing streams, we can't
|
|
1632
1683
|
// know if we parsed all the digits if we hit EOS.
|
|
@@ -1658,7 +1709,9 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1658
1709
|
|
|
1659
1710
|
case '[': {
|
|
1660
1711
|
state->cursor++;
|
|
1661
|
-
|
|
1712
|
+
// The '[' is consumed but its frame is only pushed below, so a
|
|
1713
|
+
// comment suspending here must resume from the bracket.
|
|
1714
|
+
json_eat_whitespace_resume_at(state, config, true, value_start);
|
|
1662
1715
|
|
|
1663
1716
|
const char next = peek(state);
|
|
1664
1717
|
if (next == ']') {
|
|
@@ -1687,7 +1740,8 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1687
1740
|
|
|
1688
1741
|
case '{': {
|
|
1689
1742
|
state->cursor++;
|
|
1690
|
-
|
|
1743
|
+
// Same as '[': the frame is only pushed below.
|
|
1744
|
+
json_eat_whitespace_resume_at(state, config, true, value_start);
|
|
1691
1745
|
|
|
1692
1746
|
if (peek(state) == '}') {
|
|
1693
1747
|
state->cursor++;
|
|
@@ -1745,6 +1799,13 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1745
1799
|
|
|
1746
1800
|
json_eat_whitespace(state, config, true);
|
|
1747
1801
|
|
|
1802
|
+
// A trailing comma lands us here expecting a key but finding the closing
|
|
1803
|
+
// brace; hand off to OBJECT_COMMA to close. An empty object closes inline
|
|
1804
|
+
// at '{', so this position is only reached after a ','.
|
|
1805
|
+
if (config->allow_trailing_comma && peek(state) == '}') {
|
|
1806
|
+
goto JSON_PHASE_OBJECT_COMMA;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1748
1809
|
const char *start = state->cursor;
|
|
1749
1810
|
|
|
1750
1811
|
if (RB_LIKELY(peek(state) == '"')) {
|
|
@@ -1807,13 +1868,10 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1807
1868
|
|
|
1808
1869
|
if (RB_LIKELY(next_char == ',')) {
|
|
1809
1870
|
state->cursor++;
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
goto JSON_PHASE_ARRAY_COMMA;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1871
|
+
// Commit the phase before eating the whitespace that follows: an
|
|
1872
|
+
// incomplete comment there would suspend the parse, and a phase not
|
|
1873
|
+
// yet advanced past the ',' would drop it on resume. A trailing comma
|
|
1874
|
+
// is recognized in JSON_PHASE_VALUE once the ']' is in the buffer.
|
|
1817
1875
|
frame->phase = JSON_PHASE_VALUE;
|
|
1818
1876
|
goto JSON_PHASE_VALUE;
|
|
1819
1877
|
} else if (next_char == ']') {
|
|
@@ -1852,15 +1910,10 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1852
1910
|
|
|
1853
1911
|
if (RB_LIKELY(next_char == ',')) {
|
|
1854
1912
|
state->cursor++;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
// Trailing comma: stay in COMMA to close on the next iteration.
|
|
1860
|
-
goto JSON_PHASE_OBJECT_COMMA;
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1913
|
+
// Commit the phase before eating the whitespace that follows: an
|
|
1914
|
+
// incomplete comment there would suspend the parse, and a phase not
|
|
1915
|
+
// yet advanced past the ',' would drop it on resume. A trailing comma
|
|
1916
|
+
// is recognized in JSON_PHASE_OBJECT_KEY once the '}' is in the buffer.
|
|
1864
1917
|
frame->phase = JSON_PHASE_OBJECT_KEY;
|
|
1865
1918
|
goto JSON_PHASE_OBJECT_KEY;
|
|
1866
1919
|
} else if (next_char == '}') {
|
|
@@ -2180,7 +2233,7 @@ static const rb_data_type_t JSON_ParserConfig_type = {
|
|
|
2180
2233
|
.dsize = JSON_ParserConfig_memsize,
|
|
2181
2234
|
.dcompact = JSON_ParserConfig_compact,
|
|
2182
2235
|
},
|
|
2183
|
-
.flags =
|
|
2236
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
|
|
2184
2237
|
};
|
|
2185
2238
|
|
|
2186
2239
|
static VALUE cJSON_parser_s_allocate(VALUE klass)
|
|
@@ -2265,7 +2318,7 @@ static const rb_data_type_t JSON_ResumableParser_type = {
|
|
|
2265
2318
|
// RUBY_TYPED_WB_PROTECTED is deliberately not declared because
|
|
2266
2319
|
// this is a superset of JSON_Parser_rvalue_stack_type, so we'd need
|
|
2267
2320
|
// to trigger a lot of write barriers.
|
|
2268
|
-
.flags =
|
|
2321
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_EMBEDDABLE,
|
|
2269
2322
|
};
|
|
2270
2323
|
|
|
2271
2324
|
static VALUE cResumableParser_allocate(VALUE klass)
|
|
@@ -2519,6 +2572,7 @@ static VALUE cResumableParser_parse(VALUE self)
|
|
|
2519
2572
|
if (eos(&parser->state)) {
|
|
2520
2573
|
json_str_clear(parser->buffer);
|
|
2521
2574
|
parser->buffer = Qfalse;
|
|
2575
|
+
parser->state.start = parser->state.cursor = parser->state.end = 0;
|
|
2522
2576
|
}
|
|
2523
2577
|
parser->in_use = false;
|
|
2524
2578
|
|
|
@@ -2615,11 +2669,16 @@ static VALUE cResumableParser_partial_value_body(VALUE self)
|
|
|
2615
2669
|
missing_object_value = 1;
|
|
2616
2670
|
}
|
|
2617
2671
|
|
|
2618
|
-
// Copy the value stack as we need to mutate it.
|
|
2672
|
+
// Copy the value stack as we need to mutate it. The collapse loop folds each
|
|
2673
|
+
// open container by popping its entries and pushing the single result, so a
|
|
2674
|
+
// parent always reclaims its child's slot; head exceeds its live size by at
|
|
2675
|
+
// most one, either for the missing-value placeholder pushed below or for the
|
|
2676
|
+
// result of folding an empty innermost container. That one spare slot keeps
|
|
2677
|
+
// rvalue_stack_push from growing (reallocating) this ALLOCV buffer.
|
|
2619
2678
|
long capa = parser.value_stack.head;
|
|
2620
|
-
parser.value_stack.capa =
|
|
2621
|
-
VALUE tmpbuf, *value_stack_buffer = ALLOCV_N(VALUE, tmpbuf, capa
|
|
2622
|
-
MEMCPY(value_stack_buffer, parser.value_stack.ptr, VALUE,
|
|
2679
|
+
parser.value_stack.capa = capa + 1;
|
|
2680
|
+
VALUE tmpbuf, *value_stack_buffer = ALLOCV_N(VALUE, tmpbuf, parser.value_stack.capa);
|
|
2681
|
+
MEMCPY(value_stack_buffer, parser.value_stack.ptr, VALUE, capa);
|
|
2623
2682
|
parser.value_stack.ptr = value_stack_buffer;
|
|
2624
2683
|
|
|
2625
2684
|
JSON_ParserState *state = &parser.state;
|
|
@@ -2712,7 +2771,7 @@ static VALUE cResumableParser_rest(VALUE self)
|
|
|
2712
2771
|
}
|
|
2713
2772
|
|
|
2714
2773
|
/*
|
|
2715
|
-
* call-seq:
|
|
2774
|
+
* call-seq: eos? -> true or false
|
|
2716
2775
|
*
|
|
2717
2776
|
* Returns whether the internal buffer has been entirely consumed.
|
|
2718
2777
|
*/
|
|
@@ -2722,6 +2781,41 @@ static VALUE cResumableParser_eos_p(VALUE self)
|
|
|
2722
2781
|
return eos(&parser->state) ? Qtrue : Qfalse;
|
|
2723
2782
|
}
|
|
2724
2783
|
|
|
2784
|
+
/*
|
|
2785
|
+
* call-seq: partial_value? -> true or false
|
|
2786
|
+
*
|
|
2787
|
+
* Returns whether a document is currently under construction: an unclosed
|
|
2788
|
+
* container, a key awaiting its value, etc.
|
|
2789
|
+
*
|
|
2790
|
+
* It answers the same question as <tt>!partial_value.nil?</tt>, but as a
|
|
2791
|
+
* cheap predicate on the parser's internal state, without materializing the
|
|
2792
|
+
* partially parsed Ruby objects:
|
|
2793
|
+
* parser << '{"a":1,'
|
|
2794
|
+
* parser.parse # => false
|
|
2795
|
+
* parser.partial_value? # => true
|
|
2796
|
+
*
|
|
2797
|
+
* A fully parsed document whose value hasn't been retrieved yet is not under
|
|
2798
|
+
* construction: #value? returns true and #partial_value? returns false.
|
|
2799
|
+
*/
|
|
2800
|
+
static VALUE cResumableParser_partial_value_p(VALUE self)
|
|
2801
|
+
{
|
|
2802
|
+
JSON_ResumableParser *parser = cResumableParser_get(self);
|
|
2803
|
+
|
|
2804
|
+
// Mirror of #value?: values on the stack while the document isn't DONE
|
|
2805
|
+
// belong to a partially built document. A container whose first key or
|
|
2806
|
+
// element hasn't been parsed yet has no frame nor value registered (the
|
|
2807
|
+
// tokenizer rewinds to the container start on EOS), so that state is
|
|
2808
|
+
// observable through the buffer (#eos?/#rest) instead, keeping this
|
|
2809
|
+
// predicate consistent with #partial_value returning nil.
|
|
2810
|
+
if (parser->value_stack.head > 0) {
|
|
2811
|
+
json_frame *frame = json_frame_stack_peek(&parser->frames);
|
|
2812
|
+
if (frame->phase != JSON_PHASE_DONE) {
|
|
2813
|
+
return Qtrue;
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
return Qfalse;
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2725
2819
|
/*
|
|
2726
2820
|
* call-seq: parsed_bytes -> integer
|
|
2727
2821
|
*
|
|
@@ -2778,6 +2872,7 @@ void Init_parser(void)
|
|
|
2778
2872
|
rb_define_method(cResumableParser, "value", cResumableParser_value, 0);
|
|
2779
2873
|
rb_define_method(cResumableParser, "value?", cResumableParser_value_p, 0);
|
|
2780
2874
|
rb_define_method(cResumableParser, "partial_value", cResumableParser_partial_value, 0);
|
|
2875
|
+
rb_define_method(cResumableParser, "partial_value?", cResumableParser_partial_value_p, 0);
|
|
2781
2876
|
rb_define_method(cResumableParser, "clear", cResumableParser_clear, 0);
|
|
2782
2877
|
rb_define_method(cResumableParser, "rest", cResumableParser_rest, 0);
|
|
2783
2878
|
rb_define_method(cResumableParser, "eos?", cResumableParser_eos_p, 0);
|
|
@@ -2812,7 +2907,9 @@ void Init_parser(void)
|
|
|
2812
2907
|
|
|
2813
2908
|
i_new = rb_intern("new");
|
|
2814
2909
|
i_try_convert = rb_intern("try_convert");
|
|
2910
|
+
#ifndef HAVE_RB_STR_TO_INTERNED_STR
|
|
2815
2911
|
i_uminus = rb_intern("-@");
|
|
2912
|
+
#endif
|
|
2816
2913
|
i_encode = rb_intern("encode");
|
|
2817
2914
|
i_at_line = rb_intern("@line");
|
|
2818
2915
|
i_at_column = rb_intern("@column");
|
data/lib/json/common.rb
CHANGED
|
@@ -155,6 +155,15 @@ module JSON
|
|
|
155
155
|
# Set the module _generator_ to be used by JSON.
|
|
156
156
|
def generator=(generator) # :nodoc:
|
|
157
157
|
old, $VERBOSE = $VERBOSE, nil
|
|
158
|
+
|
|
159
|
+
# The default proc used when the +sort_keys+ generation option is +true+.
|
|
160
|
+
# It returns a new hash with the entries sorted by their keys.
|
|
161
|
+
sort_keys_proc = ->(hash) { hash.sort.to_h }
|
|
162
|
+
if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda)
|
|
163
|
+
sort_keys_proc = Ractor.shareable_lambda(&sort_keys_proc)
|
|
164
|
+
end
|
|
165
|
+
generator::State.default_sort_keys_proc = sort_keys_proc
|
|
166
|
+
|
|
158
167
|
@generator = generator
|
|
159
168
|
if generator.const_defined?(:GeneratorMethods)
|
|
160
169
|
generator_methods = generator::GeneratorMethods
|
data/lib/json/ext.rb
CHANGED
|
@@ -41,5 +41,31 @@ module JSON
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
if defined?(ResumableParser) # Not yet available on JRuby
|
|
45
|
+
class ResumableParser
|
|
46
|
+
# Returns whether the parser is entirely done: no unconsumed bytes in
|
|
47
|
+
# the buffer, no document under construction and no parsed value
|
|
48
|
+
# awaiting retrieval.
|
|
49
|
+
#
|
|
50
|
+
# The main use case is detecting a truncated stream once the input is
|
|
51
|
+
# exhausted:
|
|
52
|
+
#
|
|
53
|
+
# loop do
|
|
54
|
+
# begin
|
|
55
|
+
# parser << socket.readpartial(4096)
|
|
56
|
+
# rescue EOFError
|
|
57
|
+
# break
|
|
58
|
+
# end
|
|
59
|
+
# while parser.parse
|
|
60
|
+
# process(parser.value)
|
|
61
|
+
# end
|
|
62
|
+
# end
|
|
63
|
+
# warn "stream was truncated" unless parser.empty?
|
|
64
|
+
def empty?
|
|
65
|
+
eos? && !partial_value? && !value?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
44
70
|
JSON_LOADED = true unless defined?(JSON::JSON_LOADED)
|
|
45
71
|
end
|
|
@@ -111,6 +111,8 @@ module JSON
|
|
|
111
111
|
# This class is used to create State instances, that are use to hold data
|
|
112
112
|
# while generating a JSON text from a Ruby data structure.
|
|
113
113
|
class State
|
|
114
|
+
singleton_class.attr_accessor :default_sort_keys_proc # :nodoc:
|
|
115
|
+
|
|
114
116
|
def self.generate(obj, opts = nil, io = nil)
|
|
115
117
|
new(opts).generate(obj, io)
|
|
116
118
|
end
|
|
@@ -164,6 +166,7 @@ module JSON
|
|
|
164
166
|
@script_safe = false
|
|
165
167
|
@strict = false
|
|
166
168
|
@max_nesting = 100
|
|
169
|
+
@sort_keys = false
|
|
167
170
|
configure(opts) if opts
|
|
168
171
|
end
|
|
169
172
|
|
|
@@ -199,6 +202,33 @@ module JSON
|
|
|
199
202
|
# supported by the JSON spec will raise a JSON::GeneratorError
|
|
200
203
|
attr_accessor :strict
|
|
201
204
|
|
|
205
|
+
# Controls key sorting in the generated JSON. If set to +true+, object
|
|
206
|
+
# keys are sorted by key lexicographically. If set to a Proc, it
|
|
207
|
+
# receives the entire Hash and must return a Hash with its pairs in the
|
|
208
|
+
# desired order.
|
|
209
|
+
attr_reader :sort_keys
|
|
210
|
+
|
|
211
|
+
def sort_keys=(value) # :nodoc:
|
|
212
|
+
type_error = false
|
|
213
|
+
@sort_keys = case value
|
|
214
|
+
when Proc
|
|
215
|
+
value
|
|
216
|
+
when true
|
|
217
|
+
State.default_sort_keys_proc
|
|
218
|
+
when nil, false
|
|
219
|
+
false
|
|
220
|
+
else
|
|
221
|
+
type_error = true
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if type_error
|
|
226
|
+
raise TypeError, "The `sort_keys` argument must be a boolean or a Proc"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
@sort_keys
|
|
230
|
+
end
|
|
231
|
+
|
|
202
232
|
# :stopdoc:
|
|
203
233
|
attr_reader :buffer_initial_length
|
|
204
234
|
|
|
@@ -285,6 +315,7 @@ module JSON
|
|
|
285
315
|
@allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
|
|
286
316
|
@as_json = opts[:as_json].to_proc if opts[:as_json]
|
|
287
317
|
@ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
|
|
318
|
+
self.sort_keys = opts[:sort_keys] if opts.key?(:sort_keys)
|
|
288
319
|
@depth = opts[:depth] || 0
|
|
289
320
|
@buffer_initial_length ||= opts[:buffer_initial_length]
|
|
290
321
|
|
|
@@ -349,9 +380,13 @@ module JSON
|
|
|
349
380
|
|
|
350
381
|
depth = @depth
|
|
351
382
|
if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and
|
|
352
|
-
!@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj)
|
|
383
|
+
!@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) and !@sort_keys
|
|
353
384
|
result = generate_json(obj, ''.dup)
|
|
354
385
|
else
|
|
386
|
+
if @sort_keys
|
|
387
|
+
obj = @sort_keys.call(obj)
|
|
388
|
+
end
|
|
389
|
+
|
|
355
390
|
result = obj.to_json(self)
|
|
356
391
|
end
|
|
357
392
|
JSON::TruffleRuby::Generator.valid_utf8?(result) or raise GeneratorError.new(
|
data/lib/json/version.rb
CHANGED
data/lib/json.rb
CHANGED
|
@@ -121,9 +121,11 @@ require 'json/common'
|
|
|
121
121
|
# ====== Input Options
|
|
122
122
|
#
|
|
123
123
|
# Option +max_nesting+ (\Integer) specifies the maximum nesting depth allowed;
|
|
124
|
-
# defaults to +100+;
|
|
124
|
+
# defaults to +100+;
|
|
125
|
+
# You can set it to +false+ to disable depth checking entirely, but that is dangerous
|
|
126
|
+
# when parsing untrusted input.
|
|
125
127
|
#
|
|
126
|
-
# With the default, +
|
|
128
|
+
# With the default, +100+:
|
|
127
129
|
# source = '[0, [1, [2, [3]]]]'
|
|
128
130
|
# ruby = JSON.parse(source)
|
|
129
131
|
# ruby # => [0, [1, [2, [3]]]]
|
|
@@ -384,6 +386,15 @@ require 'json/common'
|
|
|
384
386
|
# # Raises JSON::NestingError (nesting of 2 is too deep):
|
|
385
387
|
# JSON.generate(obj, max_nesting: 2)
|
|
386
388
|
#
|
|
389
|
+
# With +false+:
|
|
390
|
+
# obj = []
|
|
391
|
+
# obj[0] = obj
|
|
392
|
+
# # Raises SystemStackError: stack level too deep
|
|
393
|
+
# JSON.generate(obj, max_nesting: false)
|
|
394
|
+
#
|
|
395
|
+
# Setting +max_nesting+ to +false+ can lead to a stackoverflow and may leave the program
|
|
396
|
+
# in an unrecoverable state. It is discouraged.
|
|
397
|
+
#
|
|
387
398
|
# ====== Escaping Options
|
|
388
399
|
#
|
|
389
400
|
# Options +script_safe+ (boolean) specifies wether <tt>'\u2028'</tt>, <tt>'\u2029'</tt>
|
|
@@ -408,7 +419,6 @@ require 'json/common'
|
|
|
408
419
|
# to be inserted after each \JSON object; defaults to the empty \String, <tt>''</tt>.
|
|
409
420
|
# - Option +indent+ (\String) specifies the string (usually spaces) to be
|
|
410
421
|
# used for indentation; defaults to the empty \String, <tt>''</tt>;
|
|
411
|
-
# defaults to the empty \String, <tt>''</tt>;
|
|
412
422
|
# has no effect unless options +array_nl+ or +object_nl+ specify newlines.
|
|
413
423
|
# - Option +space+ (\String) specifies a string (usually a space) to be
|
|
414
424
|
# inserted after the colon in each \JSON object's pair;
|
|
@@ -416,6 +426,11 @@ require 'json/common'
|
|
|
416
426
|
# - Option +space_before+ (\String) specifies a string (usually a space) to be
|
|
417
427
|
# inserted before the colon in each \JSON object's pair;
|
|
418
428
|
# defaults to the empty \String, <tt>''</tt>.
|
|
429
|
+
# - Option +sort_keys+ (boolean or \Proc) controls whether and how the keys of a
|
|
430
|
+
# hash are sorted when generating the output; defaults to <tt>false</tt>.
|
|
431
|
+
# When +true+, keys are sorted lexicographically. When a \Proc, it receives
|
|
432
|
+
# the entire \Hash and must return a \Hash with its pairs in the desired
|
|
433
|
+
# order, allowing for arbitrary sort orders.
|
|
419
434
|
#
|
|
420
435
|
# In this example, +obj+ is used first to generate the shortest
|
|
421
436
|
# \JSON data (no whitespace), then again with all formatting options
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: json
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.21.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Florian Frank
|
|
@@ -84,7 +84,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
84
84
|
- !ruby/object:Gem::Version
|
|
85
85
|
version: '0'
|
|
86
86
|
requirements: []
|
|
87
|
-
rubygems_version: 4.0.
|
|
87
|
+
rubygems_version: 4.0.16
|
|
88
88
|
specification_version: 4
|
|
89
89
|
summary: JSON Implementation for Ruby
|
|
90
90
|
test_files: []
|