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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 24e3bc40d587f5a4c001b5f4c7da30170bdf06f5401f2aa1928469c8fb3860e2
4
- data.tar.gz: b668789b5f3a56d7db311eb4ec9d10b6d236debab23016fabd1682316d50ae50
3
+ metadata.gz: ea6afdf75f49e474e408c24daae4856891310979fbb10aa34ead66ad2fbf3321
4
+ data.tar.gz: 19eb129df23cde0ac607fa6ddf909d6ea5eaef0035b2180eea4c2c8a7a5c5ff0
5
5
  SHA512:
6
- metadata.gz: aaa62a65724e3caa24797ce93ed3ac710a0186a1fd2db990a20fddb10eb99c71d92a967d0bed9d5086cb5b203c5dda0793faa461fda1e5c6963c62da5b4db3dc
7
- data.tar.gz: fc44e571dcb662a35a36165d7f7afbb94b11ab8d5660ab1a65905961b7548260152afbb3286fd233223ea5435b6bb62135a3b67b30bd26ca0cd8114e6921c980
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::Combining.new do |object, is_object_key|
120
+ coder = JSON::Coder.new do |object, is_object_key|
121
121
  case object
122
122
  when String
123
- if !string.valid_encoding? || string.encoding != Encoding::UTF_8
124
- Base64.encode64(string)
123
+ if !object.valid_encoding? || object.encoding != Encoding::UTF_8
124
+ Base64.encode64(object)
125
125
  else
126
- string
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 | RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
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
@@ -31,6 +31,10 @@
31
31
 
32
32
  /* shims */
33
33
 
34
+ #ifndef RUBY_TYPED_THREAD_SAFE_FREE
35
+ #define RUBY_TYPED_THREAD_SAFE_FREE RUBY_TYPED_FREE_IMMEDIATELY
36
+ #endif
37
+
34
38
  #ifndef UNDEF_P
35
39
  #define UNDEF_P(val) (val == Qundef)
36
40
  #endif
@@ -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, i_uminus, i_encode, i_at_line, i_at_column;
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 = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE,
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 = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE,
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
- long line, column;
622
- cursor_position(state, &line, &column);
623
-
624
- VALUE warning = rb_sprintf("%s at line %ld column %ld", message, line, column);
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
- state->cursor = memchr(state->cursor, '\n', state->end - state->cursor);
780
- if (!state->cursor) {
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, start);
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
- json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool include_comments)
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
- json_eat_whitespace(state, config, true);
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
- json_eat_whitespace(state, config, true);
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
- if (config->allow_trailing_comma) {
1811
- json_eat_whitespace(state, config, true);
1812
- if (peek(state) == ']') {
1813
- // Trailing comma: stay in COMMA to close on the next iteration.
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
- json_eat_whitespace(state, config, true);
1856
-
1857
- if (config->allow_trailing_comma) {
1858
- if (peek(state) == '}') {
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 = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
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 = RUBY_TYPED_FREE_IMMEDIATELY | RUBY_TYPED_EMBEDDABLE,
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 = (capa + missing_object_value);
2621
- VALUE tmpbuf, *value_stack_buffer = ALLOCV_N(VALUE, tmpbuf, capa + missing_object_value);
2622
- MEMCPY(value_stack_buffer, parser.value_stack.ptr, VALUE, parser.value_stack.capa);
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: value? -> true or false
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
@@ -54,6 +54,7 @@ module JSON
54
54
  strict: strict?,
55
55
  depth: depth,
56
56
  buffer_initial_length: buffer_initial_length,
57
+ sort_keys: sort_keys
57
58
  }
58
59
 
59
60
  allow_duplicate_key = allow_duplicate_key?
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSON
4
- VERSION = '2.20.0'
4
+ VERSION = '2.21.2'
5
5
  end
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+; specify +false+ to disable depth checking.
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, +false+:
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.20.0
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.12
87
+ rubygems_version: 4.0.16
88
88
  specification_version: 4
89
89
  summary: JSON Implementation for Ruby
90
90
  test_files: []