oj 3.17.6 → 3.17.7

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: af3a1f2b343eefacdaeb1f82d4b48fe67bece49e4aae63ad46aa878f726ae3de
4
- data.tar.gz: 8cd6e1ac300706f39d434cbbba52ea780a96b761ba2090d3d3624185bc021819
3
+ metadata.gz: 6d8e7bd60ca653140f1d101a88ded37ad5b6fe2af3d5e01ae6787435238c75eb
4
+ data.tar.gz: 9ef057d4e77c5b2ba30f9a34b2fc3621875c902ca05da25495a4abf657f0bbdb
5
5
  SHA512:
6
- metadata.gz: e081fafa7bd5af792fef91dc58612d6ea85bdae7b3d709bbd945773a5ad9d1feec4c88e3844aa403098c3fa267814ef61dcbba5fb4d9ee86a239eecf5b3926fa
7
- data.tar.gz: 339ba6bedb36fb30a382f8cb5037053a4197d4e9364bb11f50b79ec6ec3002cdd687868e140fec1dacabdc9cfa972a45cfcd6cb8a85e4c4148cd484f56278004
6
+ metadata.gz: b9654f35d338c316388534ee7d7143165dd2aab82960324f7fd313771445a2dc2d2b22964408b533735dab30e4fd554b5f8abe272a7b3beb5352720cd039fb16
7
+ data.tar.gz: 4ffc5a78fc866f6824564d34e2db316c56454999bdcb0f91068b40e4dd28f04f23acfb9ce819364bb6a33c3aa5c2e3be0979982220552a1ec3a5eceedfd6f320
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.17.7 - 2026-09-29
4
+
5
+ ### Fixed
6
+ - issue #1104, where `Oj.dump` in compat and custom mode forwarded its whole options hash to the `to_json` methods it calls, so with json 3.0, whose generator raises `ArgumentError` on options it does not know, dumping a `Time` or any other object whose `to_json` comes from the json gem failed with `unknown keywords: mode, use_to_json`. Nested values reach `to_json` even without `use_to_json`, so `Oj.dump(data, mode: :compat)` failed the same way for data holding a `BigDecimal` or `Time`. The options Oj acts on are now dropped from the hash `to_json` receives, except `:only` and `:except`, which ActiveSupport's `as_json` reads. Keys Oj does not know are passed through untouched.
7
+ - Speed up hash-heavy documents: skip the 11KB per-parse memset and bulk-insert hashes (#1096)
8
+ - Preserve decimal placement for large parser numbers (#1099)
9
+ - Keep zero digits in oversized parser exponents (#1100)
10
+ - Parse very small usual-mode numbers correctly (#1101)
11
+ - Speed up symbol keys by interning without a temporary String (#1102)
12
+ - Keep Oj's own options out of the hash passed to to_json (#1106)
13
+ - Ensure class-cache mutex is released on exceptions (#1108)
14
+ - Guard __has_include with an #ifdef to support older GCC versions (4.4.7 etc.) (#1111)
15
+
3
16
  ## 3.17.6 - 2026-08-10
4
17
 
5
18
  - Fixed issue #1092, where the encoder ActiveSupport 8.1 caches with `escape: false` froze the options as they stood before `set_encoder` wrote `time_precision` into them, so `to_json(escape: false)` emitted 9 fractional digits. An options hash that names no option Oj knows no longer detaches an encoder from the defaults. (#1093)
data/ext/oj/compat.c CHANGED
@@ -33,7 +33,10 @@ static void hash_set_cstr(ParseInfo pi, Val kval, const char *str, size_t len, c
33
33
  rstr = rb_funcall(clas, oj_json_create_id, 1, rstr);
34
34
  }
35
35
  }
36
- if (rb_cHash != rb_obj_class(parent->val)) {
36
+ if (Qnil == pi->options.hash_class) {
37
+ stack_pair_push(&pi->stack, rkey, rstr);
38
+ parent->pcnt += 2;
39
+ } else if (rb_cHash != rb_obj_class(parent->val)) {
37
40
  // The rb_hash_set would still work but the unit tests for the
38
41
  // json gem require the less efficient []= method be called to set
39
42
  // values. Even using the store method to set the values will fail
@@ -47,20 +50,34 @@ static void hash_set_cstr(ParseInfo pi, Val kval, const char *str, size_t len, c
47
50
  }
48
51
 
49
52
  static VALUE start_hash(ParseInfo pi) {
50
- volatile VALUE h;
51
-
52
53
  if (Qnil != pi->options.hash_class) {
53
- h = rb_class_new_instance(0, NULL, pi->options.hash_class);
54
- } else {
55
- h = rb_hash_new();
54
+ return rb_class_new_instance(0, NULL, pi->options.hash_class);
56
55
  }
57
56
  TRACE_PARSE_IN(pi->options.trace, "start_hash", pi);
58
- return h;
57
+ // The Hash is built at end_hash from the buffered pairs.
58
+ return Qundef;
59
59
  }
60
60
 
61
61
  static void end_hash(struct _parseInfo *pi) {
62
62
  Val parent = stack_peek(&pi->stack);
63
63
 
64
+ if (Qnil == pi->options.hash_class) {
65
+ ValStack stack = &pi->stack;
66
+ size_t cnt = parent->pcnt;
67
+ volatile VALUE h;
68
+
69
+ #if HAVE_RB_HASH_NEW_CAPA
70
+ h = rb_hash_new_capa((long)(cnt / 2));
71
+ #else
72
+ h = rb_hash_new();
73
+ #endif
74
+ if (0 < cnt) {
75
+ rb_hash_bulk_insert((long)cnt, stack->pairs + stack->pcnt - cnt, h);
76
+ stack->pcnt -= cnt;
77
+ parent->pcnt = 0;
78
+ }
79
+ parent->val = h;
80
+ }
64
81
  if (0 != parent->classname) {
65
82
  volatile VALUE clas;
66
83
 
@@ -103,7 +120,10 @@ static void add_num(ParseInfo pi, NumInfo ni) {
103
120
  static void hash_set_num(struct _parseInfo *pi, Val parent, NumInfo ni) {
104
121
  volatile VALUE rval = oj_num_as_value(ni);
105
122
 
106
- if (rb_cHash != rb_obj_class(parent->val)) {
123
+ if (Qnil == pi->options.hash_class) {
124
+ stack_pair_push(&pi->stack, oj_calc_hash_key(pi, parent), rval);
125
+ parent->pcnt += 2;
126
+ } else if (rb_cHash != rb_obj_class(parent->val)) {
107
127
  // The rb_hash_set would still work but the unit tests for the
108
128
  // json gem require the less efficient []= method be called to set
109
129
  // values. Even using the store method to set the values will fail
@@ -116,7 +136,10 @@ static void hash_set_num(struct _parseInfo *pi, Val parent, NumInfo ni) {
116
136
  }
117
137
 
118
138
  static void hash_set_value(ParseInfo pi, Val parent, VALUE value) {
119
- if (rb_cHash != rb_obj_class(parent->val)) {
139
+ if (Qnil == pi->options.hash_class) {
140
+ stack_pair_push(&pi->stack, oj_calc_hash_key(pi, parent), value);
141
+ parent->pcnt += 2;
142
+ } else if (rb_cHash != rb_obj_class(parent->val)) {
120
143
  // The rb_hash_set would still work but the unit tests for the
121
144
  // json gem require the less efficient []= method be called to set
122
145
  // values. Even using the store method to set the values will fail
data/ext/oj/custom.c CHANGED
@@ -1033,8 +1033,24 @@ static void array_append_cstr(ParseInfo pi, const char *str, size_t len, const c
1033
1033
  TRACE_PARSE_CALL(pi->options.trace, "append_string", pi, rstr);
1034
1034
  }
1035
1035
 
1036
+ // Compat mode defers Hash creation to its end_hash (bulk insert from the
1037
+ // buffered pairs) but the custom callbacks below fill the Hash as they go,
1038
+ // so hashes must exist from the opening brace here.
1039
+ static VALUE start_hash(ParseInfo pi) {
1040
+ volatile VALUE h;
1041
+
1042
+ if (Qnil != pi->options.hash_class) {
1043
+ h = rb_class_new_instance(0, NULL, pi->options.hash_class);
1044
+ } else {
1045
+ h = rb_hash_new();
1046
+ }
1047
+ TRACE_PARSE_IN(pi->options.trace, "start_hash", pi);
1048
+ return h;
1049
+ }
1050
+
1036
1051
  void oj_set_custom_callbacks(ParseInfo pi) {
1037
1052
  oj_set_compat_callbacks(pi);
1053
+ pi->start_hash = start_hash;
1038
1054
  pi->hash_set_cstr = hash_set_cstr;
1039
1055
  pi->end_hash = end_hash;
1040
1056
  pi->hash_set_num = hash_set_num;
data/ext/oj/extconf.rb CHANGED
@@ -31,6 +31,7 @@ have_func('stpcpy')
31
31
  have_func('pthread_mutex_init')
32
32
  have_func('getrlimit', 'sys/resource.h')
33
33
  have_func('rb_enc_interned_str')
34
+ have_func('rb_hash_new_capa', 'ruby.h')
34
35
  have_func('rb_ext_ractor_safe', 'ruby.h')
35
36
  have_func('rb_hash_start', 'ruby.h')
36
37
 
@@ -48,6 +49,12 @@ if try_cflags('-msse2')
48
49
  $CPPFLAGS += ' -msse2'
49
50
  end
50
51
 
52
+ # Ensure C99 mode for compilers that default to C90 (e.g. GCC 4.x).
53
+ # simd.h uses C99 for-loop declarations that fail under -std=gnu90.
54
+ if !$CFLAGS.include?('-std=') && try_cflags('-std=gnu99')
55
+ $CFLAGS += ' -std=gnu99'
56
+ end
57
+
51
58
  if enable_config('trace-log', false)
52
59
  dflags['OJ_ENABLE_TRACE_LOG'] = 1
53
60
  end
data/ext/oj/intern.c CHANGED
@@ -50,7 +50,12 @@ static VALUE form_str(const char *str, size_t len) {
50
50
  }
51
51
 
52
52
  static VALUE form_sym(const char *str, size_t len) {
53
- return rb_to_symbol(rb_str_intern(rb_utf8_str_new(str, len)));
53
+ VALUE sym = rb_check_symbol_cstr(str, (long)len, oj_utf8_encoding);
54
+
55
+ if (Qnil == sym) {
56
+ sym = rb_str_intern(rb_utf8_str_new(str, len));
57
+ }
58
+ return sym;
54
59
  }
55
60
 
56
61
  static VALUE form_attr(const char *str, size_t len) {
@@ -243,61 +248,83 @@ static VALUE resolve_classpath(ParseInfo pi, const char *name, size_t len, int a
243
248
  return clas;
244
249
  }
245
250
 
251
+ typedef struct _internArgs {
252
+ KeyVal bucket; // last bucket in the chain for the key's slot
253
+ const char *key;
254
+ size_t len;
255
+ ParseInfo pi;
256
+ int auto_define;
257
+ VALUE error_class;
258
+ } *InternArgs;
259
+
260
+ // Resolves the class and only then adds it to the cache so a raise while
261
+ // resolving does not leave a key cached with no value.
262
+ static VALUE intern_new_class(VALUE a) {
263
+ InternArgs args = (InternArgs)a;
264
+ KeyVal bucket = args->bucket;
265
+ VALUE clas = resolve_classpath(args->pi, args->key, args->len, args->auto_define, args->error_class);
266
+
267
+ if (NULL != bucket->key) { // not the top slot
268
+ KeyVal b = OJ_R_ALLOC(struct _keyVal);
269
+
270
+ b->next = NULL;
271
+ bucket->next = b;
272
+ bucket = b;
273
+ }
274
+ bucket->key = oj_strndup(args->key, args->len);
275
+ bucket->len = args->len;
276
+ bucket->val = clas;
277
+
278
+ return clas;
279
+ }
280
+
281
+ static KeyVal find_class(KeyVal bucket, const char *key, size_t len, VALUE *clas) {
282
+ if (NULL != bucket->key) { // not the top slot
283
+ for (KeyVal b = bucket; 0 != b; b = b->next) {
284
+ if (len == b->len && 0 == strncmp(b->key, key, len)) {
285
+ *clas = b->val;
286
+ return NULL;
287
+ }
288
+ bucket = b;
289
+ }
290
+ }
291
+ return bucket;
292
+ }
293
+
246
294
  VALUE oj_class_intern(const char *key, size_t len, bool safe, ParseInfo pi, int auto_define, VALUE error_class) {
247
- uint64_t h = hash_calc((const uint8_t *)key, len) & HASH_MASK;
248
- KeyVal bucket = class_hash.slots + h;
249
- KeyVal b;
295
+ uint64_t h = hash_calc((const uint8_t *)key, len) & HASH_MASK;
296
+ struct _internArgs args = {NULL, key, len, pi, auto_define, error_class};
297
+ VALUE clas = Qundef;
250
298
 
251
299
  if (safe) {
300
+ int state = 0;
301
+
252
302
  #if HAVE_PTHREAD_MUTEX_INIT
253
303
  pthread_mutex_lock(&class_hash.mutex);
254
304
  #else
255
305
  rb_mutex_lock(class_hash.mutex);
256
306
  #endif
257
- if (NULL != bucket->key) { // not the top slot
258
- for (b = bucket; 0 != b; b = b->next) {
259
- if (len == b->len && 0 == strncmp(b->key, key, len)) {
260
- #if HAVE_PTHREAD_MUTEX_INIT
261
- pthread_mutex_unlock(&class_hash.mutex);
262
- #else
263
- rb_mutex_unlock(class_hash.mutex);
264
- #endif
265
- return b->val;
266
- }
267
- bucket = b;
268
- }
269
- b = OJ_R_ALLOC(struct _keyVal);
270
- b->next = NULL;
271
- bucket->next = b;
272
- bucket = b;
307
+ // Resolving calls into Ruby and allocating, either of which can
308
+ // raise and unwind past the unlock, so the work is protected and any
309
+ // exception rethrown after the mutex is released.
310
+ if (NULL != (args.bucket = find_class(class_hash.slots + h, key, len, &clas))) {
311
+ clas = rb_protect(intern_new_class, (VALUE)&args, &state);
273
312
  }
274
- bucket->key = oj_strndup(key, len);
275
- bucket->len = len;
276
- bucket->val = resolve_classpath(pi, key, len, auto_define, error_class);
277
313
  #if HAVE_PTHREAD_MUTEX_INIT
278
314
  pthread_mutex_unlock(&class_hash.mutex);
279
315
  #else
280
316
  rb_mutex_unlock(class_hash.mutex);
281
317
  #endif
282
- } else {
283
- if (NULL != bucket->key) {
284
- for (b = bucket; 0 != b; b = b->next) {
285
- if (len == b->len && 0 == strncmp(b->key, key, len)) {
286
- return (ID)b->val;
287
- }
288
- bucket = b;
289
- }
290
- b = OJ_R_ALLOC(struct _keyVal);
291
- b->next = NULL;
292
- bucket->next = b;
293
- bucket = b;
318
+ if (0 != state) {
319
+ rb_jump_tag(state);
294
320
  }
295
- bucket->key = oj_strndup(key, len);
296
- bucket->len = len;
297
- bucket->val = resolve_classpath(pi, key, len, auto_define, error_class);
321
+ } else if (NULL != (args.bucket = find_class(class_hash.slots + h, key, len, &clas))) {
322
+ clas = intern_new_class((VALUE)&args);
298
323
  }
299
- rb_gc_register_mark_object(bucket->val);
300
- return bucket->val;
324
+ if (NULL != args.bucket) { // newly added to the cache
325
+ rb_gc_register_mark_object(clas);
326
+ }
327
+ return clas;
301
328
  }
302
329
 
303
330
  char *oj_strndup(const char *s, size_t len) {
data/ext/oj/mimic_json.c CHANGED
@@ -461,7 +461,7 @@ oj_mimic_pretty_generate(int argc, VALUE *argv, VALUE self) {
461
461
  if (1 == argc || Qnil == argv[1]) {
462
462
  h = rb_hash_new();
463
463
  } else {
464
- h = argv[1];
464
+ h = rb_hash_dup(argv[1]);
465
465
  }
466
466
  if (!oj_hash_has_key(h, oj_indent_sym)) {
467
467
  rb_hash_aset(h, oj_indent_sym, rb_str_new2(" "));
data/ext/oj/oj.c CHANGED
@@ -1241,45 +1241,84 @@ static bool parse_one_option(VALUE k, VALUE v, Options copts) {
1241
1241
  return true;
1242
1242
  }
1243
1243
 
1244
- // Collects the return of parse_one_option() over every key in a hash.
1244
+ // Collects the results of parse_one_option() over every key in a hash.
1245
1245
  struct _optsParse {
1246
1246
  Options copts;
1247
- bool consumed;
1247
+ bool consumed; // some key was an Oj option
1248
+ VALUE orig; // the caller's hash, never modified
1249
+ VALUE rest; // orig, or a copy of it without the Oj options
1248
1250
  };
1249
1251
 
1252
+ static void drop_from_rest(struct _optsParse *op, VALUE k) {
1253
+ if (op->rest == op->orig) {
1254
+ op->rest = rb_hash_dup(op->orig);
1255
+ }
1256
+ rb_hash_delete(op->rest, k);
1257
+ }
1258
+
1250
1259
  static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1251
1260
  struct _optsParse *op = (struct _optsParse *)opts;
1252
1261
 
1253
1262
  if (parse_one_option(k, v, op->copts)) {
1254
1263
  op->consumed = true;
1264
+ // :only and :except are also read by ActiveSupport's as_json, which
1265
+ // compat mode reaches through to_json, so they are left in.
1266
+ if (only_sym != k && except_sym != k) {
1267
+ drop_from_rest(op, k);
1268
+ }
1255
1269
  }
1256
1270
  return ST_CONTINUE;
1257
1271
  }
1258
1272
 
1259
- // Apply the options in ropts to copts and report whether any key was
1260
- // recognized. A hash of nothing but unknown keys leaves copts untouched and
1261
- // returns false.
1262
- bool oj_parse_options_consumed(VALUE ropts, Options copts) {
1263
- struct _optsParse op = {copts, false};
1273
+ static void parse_options(VALUE ropts, Options copts, struct _optsParse *op) {
1274
+ op->copts = copts;
1275
+ op->consumed = false;
1276
+ op->orig = ropts;
1277
+ op->rest = ropts;
1264
1278
 
1265
1279
  if (T_HASH != rb_type(ropts)) {
1266
- return false;
1280
+ return;
1267
1281
  }
1268
- rb_hash_foreach(ropts, parse_options_cb, (VALUE)&op);
1282
+ rb_hash_foreach(ropts, parse_options_cb, (VALUE)op);
1269
1283
  if (Qnil != rb_hash_lookup(ropts, match_string_sym)) {
1270
- op.consumed = true;
1284
+ op->consumed = true;
1285
+ drop_from_rest(op, match_string_sym);
1271
1286
  }
1272
1287
  oj_parse_opt_match_string(&copts->str_rx, ropts);
1273
1288
 
1274
1289
  copts->dump_opts.use = (0 < copts->dump_opts.indent_size || 0 < copts->dump_opts.after_size ||
1275
1290
  0 < copts->dump_opts.before_size || 0 < copts->dump_opts.hash_size ||
1276
1291
  0 < copts->dump_opts.array_size);
1292
+ }
1293
+
1294
+ // Apply the options in ropts to copts and report whether any key was
1295
+ // recognized. A hash of nothing but unknown keys leaves copts untouched and
1296
+ // returns false.
1297
+ bool oj_parse_options_consumed(VALUE ropts, Options copts) {
1298
+ struct _optsParse op;
1299
+
1300
+ parse_options(ropts, copts, &op);
1277
1301
 
1278
1302
  return op.consumed;
1279
1303
  }
1280
1304
 
1305
+ // Apply the options in ropts to copts and return what is left for the
1306
+ // to_json and as_json methods that Oj.dump() calls: ropts itself when it
1307
+ // holds no Oj option, otherwise a copy without them. The json gem's
1308
+ // generator raises on options it does not know as of json 3.0, so Oj's own
1309
+ // must not reach it. ropts is never modified.
1310
+ VALUE oj_parse_options_rest(VALUE ropts, Options copts) {
1311
+ struct _optsParse op;
1312
+
1313
+ parse_options(ropts, copts, &op);
1314
+
1315
+ return op.rest;
1316
+ }
1317
+
1281
1318
  void oj_parse_options(VALUE ropts, Options copts) {
1282
- oj_parse_options_consumed(ropts, copts);
1319
+ struct _optsParse op;
1320
+
1321
+ parse_options(ropts, copts, &op);
1283
1322
  }
1284
1323
 
1285
1324
  // Free the option buffers that oj_parse_options() allocated for a single call.
@@ -1703,6 +1742,7 @@ static VALUE dump(int argc, VALUE *argv, VALUE self) {
1703
1742
  struct dump_arg arg;
1704
1743
  struct _out out;
1705
1744
  struct _options copts = oj_default_options;
1745
+ VALUE to_json_argv[2];
1706
1746
 
1707
1747
  if (1 > argc) {
1708
1748
  rb_raise(rb_eArgError, "wrong number of arguments (0 for 1).");
@@ -1711,7 +1751,11 @@ static VALUE dump(int argc, VALUE *argv, VALUE self) {
1711
1751
  copts.dump_opts.nan_dump = WordNan;
1712
1752
  }
1713
1753
  if (2 == argc) {
1714
- oj_parse_options(argv[1], &copts);
1754
+ // The options hash is also what to_json and as_json receive, so
1755
+ // hand them the hash without the options Oj itself acted on.
1756
+ to_json_argv[0] = argv[0];
1757
+ to_json_argv[1] = oj_parse_options_rest(argv[1], &copts);
1758
+ argv = to_json_argv;
1715
1759
  }
1716
1760
  if (CompatMode == copts.mode && copts.escape_mode != ASCIIEsc) {
1717
1761
  copts.escape_mode = JSONEsc;
data/ext/oj/oj.h CHANGED
@@ -255,13 +255,14 @@ extern VALUE oj_compat_parse_cstr(int argc, VALUE *argv, char *json, size_t len)
255
255
  extern VALUE oj_object_parse_cstr(int argc, VALUE *argv, char *json, size_t len);
256
256
  extern VALUE oj_custom_parse_cstr(int argc, VALUE *argv, char *json, size_t len);
257
257
 
258
- extern bool oj_hash_has_key(VALUE hash, VALUE key);
259
- extern void oj_parse_options(VALUE ropts, Options copts);
260
- extern bool oj_parse_options_consumed(VALUE ropts, Options copts);
261
- extern void oj_free_call_options(Options copts);
262
- extern void oj_options_take_ownership(Options copts);
263
- extern void oj_options_release(Options copts);
264
- extern void oj_options_mark(Options copts);
258
+ extern bool oj_hash_has_key(VALUE hash, VALUE key);
259
+ extern void oj_parse_options(VALUE ropts, Options copts);
260
+ extern bool oj_parse_options_consumed(VALUE ropts, Options copts);
261
+ extern VALUE oj_parse_options_rest(VALUE ropts, Options copts);
262
+ extern void oj_free_call_options(Options copts);
263
+ extern void oj_options_take_ownership(Options copts);
264
+ extern void oj_options_release(Options copts);
265
+ extern void oj_options_mark(Options copts);
265
266
 
266
267
  extern void oj_dump_obj_to_json(VALUE obj, Options copts, Out out);
267
268
  extern void oj_dump_obj_to_json_using_params(VALUE obj, Options copts, Out out, int argc, VALUE *argv);
data/ext/oj/parse.c CHANGED
@@ -1261,7 +1261,11 @@ oj_pi_parse(int argc, VALUE *argv, ParseInfo pi, char *json, size_t len, int yie
1261
1261
  // value stack (while it is in scope).
1262
1262
  wrapped_stack = oj_stack_init(&pi->stack);
1263
1263
  rb_protect(protect_parse, (VALUE)pi, &line);
1264
- if (Qundef == pi->stack.head->val && !empty_ok(&pi->options)) {
1264
+ // Only when nothing is on the stack was the input actually empty. A
1265
+ // deferred hash (built at its closing brace from buffered pairs) leaves
1266
+ // head->val Qundef while its entry is still on the stack, e.g. when a
1267
+ // parse error cut the document short; the real error is already set.
1268
+ if (Qundef == pi->stack.head->val && stack_empty(&pi->stack) && !empty_ok(&pi->options)) {
1265
1269
  if (No == pi->options.nilnil || (CompatMode == pi->options.mode && 0 < pi->cur - pi->json)) {
1266
1270
  oj_set_error_at(pi, oj_json_parser_error_class, __FILE__, __LINE__, "Empty input");
1267
1271
  }
data/ext/oj/parse.h CHANGED
@@ -5,6 +5,7 @@
5
5
  #define OJ_PARSE_H
6
6
 
7
7
  #include <stdarg.h>
8
+ #include <stddef.h>
8
9
  #include <stdio.h>
9
10
  #include <string.h>
10
11
 
@@ -90,7 +91,19 @@ extern VALUE oj_cstr_to_value(const char *str, size_t len, size_t cache_str);
90
91
  extern VALUE oj_calc_hash_key(ParseInfo pi, Val parent);
91
92
 
92
93
  static inline void parse_info_init(ParseInfo pi) {
93
- memset(pi, 0, sizeof(struct _parseInfo));
94
+ // Zeroing the whole struct costs more than parsing a small document: the
95
+ // embedded reader buffer (rd.base, 4KB) and value stack entries
96
+ // (stack.base, 6.5KB) dominate its 11KB. Both are fully initialized
97
+ // before use — oj_reader_init() for stream parses, oj_stack_init() and
98
+ // stack_push() for the value stack — so zero only the scalars around
99
+ // them.
100
+ memset(pi, 0, offsetof(struct _parseInfo, rd));
101
+ memset((char *)pi + offsetof(struct _parseInfo, rd) + offsetof(struct _reader, head),
102
+ 0,
103
+ offsetof(struct _parseInfo, stack) - offsetof(struct _parseInfo, rd) - offsetof(struct _reader, head));
104
+ memset((char *)pi + offsetof(struct _parseInfo, stack) + offsetof(struct _valStack, head),
105
+ 0,
106
+ sizeof(struct _parseInfo) - offsetof(struct _parseInfo, stack) - offsetof(struct _valStack, head));
94
107
  }
95
108
 
96
109
  extern void oj_scanner_init(void);
data/ext/oj/parser.c CHANGED
@@ -509,20 +509,29 @@ static void calc_num(ojParser p) {
509
509
  d = -d;
510
510
  }
511
511
  if (0 < p->num.shift) {
512
- d /= pow_map[p->num.shift];
513
- }
514
- if (0 < p->num.exp) {
515
- long double x;
512
+ uint32_t shift = p->num.shift;
516
513
 
517
- if (MAX_POW < p->num.exp) {
518
- x = powl(10.0L, (long double)p->num.exp);
519
- } else {
520
- x = pow_map[p->num.exp];
514
+ while (MAX_POW < shift) {
515
+ d /= pow_map[MAX_POW];
516
+ shift -= MAX_POW;
521
517
  }
518
+ d /= pow_map[shift];
519
+ }
520
+ if (0 < p->num.exp) {
522
521
  if (p->num.exp_neg) {
523
- d /= x;
522
+ int exp = p->num.exp;
523
+
524
+ while (MAX_POW < exp) {
525
+ d /= pow_map[MAX_POW];
526
+ exp -= MAX_POW;
527
+ }
528
+ d /= pow_map[exp];
524
529
  } else {
525
- d *= x;
530
+ if (MAX_POW < p->num.exp) {
531
+ d *= powl(10.0L, (long double)p->num.exp);
532
+ } else {
533
+ d *= pow_map[p->num.exp];
534
+ }
526
535
  }
527
536
  }
528
537
  p->num.dub = d;
@@ -559,19 +568,31 @@ static void big_change(ojParser p) {
559
568
  break;
560
569
  case OJ_DECIMAL: {
561
570
  int shift = p->num.shift;
571
+ int end = sizeof(buf) - 1;
562
572
 
563
- for (len = sizeof(buf) - 1; 0 < i; len--, i /= 10, shift--) {
564
- if (0 == shift) {
565
- buf[len] = '.';
566
- len--;
567
- }
568
- buf[len] = '0' + (i % 10);
569
- }
573
+ len = end;
574
+ do {
575
+ buf[--len] = '0' + (i % 10);
576
+ i /= 10;
577
+ } while (0 < i);
570
578
  if (p->num.neg) {
571
- buf[len] = '-';
572
- len--;
579
+ buf_append(&p->buf, '-');
580
+ }
581
+ if (end - len <= shift) {
582
+ buf_append_string(&p->buf, "0.", 2);
583
+ for (int cnt = shift - (end - len); 0 < cnt; cnt--) {
584
+ buf_append(&p->buf, '0');
585
+ }
586
+ buf_append_string(&p->buf, buf + len, end - len);
587
+ } else {
588
+ int decimal = end - shift;
589
+
590
+ buf_append_string(&p->buf, buf + len, decimal - len);
591
+ if (0 < shift) {
592
+ buf_append(&p->buf, '.');
593
+ buf_append_string(&p->buf, buf + decimal, shift);
594
+ }
573
595
  }
574
- buf_append_string(&p->buf, buf + len + 1, sizeof(buf) - len - 1);
575
596
  if (0 < p->num.exp) {
576
597
  int x = p->num.exp;
577
598
  int d, div;
@@ -585,6 +606,7 @@ static void big_change(ojParser p) {
585
606
  d = x / div % 10;
586
607
  if (started || 0 < d) {
587
608
  buf_append(&p->buf, '0' + d);
609
+ started = true;
588
610
  }
589
611
  }
590
612
  }
data/ext/oj/parser.h CHANGED
@@ -34,7 +34,7 @@ typedef struct _num {
34
34
  uint32_t len;
35
35
  int16_t div; // 10^div
36
36
  int16_t exp;
37
- uint8_t shift; // shift of fixnum to get decimal
37
+ uint32_t shift; // shift of fixnum to get decimal
38
38
  bool neg;
39
39
  bool exp_neg;
40
40
  // for numbers as strings, reuse buf
data/ext/oj/simd.h CHANGED
@@ -99,9 +99,11 @@ static inline int oj_ctz64_fallback(uint64_t x) {
99
99
  // GCC/Clang: check for header availability and include them
100
100
  // We include headers but use target attributes to enable instructions per-function
101
101
  // Include cpuid.h for __get_cpuid fallback when __builtin_cpu_supports is unavailable
102
+ #ifdef __has_include
102
103
  #if __has_include(<cpuid.h>)
103
104
  #include <cpuid.h>
104
105
  #endif
106
+ #endif
105
107
  #if defined(__SSE4_2__) || defined(__SSE2__)
106
108
  // If any SSE is enabled globally, x86intrin.h should be available
107
109
  #include <x86intrin.h>
@@ -109,6 +111,7 @@ static inline int oj_ctz64_fallback(uint64_t x) {
109
111
  #define HAVE_SIMD_SSE2 1
110
112
  #else
111
113
  // Try to include headers anyway for target attribute functions
114
+ #ifdef __has_include
112
115
  #if __has_include(<x86intrin.h>)
113
116
  #include <x86intrin.h>
114
117
  #define HAVE_SIMD_SSE4_2 1
@@ -123,6 +126,7 @@ static inline int oj_ctz64_fallback(uint64_t x) {
123
126
  #endif
124
127
  #endif
125
128
  #endif
129
+ #endif
126
130
 
127
131
  // Target attribute macros for function-level SIMD enabling
128
132
  #if defined(__clang__) || defined(__GNUC__)
data/ext/oj/strict.c CHANGED
@@ -47,7 +47,29 @@ VALUE oj_calc_hash_key(ParseInfo pi, Val parent) {
47
47
  return rkey;
48
48
  }
49
49
 
50
+ // With the default Hash class the pairs of the just closed hash sit on top
51
+ // of the pair buffer; build the Hash in one bulk insert with the final size
52
+ // known instead of one rb_hash_aset per pair (which converts the small
53
+ // ar_table to an st_table mid-insertion for hashes above 8 entries).
50
54
  static void hash_end(ParseInfo pi) {
55
+ if (Qnil == pi->options.hash_class) {
56
+ ValStack stack = &pi->stack;
57
+ Val hash = stack_peek(stack);
58
+ size_t cnt = hash->pcnt;
59
+ volatile VALUE h;
60
+
61
+ #if HAVE_RB_HASH_NEW_CAPA
62
+ h = rb_hash_new_capa((long)(cnt / 2));
63
+ #else
64
+ h = rb_hash_new();
65
+ #endif
66
+ if (0 < cnt) {
67
+ rb_hash_bulk_insert((long)cnt, stack->pairs + stack->pcnt - cnt, h);
68
+ stack->pcnt -= cnt;
69
+ hash->pcnt = 0;
70
+ }
71
+ hash->val = h;
72
+ }
51
73
  TRACE_PARSE_HASH_END(pi->options.trace, pi);
52
74
  }
53
75
 
@@ -84,13 +106,19 @@ static VALUE start_hash(ParseInfo pi) {
84
106
  return rb_class_new_instance(0, NULL, pi->options.hash_class);
85
107
  }
86
108
  TRACE_PARSE_IN(pi->options.trace, "start_hash", pi);
87
- return rb_hash_new();
109
+ // The Hash is built at hash_end from the buffered pairs.
110
+ return Qundef;
88
111
  }
89
112
 
90
113
  static void hash_set_cstr(ParseInfo pi, Val parent, const char *str, size_t len, const char *orig) {
91
114
  volatile VALUE rstr = oj_cstr_to_value(str, len, (size_t)pi->options.cache_str);
92
115
 
93
- rb_hash_aset(stack_peek(&pi->stack)->val, oj_calc_hash_key(pi, parent), rstr);
116
+ if (Qnil == pi->options.hash_class) {
117
+ stack_pair_push(&pi->stack, oj_calc_hash_key(pi, parent), rstr);
118
+ parent->pcnt += 2;
119
+ } else {
120
+ rb_hash_aset(stack_peek(&pi->stack)->val, oj_calc_hash_key(pi, parent), rstr);
121
+ }
94
122
  TRACE_PARSE_CALL(pi->options.trace, "set_string", pi, rstr);
95
123
  }
96
124
 
@@ -101,12 +129,22 @@ static void hash_set_num(ParseInfo pi, Val parent, NumInfo ni) {
101
129
  oj_set_error_at(pi, oj_parse_error_class, __FILE__, __LINE__, "not a number or other value");
102
130
  }
103
131
  v = oj_num_as_value(ni);
104
- rb_hash_aset(stack_peek(&pi->stack)->val, oj_calc_hash_key(pi, parent), v);
132
+ if (Qnil == pi->options.hash_class) {
133
+ stack_pair_push(&pi->stack, oj_calc_hash_key(pi, parent), v);
134
+ parent->pcnt += 2;
135
+ } else {
136
+ rb_hash_aset(stack_peek(&pi->stack)->val, oj_calc_hash_key(pi, parent), v);
137
+ }
105
138
  TRACE_PARSE_CALL(pi->options.trace, "set_number", pi, v);
106
139
  }
107
140
 
108
141
  static void hash_set_value(ParseInfo pi, Val parent, VALUE value) {
109
- rb_hash_aset(stack_peek(&pi->stack)->val, oj_calc_hash_key(pi, parent), value);
142
+ if (Qnil == pi->options.hash_class) {
143
+ stack_pair_push(&pi->stack, oj_calc_hash_key(pi, parent), value);
144
+ parent->pcnt += 2;
145
+ } else {
146
+ rb_hash_aset(stack_peek(&pi->stack)->val, oj_calc_hash_key(pi, parent), value);
147
+ }
110
148
  TRACE_PARSE_CALL(pi->options.trace, "set_value", pi, value);
111
149
  }
112
150
 
data/ext/oj/usual.c CHANGED
@@ -49,7 +49,12 @@ static VALUE form_str(const char *str, size_t len) {
49
49
  }
50
50
 
51
51
  static VALUE form_sym(const char *str, size_t len) {
52
- return rb_str_intern(rb_utf8_str_new(str, len));
52
+ VALUE sym = rb_check_symbol_cstr(str, (long)len, oj_utf8_encoding);
53
+
54
+ if (Qnil == sym) {
55
+ sym = rb_str_intern(rb_utf8_str_new(str, len));
56
+ }
57
+ return sym;
53
58
  }
54
59
 
55
60
  static VALUE form_attr(const char *str, size_t len) {
@@ -172,9 +177,9 @@ static VALUE str_key(ojParser p, Key kp) {
172
177
 
173
178
  static VALUE sym_key(ojParser p, Key kp) {
174
179
  if ((size_t)kp->len < sizeof(kp->buf)) {
175
- return rb_str_freeze(rb_str_intern(rb_utf8_str_new(kp->buf, kp->len)));
180
+ return form_sym(kp->buf, kp->len);
176
181
  }
177
- return rb_str_freeze(rb_str_intern(rb_utf8_str_new(kp->key, kp->len)));
182
+ return form_sym(kp->key, kp->len);
178
183
  }
179
184
 
180
185
  static ID get_attr_id(ojParser p, Key kp) {
data/ext/oj/val_stack.c CHANGED
@@ -21,6 +21,13 @@ static void stack_mark(void *ptr) {
21
21
  rb_mutex_lock(stack->mutex);
22
22
  rb_gc_mark(stack->mutex);
23
23
  #endif
24
+ {
25
+ size_t i;
26
+
27
+ for (i = 0; i < stack->pcnt; i++) {
28
+ rb_gc_mark(stack->pairs[i]);
29
+ }
30
+ }
24
31
  for (v = stack->head; v < stack->tail; v++) {
25
32
  if (Qnil != v->val && Qundef != v->val) {
26
33
  rb_gc_mark(v->val);
@@ -71,6 +78,9 @@ oj_stack_init(ValStack stack) {
71
78
  stack->head = stack->base;
72
79
  stack->end = stack->base + sizeof(stack->base) / sizeof(struct _val);
73
80
  stack->tail = stack->head;
81
+ stack->pairs = stack->pbase;
82
+ stack->pcnt = 0;
83
+ stack->pend = sizeof(stack->pbase) / sizeof(VALUE);
74
84
  stack->head->val = Qundef;
75
85
  stack->head->key = NULL;
76
86
  stack->head->key_val = Qundef;
@@ -79,6 +89,7 @@ oj_stack_init(ValStack stack) {
79
89
  stack->head->clas = Qundef;
80
90
  stack->head->klen = 0;
81
91
  stack->head->clen = 0;
92
+ stack->head->pcnt = 0;
82
93
  stack->head->next = NEXT_NONE;
83
94
 
84
95
  return TypedData_Wrap_Struct(oj_cstack_class, &oj_stack_type, stack);
data/ext/oj/val_stack.h CHANGED
@@ -37,16 +37,27 @@ typedef struct _val {
37
37
  OddArgs odd_args;
38
38
  size_t klen;
39
39
  size_t clen;
40
+ size_t pcnt; // VALUEs this open hash holds in the stack pair buffer
40
41
  char next; // ValNext
41
42
  char k1; // first original character in the key
42
43
  char kalloc;
43
44
  } *Val;
44
45
 
46
+ #define PAIR_BASE_CNT 64
47
+
45
48
  typedef struct _valStack {
46
49
  struct _val base[STACK_INC];
47
- Val head; // current stack
48
- Val end; // stack end
49
- Val tail; // pointer to one past last element name on stack
50
+ VALUE pbase[PAIR_BASE_CNT]; // initial buffer for pairs
51
+ // Flat key/value pair buffer for hash modes that build each Hash in one
52
+ // rb_hash_bulk_insert at the closing brace instead of one rb_hash_aset
53
+ // per pair. Nesting is strictly LIFO so each open hash's pairs occupy
54
+ // the top of the buffer; the per-hash count lives in its Val's clen.
55
+ VALUE *pairs;
56
+ size_t pcnt; // number of live VALUEs in pairs
57
+ size_t pend; // capacity of pairs in VALUEs
58
+ Val head; // current stack
59
+ Val end; // stack end
60
+ Val tail; // pointer to one past last element name on stack
50
61
  #ifdef HAVE_PTHREAD_MUTEX_INIT
51
62
  pthread_mutex_t mutex;
52
63
  #else
@@ -76,6 +87,11 @@ inline static void stack_cleanup(ValStack stack) {
76
87
  OJ_R_FREE(stack->head);
77
88
  stack->head = NULL;
78
89
  }
90
+ if (stack->pbase != stack->pairs) {
91
+ OJ_R_FREE(stack->pairs);
92
+ stack->pairs = NULL;
93
+ }
94
+ stack->pcnt = 0;
79
95
  }
80
96
 
81
97
  inline static void stack_push(ValStack stack, VALUE val, ValNext next) {
@@ -124,6 +140,7 @@ inline static void stack_push(ValStack stack, VALUE val, ValNext next) {
124
140
  stack->tail->key_val = Qundef;
125
141
  stack->tail->clen = 0;
126
142
  stack->tail->klen = 0;
143
+ stack->tail->pcnt = 0;
127
144
  stack->tail->kalloc = 0;
128
145
  stack->tail++;
129
146
  }
@@ -165,6 +182,40 @@ inline static Val stack_pop(ValStack stack) {
165
182
  return 0;
166
183
  }
167
184
 
185
+ // Append one key/value pair to the pair buffer. The same GC discipline as
186
+ // stack_push: the allocation happens outside the mutex (it can trigger a GC
187
+ // which takes the mutex in stack_mark) and the buffer pointer only changes
188
+ // under the mutex.
189
+ inline static void stack_pair_push(ValStack stack, VALUE key, VALUE value) {
190
+ if (stack->pend <= stack->pcnt + 2) {
191
+ size_t cnt = stack->pend * 2;
192
+ VALUE *pairs;
193
+
194
+ if (stack->pbase == stack->pairs) {
195
+ pairs = OJ_R_ALLOC_N(VALUE, cnt);
196
+ memcpy(pairs, stack->pairs, sizeof(VALUE) * stack->pcnt);
197
+ } else {
198
+ pairs = stack->pairs;
199
+ OJ_R_REALLOC_N(pairs, VALUE, cnt);
200
+ }
201
+ #ifdef HAVE_PTHREAD_MUTEX_INIT
202
+ pthread_mutex_lock(&stack->mutex);
203
+ #else
204
+ rb_mutex_lock(stack->mutex);
205
+ #endif
206
+ stack->pairs = pairs;
207
+ stack->pend = cnt;
208
+ #ifdef HAVE_PTHREAD_MUTEX_INIT
209
+ pthread_mutex_unlock(&stack->mutex);
210
+ #else
211
+ rb_mutex_unlock(stack->mutex);
212
+ #endif
213
+ }
214
+ stack->pairs[stack->pcnt] = key;
215
+ stack->pairs[stack->pcnt + 1] = value;
216
+ stack->pcnt += 2;
217
+ }
218
+
168
219
  extern const char *oj_stack_next_string(ValNext n);
169
220
 
170
221
  #endif /* OJ_VAL_STACK_H */
data/lib/oj/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  module Oj
2
2
  # Current version of the module.
3
- VERSION = '3.17.6'
3
+ VERSION = '3.17.7'
4
4
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: oj
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.17.6
4
+ version: 3.17.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Ohler