oj 3.17.3 → 3.17.4

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: e7458dcdf494ef6b1b283ca86d51fba0b3102ecf5cec13f43682878c01708c80
4
- data.tar.gz: ab8099b8b275aa5acab45a012bbc96ef0c860e041be2c4ae7d6f7cd331da1755
3
+ metadata.gz: 8bbb833f16ca15e4591772c707b059e1db37eee8aaf02240cd2f628bf1876f4c
4
+ data.tar.gz: 8020f6e49607b38fb120a5a6ea4eea9e5ba3e73f35b3556b7defb0202e93e015
5
5
  SHA512:
6
- metadata.gz: 60d445fd27bbea120359c21d0c1b0ac1d4fbbd678c8fd28efe620706d382a2f0cc434645967c9bac0b6d08d1065fe99dc4b15e2623d98ca0e15a61eb591a993b
7
- data.tar.gz: b7736fd3a7b27f98ef3e4df05464000849403ff33a3f82ecd6d8c51b40d0c2753d23ed14c078eb26506d504de991e35ebe68e5703225c80aab6ebbd28b7a80dc
6
+ metadata.gz: df072519e6d25cc38a04328022741db7bfe3d39a1545337aa6828982eaf233673ee2e04bb697ffb037aa25e0521258881ea96204d8a5d46b0a93b80dc002e94e
7
+ data.tar.gz: d0683f49f90c751beeb45b2d1ffb39238fd7c0c42e8c2970ed76eeb8ab2f382221c3ef7cdbc7d619ea29c50b7b3bf29559a8ae1e3ec34aa9b56e13a8fb02eac5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,11 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.17.4 - 2026-07-14
4
+
5
+ - Fixed issue in fast.c where rescuing from a panic did not reset the where counter.
6
+
7
+ - Multiple fixes thanks to @Watson1978.
8
+
3
9
  ## 3.17.3 - 2026-06-04
4
10
 
5
11
  - Fixed issue in intern.c and fast.c.
data/ext/oj/dump_compat.c CHANGED
@@ -162,6 +162,7 @@ static void dump_array(VALUE a, int depth, Out out, bool as_ok) {
162
162
  }
163
163
  }
164
164
  } else {
165
+ assure_size(out, size);
165
166
  fill_indent(out, d2);
166
167
  }
167
168
  oj_dump_compat_val(RARRAY_AREF(a, i), d2, out, true);
data/ext/oj/dump_object.c CHANGED
@@ -10,6 +10,24 @@ static const char hex_chars[17] = "0123456789abcdef";
10
10
 
11
11
  static void dump_obj_attrs(VALUE obj, VALUE clas, slot_t id, int depth, Out out);
12
12
 
13
+ // An odd class is dumped as {"^O":class,...} and is rebuilt with a create
14
+ // function when loaded so there is no place to put a "^i" circular id on it.
15
+ // Registering it in the circular cache would consume an id that is never
16
+ // written and any later occurrence would then be dumped as a "^r" that refers
17
+ // to nothing. Instead dump odd classes in full each time they are encountered
18
+ // just like Time, Rational, and Complex are.
19
+ static void dump_circular_obj(VALUE obj, VALUE clas, int depth, Out out) {
20
+ if (NULL != oj_get_odd(clas)) {
21
+ dump_obj_attrs(obj, clas, 0, depth, out);
22
+ } else {
23
+ long id = oj_check_circular(obj, out);
24
+
25
+ if (0 <= id) {
26
+ dump_obj_attrs(obj, clas, id, depth, out);
27
+ }
28
+ }
29
+ }
30
+
13
31
  static void dump_time(VALUE obj, Out out) {
14
32
  switch (out->opts->time_format) {
15
33
  case RubyTime:
@@ -47,11 +65,7 @@ static void dump_data(VALUE obj, int depth, Out out, bool as_ok) {
47
65
  oj_dump_cstr(str, len, 0, 0, out);
48
66
  }
49
67
  } else {
50
- long id = oj_check_circular(obj, out);
51
-
52
- if (0 <= id) {
53
- dump_obj_attrs(obj, clas, id, depth, out);
54
- }
68
+ dump_circular_obj(obj, clas, depth, out);
55
69
  }
56
70
  }
57
71
  }
@@ -74,11 +88,7 @@ static void dump_obj(VALUE obj, int depth, Out out, bool as_ok) {
74
88
  oj_dump_raw(str, len, out);
75
89
  }
76
90
  } else {
77
- long id = oj_check_circular(obj, out);
78
-
79
- if (0 <= id) {
80
- dump_obj_attrs(obj, clas, id, depth, out);
81
- }
91
+ dump_circular_obj(obj, clas, depth, out);
82
92
  }
83
93
  }
84
94
 
data/ext/oj/fast.c CHANGED
@@ -92,7 +92,14 @@ inline static void next_non_white(ParseInfo pi) {
92
92
  case '\f':
93
93
  case '\n':
94
94
  case '\r': break;
95
- case '/': skip_comment(pi); break;
95
+ case '/':
96
+ skip_comment(pi);
97
+ // A comment that is not terminated by a newline ends on the null
98
+ // terminator. Stop here so the loop does not step past it.
99
+ if ('\0' == *pi->s) {
100
+ return;
101
+ }
102
+ break;
96
103
  default: return;
97
104
  }
98
105
  }
@@ -210,10 +217,11 @@ static void skip_comment(ParseInfo pi) {
210
217
  if ('*' == *pi->s && '/' == *(pi->s + 1)) {
211
218
  pi->s++;
212
219
  return;
213
- } else if ('\0' == *pi->s) {
214
- raise_error("comment not terminated", pi->str, pi->s);
215
220
  }
216
221
  }
222
+ // The loop only ends on the null terminator so the comment was never
223
+ // closed.
224
+ raise_error("comment not terminated", pi->str, pi->s);
217
225
  } else if ('/' == *pi->s) {
218
226
  for (; 1; pi->s++) {
219
227
  switch (*pi->s) {
@@ -710,21 +718,23 @@ static void mark_doc(void *ptr) {
710
718
  }
711
719
  #ifdef HAVE_RB_GC_MARK_MOVABLE
712
720
  static void compact_leaf(Leaf leaf) {
713
- switch (leaf->value_type) {
714
- case COL_VAL:
715
- if (NULL != leaf->elements) {
716
- Leaf first = leaf->elements->next;
717
- Leaf e = first;
721
+ if (NULL != leaf) {
722
+ switch (leaf->value_type) {
723
+ case COL_VAL:
724
+ if (NULL != leaf->elements) {
725
+ Leaf first = leaf->elements->next;
726
+ Leaf e = first;
718
727
 
719
- do {
720
- compact_leaf(e);
721
- e = e->next;
722
- } while (e != first);
723
- }
724
- break;
725
- case RUBY_VAL: leaf->value = rb_gc_location(leaf->value); break;
728
+ do {
729
+ compact_leaf(e);
730
+ e = e->next;
731
+ } while (e != first);
732
+ }
733
+ break;
734
+ case RUBY_VAL: leaf->value = rb_gc_location(leaf->value); break;
726
735
 
727
- default: break;
736
+ default: break;
737
+ }
728
738
  }
729
739
  }
730
740
 
@@ -790,14 +800,11 @@ static VALUE parse_json(VALUE clas, char *json, bool given) {
790
800
  doc->self = self;
791
801
  result = rb_protect(protect_open_proc, (VALUE)&pi, &ex);
792
802
  if (given || 0 != ex) {
803
+ // The doc is detached from its wrapper so the GC will not free it.
804
+ // doc_free() releases both the doc and its json buffer (doc->json), so
805
+ // the caller must not free json separately.
793
806
  DATA_PTR(doc->self) = NULL;
794
- // TBD is this needed?
795
- /*
796
807
  doc_free(pi.doc);
797
- if (0 != ex) { // will jump so caller will not free
798
- OJ_R_FREE(json);
799
- }
800
- */
801
808
  } else {
802
809
  result = doc->self;
803
810
  }
@@ -946,6 +953,7 @@ static void each_leaf(Doc doc, VALUE self) {
946
953
 
947
954
  doc->where++;
948
955
  if (MAX_STACK <= doc->where - doc->where_path) {
956
+ doc->where--;
949
957
  rb_raise(rb_const_get_at(Oj, rb_intern("DepthError")), "Path too deep. Limit is %d levels.", MAX_STACK);
950
958
  }
951
959
  do {
@@ -972,8 +980,9 @@ static int move_step(Doc doc, const char *path, int loc) {
972
980
  } else {
973
981
  Leaf leaf;
974
982
 
983
+ // A document with no root (empty or comment only JSON) has no leaf to
984
+ // step from so the path can not be located.
975
985
  if (0 == doc->where || 0 == (leaf = *doc->where)) {
976
- printf("*** Internal error at %s\n", path);
977
986
  return loc;
978
987
  }
979
988
  if ('.' == *path && '.' == *(path + 1)) {
@@ -1104,12 +1113,7 @@ static VALUE doc_open(VALUE clas, VALUE str) {
1104
1113
 
1105
1114
  memcpy(json, StringValuePtr(str), len);
1106
1115
  obj = parse_json(clas, json, given);
1107
- // TBD is this needed
1108
- /*
1109
- if (given) {
1110
- OJ_R_FREE(json);
1111
- }
1112
- */
1116
+ // json is owned by the doc and freed by doc_free(); do not free it here.
1113
1117
  return obj;
1114
1118
  }
1115
1119
 
@@ -1141,7 +1145,9 @@ static VALUE doc_open_file(VALUE clas, VALUE filename) {
1141
1145
  int given = rb_block_given_p();
1142
1146
 
1143
1147
  path = StringValuePtr(filename);
1144
- if (0 == (f = fopen(path, "r"))) {
1148
+ // Open in binary mode. On Windows a text mode read translates CRLF into LF
1149
+ // so fewer bytes are read than the file size reported by ftell().
1150
+ if (0 == (f = fopen(path, "rb"))) {
1145
1151
  rb_raise(rb_eIOError, "%s", strerror(errno));
1146
1152
  }
1147
1153
  fseek(f, 0, SEEK_END);
@@ -1159,12 +1165,7 @@ static VALUE doc_open_file(VALUE clas, VALUE filename) {
1159
1165
  fclose(f);
1160
1166
  json[len] = '\0';
1161
1167
  obj = parse_json(clas, json, given);
1162
- // TBD is this needed
1163
- /*
1164
- if (given) {
1165
- OJ_R_FREE(json);
1166
- }
1167
- */
1168
+ // json is owned by the doc and freed by doc_free(); do not free it here.
1168
1169
  return obj;
1169
1170
  }
1170
1171
 
@@ -1263,10 +1264,14 @@ static VALUE doc_path(VALUE self) {
1263
1264
  * #=> nil
1264
1265
  */
1265
1266
  static VALUE doc_local_key(VALUE self) {
1266
- Doc doc = self_doc(self);
1267
- Leaf leaf = *doc->where;
1268
- volatile VALUE key = Qnil;
1267
+ Doc doc = self_doc(self);
1268
+ Leaf leaf;
1269
+ volatile VALUE key = Qnil;
1269
1270
 
1271
+ if (NULL == doc->where || NULL == *doc->where) {
1272
+ return Qnil;
1273
+ }
1274
+ leaf = *doc->where;
1270
1275
  if (T_HASH == leaf->parent_type) {
1271
1276
  key = rb_utf8_str_new_cstr(leaf->key);
1272
1277
  } else if (T_ARRAY == leaf->parent_type) {
@@ -1421,6 +1426,9 @@ static VALUE doc_each_leaf(int argc, VALUE *argv, VALUE self) {
1421
1426
  return Qnil;
1422
1427
  }
1423
1428
  }
1429
+ if (NULL == doc->where || NULL == *doc->where) {
1430
+ return Qnil;
1431
+ }
1424
1432
  each_leaf(doc, self);
1425
1433
  if (0 < wlen) {
1426
1434
  memcpy(doc->where_path, save_path, sizeof(Leaf) * (wlen + 1));
data/ext/oj/object.c CHANGED
@@ -319,20 +319,26 @@ static int hat_value(ParseInfo pi, Val parent, const char *key, size_t klen, vol
319
319
  e1 = *RARRAY_CONST_PTR(value);
320
320
  // check for anonymous Struct
321
321
  if (T_ARRAY == rb_type(e1)) {
322
- VALUE args[1024];
323
- volatile VALUE rstr;
322
+ // Build the member-name list in a GC-managed Ruby array. Using a
323
+ // fixed C buffer (VALUE args[1024]) here overflowed the stack when
324
+ // the JSON supplied more than 1024 member names.
325
+ volatile VALUE names = rb_ary_new2(RARRAY_LEN(e1));
324
326
  size_t i;
325
327
  size_t cnt = RARRAY_LEN(e1);
326
328
 
327
329
  for (i = 0; i < cnt; i++) {
328
- rstr = RARRAY_AREF(e1, i);
329
- args[i] = rb_funcall(rstr, oj_to_sym_id, 0);
330
+ rb_ary_push(names, rb_funcall(RARRAY_AREF(e1, i), oj_to_sym_id, 0));
330
331
  }
331
- sc = rb_funcall2(rb_cStruct, oj_new_id, (int)cnt, args);
332
+ sc = rb_apply(rb_cStruct, oj_new_id, names);
332
333
  } else {
333
334
  // If struct is not defined then we let this fail and raise an exception.
334
335
  sc = oj_name2struct(pi, *RARRAY_CONST_PTR(value), rb_eArgError);
335
336
  }
337
+ if (Qundef == sc || Qnil == sc) {
338
+ // oj_name2struct already recorded the error; do not pass an
339
+ // unresolved (Qundef) class on to rb_obj_alloc/rb_class_new_instance.
340
+ return 1;
341
+ }
336
342
  if (sc == rb_cRange) {
337
343
  parent->val = rb_class_new_instance((int)(len - 1), RARRAY_CONST_PTR(value) + 1, rb_cRange);
338
344
  } else {
data/ext/oj/oj.c CHANGED
@@ -606,9 +606,52 @@ static VALUE get_def_opts(VALUE self) {
606
606
  * - *:trace* [_Boolean_] turn trace on or off.
607
607
  * - *:safe* [_Boolean_] turn safe mimic on or off.
608
608
  */
609
+ static ID oj_default_options_keeper_id = 0;
610
+
611
+ // The default options are a plain C global so the Ruby objects in them, the
612
+ // classes of the hash_class, array_class, and ignore options and the regexps
613
+ // and the classes of the match_string option, can not be reached by the GC and
614
+ // are collected while the defaults still refer to them. Holding them in an
615
+ // array the GC does know about keeps them alive. The array is rebuilt whenever
616
+ // the defaults change and replaces the previous one so that only the objects
617
+ // the defaults are currently using are held.
618
+ static void keep_default_options(void) {
619
+ VALUE keep = rb_ary_new();
620
+
621
+ if (0 == oj_default_options_keeper_id) {
622
+ oj_default_options_keeper_id = rb_intern("@default_options_keeper");
623
+ }
624
+ if (Qnil != oj_default_options.hash_class) {
625
+ rb_ary_push(keep, oj_default_options.hash_class);
626
+ }
627
+ if (Qnil != oj_default_options.array_class) {
628
+ rb_ary_push(keep, oj_default_options.array_class);
629
+ }
630
+ if (NULL != oj_default_options.ignore) {
631
+ VALUE *vp;
632
+
633
+ for (vp = oj_default_options.ignore; Qnil != *vp; vp++) {
634
+ rb_ary_push(keep, *vp);
635
+ }
636
+ }
637
+ oj_rxclass_keep(&oj_default_options.str_rx, keep);
638
+ // An instance variable of the Oj module is a reference the GC can follow on
639
+ // every engine, unlike a C global.
640
+ rb_ivar_set(Oj, oj_default_options_keeper_id, keep);
641
+ }
642
+
609
643
  static VALUE set_def_opts(VALUE self, VALUE opts) {
610
644
  Check_Type(opts, T_HASH);
645
+ // A :match_string option replaces the regexps instead of adding to them so
646
+ // the ones the defaults are holding have to be freed here, the only place
647
+ // that owns them. A per call options struct only aliases them.
648
+ if (Qnil != rb_hash_lookup(opts, match_string_sym)) {
649
+ oj_rxclass_cleanup(&oj_default_options.str_rx);
650
+ oj_default_options.str_rx.head = NULL;
651
+ oj_default_options.str_rx.tail = NULL;
652
+ }
611
653
  oj_parse_options(opts, &oj_default_options);
654
+ keep_default_options();
612
655
 
613
656
  return Qnil;
614
657
  }
@@ -921,6 +964,9 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
921
964
 
922
965
  len = RSTRING_LEN(v);
923
966
  if (len != copts->create_id_len || 0 != strcmp(copts->create_id, str)) {
967
+ if (&oj_default_options == copts && oj_json_class != copts->create_id) {
968
+ OJ_R_FREE((char *)copts->create_id);
969
+ }
924
970
  copts->create_id = OJ_R_ALLOC_N(char, len + 1);
925
971
  strcpy((char *)copts->create_id, str);
926
972
  copts->create_id_len = len;
@@ -1052,7 +1098,11 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1052
1098
  copts->array_class = v;
1053
1099
  }
1054
1100
  } else if (ignore_sym == k) {
1055
- OJ_R_FREE(copts->ignore);
1101
+ // Only free when replacing the defaults; a per-call copy's ignore still
1102
+ // aliases the default-owned buffer, which oj_free_call_options() frees.
1103
+ if (&oj_default_options == copts) {
1104
+ OJ_R_FREE(copts->ignore);
1105
+ }
1056
1106
  copts->ignore = NULL;
1057
1107
  if (Qnil != v) {
1058
1108
  size_t cnt;
@@ -1125,15 +1175,15 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1125
1175
  strncpy(copts->float_fmt, RSTRING_PTR(v), (size_t)RSTRING_LEN(v));
1126
1176
  copts->float_fmt[RSTRING_LEN(v)] = '\0';
1127
1177
  } else if (only_sym == k) {
1128
- if (NULL != copts->dump_opts.only) {
1178
+ // Only free when replacing the defaults; a per-call copy's pointer still
1179
+ // aliases the default-owned buffer, which oj_free_call_options() frees.
1180
+ if (&oj_default_options == copts && NULL != copts->dump_opts.only) {
1129
1181
  OJ_R_FREE((void *)copts->dump_opts.only);
1130
- copts->dump_opts.only = NULL;
1131
1182
  }
1132
1183
  copts->dump_opts.only = make_only_value(v);
1133
1184
  } else if (except_sym == k) {
1134
- if (NULL != copts->dump_opts.except) {
1185
+ if (&oj_default_options == copts && NULL != copts->dump_opts.except) {
1135
1186
  OJ_R_FREE((void *)copts->dump_opts.except);
1136
- copts->dump_opts.except = NULL;
1137
1187
  }
1138
1188
  copts->dump_opts.except = make_only_value(v);
1139
1189
  }
@@ -1153,6 +1203,120 @@ void oj_parse_options(VALUE ropts, Options copts) {
1153
1203
  return;
1154
1204
  }
1155
1205
 
1206
+ // Free the option buffers that oj_parse_options() allocated for a single call.
1207
+ // A per-call options struct is a copy of oj_default_options, so its create_id,
1208
+ // ignore, only, and except members initially alias the ones owned by the
1209
+ // defaults. Only free a member when it differs from the default, i.e. when it
1210
+ // was allocated for this call; the default-owned buffers must be left alone.
1211
+ void oj_free_call_options(Options copts) {
1212
+ if (NULL != copts->create_id && oj_default_options.create_id != copts->create_id) {
1213
+ OJ_R_FREE((char *)copts->create_id);
1214
+ copts->create_id = NULL;
1215
+ copts->create_id_len = 0;
1216
+ }
1217
+ if (NULL != copts->ignore && oj_default_options.ignore != copts->ignore) {
1218
+ OJ_R_FREE(copts->ignore);
1219
+ copts->ignore = NULL;
1220
+ }
1221
+ if (NULL != copts->dump_opts.only && oj_default_options.dump_opts.only != copts->dump_opts.only) {
1222
+ OJ_R_FREE((void *)copts->dump_opts.only);
1223
+ copts->dump_opts.only = NULL;
1224
+ }
1225
+ if (NULL != copts->dump_opts.except && oj_default_options.dump_opts.except != copts->dump_opts.except) {
1226
+ OJ_R_FREE((void *)copts->dump_opts.except);
1227
+ copts->dump_opts.except = NULL;
1228
+ }
1229
+ }
1230
+
1231
+ static char *dup_option_str(const char *str, size_t len) {
1232
+ char *buf = OJ_R_ALLOC_N(char, len + 1);
1233
+
1234
+ memcpy(buf, str, len);
1235
+ buf[len] = '\0';
1236
+
1237
+ return buf;
1238
+ }
1239
+
1240
+ // Give an options struct private copies of the option buffers it still shares
1241
+ // with the defaults so that it owns all of them.
1242
+ //
1243
+ // An options struct that outlives the call it was created in can not rely on
1244
+ // the default-owned buffers it aliases: Oj.default_options= is free to replace
1245
+ // them at any time, which would leave the alias dangling. Call this once, in
1246
+ // the call that creates such an object and after oj_parse_options(), then free
1247
+ // the buffers with oj_options_release() when the object is collected.
1248
+ void oj_options_take_ownership(Options copts) {
1249
+ if (NULL != copts->create_id && oj_default_options.create_id == copts->create_id) {
1250
+ copts->create_id = dup_option_str(copts->create_id, copts->create_id_len);
1251
+ }
1252
+ if (NULL != copts->ignore && oj_default_options.ignore == copts->ignore) {
1253
+ VALUE *vp;
1254
+ VALUE *buf;
1255
+ size_t cnt = 0;
1256
+
1257
+ for (vp = copts->ignore; Qnil != *vp; vp++) {
1258
+ cnt++;
1259
+ }
1260
+ buf = OJ_R_ALLOC_N(VALUE, cnt + 1);
1261
+ memcpy(buf, copts->ignore, sizeof(VALUE) * (cnt + 1));
1262
+ copts->ignore = buf;
1263
+ }
1264
+ if (NULL != copts->dump_opts.only && oj_default_options.dump_opts.only == copts->dump_opts.only) {
1265
+ copts->dump_opts.only = dup_option_str(copts->dump_opts.only, strlen(copts->dump_opts.only));
1266
+ }
1267
+ if (NULL != copts->dump_opts.except && oj_default_options.dump_opts.except == copts->dump_opts.except) {
1268
+ copts->dump_opts.except = dup_option_str(copts->dump_opts.except, strlen(copts->dump_opts.except));
1269
+ }
1270
+ }
1271
+
1272
+ // Free the option buffers of an options struct that owns all of them, i.e. one
1273
+ // that oj_options_take_ownership() was called on. Unlike oj_free_call_options()
1274
+ // this never looks at the defaults, so it is safe to call from a GC free
1275
+ // function, where the defaults may have been replaced since the object was
1276
+ // created.
1277
+ void oj_options_release(Options copts) {
1278
+ if (NULL != copts->create_id) {
1279
+ OJ_R_FREE((char *)copts->create_id);
1280
+ copts->create_id = NULL;
1281
+ copts->create_id_len = 0;
1282
+ }
1283
+ if (NULL != copts->ignore) {
1284
+ OJ_R_FREE(copts->ignore);
1285
+ copts->ignore = NULL;
1286
+ }
1287
+ if (NULL != copts->dump_opts.only) {
1288
+ OJ_R_FREE((void *)copts->dump_opts.only);
1289
+ copts->dump_opts.only = NULL;
1290
+ }
1291
+ if (NULL != copts->dump_opts.except) {
1292
+ OJ_R_FREE((void *)copts->dump_opts.except);
1293
+ copts->dump_opts.except = NULL;
1294
+ }
1295
+ // The str_rx chain is detached from the defaults when the owner is created
1296
+ // so the chain, if there is one, was built for this options struct alone.
1297
+ oj_rxclass_cleanup(&copts->str_rx);
1298
+ }
1299
+
1300
+ // Mark the Ruby objects held by an owned options struct. A long lived object
1301
+ // such as a writer or an encoder can be the only reference to the classes in
1302
+ // the options so without this they could be collected while the owner is still
1303
+ // alive leaving the options with dangling VALUEs.
1304
+ void oj_options_mark(Options copts) {
1305
+ if (Qnil != copts->hash_class) {
1306
+ rb_gc_mark(copts->hash_class);
1307
+ }
1308
+ if (Qnil != copts->array_class) {
1309
+ rb_gc_mark(copts->array_class);
1310
+ }
1311
+ if (NULL != copts->ignore) {
1312
+ VALUE *vp;
1313
+
1314
+ for (vp = copts->ignore; Qnil != *vp; vp++) {
1315
+ rb_gc_mark(*vp);
1316
+ }
1317
+ }
1318
+ }
1319
+
1156
1320
  static int match_string_cb(VALUE key, VALUE value, VALUE rx) {
1157
1321
  RxClass rc = (RxClass)rx;
1158
1322
 
@@ -1444,6 +1608,7 @@ static VALUE dump_ensure(VALUE a) {
1444
1608
  volatile struct dump_arg *arg = (void *)a;
1445
1609
 
1446
1610
  oj_out_free(arg->out);
1611
+ oj_free_call_options(arg->copts);
1447
1612
 
1448
1613
  return Qnil;
1449
1614
  }
@@ -1559,6 +1724,7 @@ static VALUE to_file(int argc, VALUE *argv, VALUE self) {
1559
1724
  oj_parse_options(argv[2], &copts);
1560
1725
  }
1561
1726
  oj_write_obj_to_file(argv[1], StringValuePtr(*argv), &copts);
1727
+ oj_free_call_options(&copts);
1562
1728
 
1563
1729
  return Qnil;
1564
1730
  }
@@ -1580,6 +1746,7 @@ static VALUE to_stream(int argc, VALUE *argv, VALUE self) {
1580
1746
  oj_parse_options(argv[2], &copts);
1581
1747
  }
1582
1748
  oj_write_obj_to_stream(argv[1], *argv, &copts);
1749
+ oj_free_call_options(&copts);
1583
1750
 
1584
1751
  return Qnil;
1585
1752
  }
data/ext/oj/oj.h CHANGED
@@ -256,6 +256,10 @@ extern VALUE oj_custom_parse_cstr(int argc, VALUE *argv, char *json, size_t len)
256
256
 
257
257
  extern bool oj_hash_has_key(VALUE hash, VALUE key);
258
258
  extern void oj_parse_options(VALUE ropts, Options copts);
259
+ extern void oj_free_call_options(Options copts);
260
+ extern void oj_options_take_ownership(Options copts);
261
+ extern void oj_options_release(Options copts);
262
+ extern void oj_options_mark(Options copts);
259
263
 
260
264
  extern void oj_dump_obj_to_json(VALUE obj, Options copts, Out out);
261
265
  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
@@ -1306,9 +1306,16 @@ CLEANUP:
1306
1306
  OJ_R_FREE(json);
1307
1307
  }
1308
1308
  stack_cleanup(&pi->stack);
1309
- if (pi->str_rx.head != oj_default_options.str_rx.head) {
1310
- oj_rxclass_cleanup(&pi->str_rx);
1309
+ // The parsers match against pi->options.str_rx. A :match_string option
1310
+ // builds a chain of its own for this call, which has to be freed. Without
1311
+ // the option the member still aliases the chain owned by the defaults and
1312
+ // must be left alone.
1313
+ if (pi->options.str_rx.head != oj_default_options.str_rx.head) {
1314
+ oj_rxclass_cleanup(&pi->options.str_rx);
1315
+ pi->options.str_rx.head = NULL;
1316
+ pi->options.str_rx.tail = NULL;
1311
1317
  }
1318
+ oj_free_call_options(&pi->options);
1312
1319
  if (err_has(&pi->err)) {
1313
1320
  rb_set_errinfo(Qnil);
1314
1321
  if (Qnil != pi->err_class) {
data/ext/oj/parse.h CHANGED
@@ -49,7 +49,6 @@ typedef struct _parseInfo {
49
49
  VALUE handler;
50
50
  struct _valStack stack;
51
51
  CircArray circ_array;
52
- struct _rxClass str_rx;
53
52
  int expect_value;
54
53
  int max_depth; // just for the json gem
55
54
  VALUE proc;
data/ext/oj/parser.c CHANGED
@@ -5,6 +5,7 @@
5
5
  #include <fcntl.h>
6
6
 
7
7
  #include "oj.h"
8
+ #include "simd.h"
8
9
 
9
10
  #define DEBUG 0
10
11
 
@@ -594,9 +595,64 @@ static void big_change(ojParser p) {
594
595
  }
595
596
  }
596
597
 
597
- static void parse(ojParser p, const byte *json, bool more) {
598
+ // Scan forward over string content, returning the first byte that is not
599
+ // STR_OK in string_map. This is a pure drop-in for the scalar loop
600
+ //
601
+ // for (; STR_OK == string_map[*b]; b++) {}
602
+ //
603
+ // and must return the exact same stop position. The stop set (bytes whose
604
+ // string_map class is not 'R'/STR_OK) is:
605
+ //
606
+ // * control bytes 0x00..0x1F (class '.', also stops at the NUL terminator)
607
+ // * the quote 0x22 '"' (class 'z')
608
+ // * the backslash 0x5C '\' (class 'A')
609
+ // * every high byte 0x80..0xFF (UTF-8 lead/continuation, classes M/P/Q/'.')
610
+ //
611
+ // Note this differs from parse.c's string_scan_neon, which stops only on
612
+ // \0 \\ " -- parser.c hands multi-byte UTF-8 to its state machine, so the
613
+ // scanner must stop on the high bytes too. The predictor below is derived from
614
+ // string_map, not copied from parse.c.
615
+ //
616
+ // The NEON path only loads a full 16-byte vector when [b, b+16) stays within
617
+ // [., end), so it never reads past the string's allocation; the sub-16-byte
618
+ // tail (and the whole scan on non-NEON builds) uses the scalar loop, which
619
+ // stops naturally at the guaranteed NUL terminator.
620
+ static inline const byte *oj_scan_str_simd(const byte *b, const byte *end) {
621
+ #ifdef HAVE_SIMD_NEON
622
+ if (SIMD_NEON == SIMD_Impl) {
623
+ const uint8x16_t quote = vdupq_n_u8('"');
624
+ const uint8x16_t bslash = vdupq_n_u8('\\');
625
+ const uint8x16_t space = vdupq_n_u8(0x20);
626
+ const uint8x16_t high = vdupq_n_u8(0x80);
627
+
628
+ while (b + sizeof(uint8x16_t) <= end) {
629
+ const uint8x16_t chunk = vld1q_u8((const uint8_t *)b);
630
+ // special lane == 0xFF for any byte that stops the scan.
631
+ const uint8x16_t special = vorrq_u8(vorrq_u8(vceqq_u8(chunk, quote), vceqq_u8(chunk, bslash)),
632
+ vorrq_u8(vcltq_u8(chunk, space), vcgeq_u8(chunk, high)));
633
+ // Reduce to a 64-bit mask with 4 bits per lane (same idiom as
634
+ // parse.c's string_scan_neon) and locate the first set lane.
635
+ const uint8x8_t res = vshrn_n_u16(vreinterpretq_u16_u8(special), 4);
636
+ uint64_t mask = vget_lane_u64(vreinterpret_u64_u8(res), 0);
637
+ if (0 != mask) {
638
+ mask &= 0x8888888888888888ull;
639
+ return b + (OJ_CTZ64(mask) >> 2);
640
+ }
641
+ b += sizeof(uint8x16_t);
642
+ }
643
+ }
644
+ #else
645
+ (void)end;
646
+ #endif
647
+ for (; STR_OK == string_map[*b]; b++) {
648
+ }
649
+ return b;
650
+ }
651
+
652
+ static void parse(ojParser p, const byte *json, size_t len, bool more) {
598
653
  const byte *start;
599
- const byte *b = json;
654
+ const byte *b = json;
655
+ const byte *end = json + len;
600
656
  int i;
601
657
 
602
658
  p->line = 1;
@@ -629,8 +685,7 @@ static void parse(ojParser p, const byte *json, bool more) {
629
685
  b++;
630
686
  p->key.tail = p->key.head;
631
687
  start = b;
632
- for (; STR_OK == string_map[*b]; b++) {
633
- }
688
+ b = oj_scan_str_simd(b, end);
634
689
  buf_append_string(&p->key, (const char *)start, b - start);
635
690
  if ('"' == *b) {
636
691
  p->map = colon_map;
@@ -654,8 +709,7 @@ static void parse(ojParser p, const byte *json, bool more) {
654
709
  b++;
655
710
  start = b;
656
711
  p->buf.tail = p->buf.head;
657
- for (; STR_OK == string_map[*b]; b++) {
658
- }
712
+ b = oj_scan_str_simd(b, end);
659
713
  buf_append_string(&p->buf, (const char *)start, b - start);
660
714
  if ('"' == *b) {
661
715
  p->cur = b - json;
@@ -905,8 +959,7 @@ static void parse(ojParser p, const byte *json, bool more) {
905
959
  break;
906
960
  case STR_OK:
907
961
  start = b;
908
- for (; STR_OK == string_map[*b]; b++) {
909
- }
962
+ b = oj_scan_str_simd(b, end);
910
963
  if (':' == p->next_map[256]) {
911
964
  buf_append_string(&p->key, (const char *)start, b - start);
912
965
  } else {
@@ -1419,7 +1472,7 @@ static VALUE parser_parse(VALUE self, VALUE json) {
1419
1472
 
1420
1473
  parser_reset(p);
1421
1474
  p->start(p);
1422
- parse(p, ptr, false);
1475
+ parse(p, ptr, (size_t)RSTRING_LEN(json), false);
1423
1476
  validate_document_end(p);
1424
1477
 
1425
1478
  return p->result(p);
@@ -1440,7 +1493,7 @@ static VALUE load(VALUE self) {
1440
1493
  while (true) {
1441
1494
  rb_funcall(p->reader, oj_readpartial_id, 2, INT2NUM(16385), rbuf);
1442
1495
  if (0 < RSTRING_LEN(rbuf)) {
1443
- parse(p, (byte *)StringValuePtr(rbuf), true);
1496
+ parse(p, (byte *)StringValuePtr(rbuf), (size_t)RSTRING_LEN(rbuf), true);
1444
1497
  }
1445
1498
  if (Qtrue == rb_funcall(p->reader, oj_eofq_id, 0)) {
1446
1499
  if (0 < p->depth) {
@@ -1510,7 +1563,7 @@ static VALUE parser_file(VALUE self, VALUE filename) {
1510
1563
  while (true) {
1511
1564
  if (0 < (rsize = read(fd, buf, size))) {
1512
1565
  buf[rsize] = '\0';
1513
- parse(p, buf, true);
1566
+ parse(p, buf, rsize, true);
1514
1567
  }
1515
1568
  if (rsize <= 0) {
1516
1569
  if (0 != rsize) {
data/ext/oj/rails.c CHANGED
@@ -623,6 +623,7 @@ static void encoder_free(void *ptr) {
623
623
  if (NULL != ptr) {
624
624
  Encoder e = (Encoder)ptr;
625
625
 
626
+ oj_options_release(&e->opts);
626
627
  if (NULL != e->ropts.table) {
627
628
  OJ_R_FREE(e->ropts.table);
628
629
  }
@@ -633,10 +634,19 @@ static void encoder_free(void *ptr) {
633
634
  static void encoder_mark(void *ptr) {
634
635
  if (NULL != ptr) {
635
636
  Encoder e = (Encoder)ptr;
637
+ int i;
636
638
 
639
+ oj_options_mark(&e->opts);
637
640
  if (Qnil != e->arg) {
638
641
  rb_gc_mark(e->arg);
639
642
  }
643
+ // The optimized classes in the encoder table are not reachable from
644
+ // anywhere else once optimize() is called on the encoder itself.
645
+ for (i = 0; i < e->ropts.len; i++) {
646
+ if (Qnil != e->ropts.table[i].clas) {
647
+ rb_gc_mark(e->ropts.table[i].clas);
648
+ }
649
+ }
640
650
  }
641
651
  }
642
652
 
@@ -661,6 +671,11 @@ static VALUE encoder_new(int argc, VALUE *argv, VALUE self) {
661
671
  Encoder e = OJ_R_ALLOC(struct _encoder);
662
672
 
663
673
  e->opts = oj_default_options;
674
+ // Detach from the match_string regexps owned by the defaults before the
675
+ // options are parsed so that a :match_string option builds a chain of its
676
+ // own that is freed with the encoder.
677
+ e->opts.str_rx.head = NULL;
678
+ e->opts.str_rx.tail = NULL;
664
679
  copy_opts(&ropts, &e->ropts);
665
680
 
666
681
  if (1 <= argc && Qnil != *argv) {
@@ -669,6 +684,7 @@ static VALUE encoder_new(int argc, VALUE *argv, VALUE self) {
669
684
  e->arg = rb_hash_new();
670
685
  }
671
686
  oj_parse_options(e->arg, &e->opts);
687
+ oj_options_take_ownership(&e->opts);
672
688
 
673
689
  return TypedData_Wrap_Struct(encoder_class, &oj_encoder_type, e);
674
690
  }
data/ext/oj/reader.c CHANGED
@@ -164,8 +164,14 @@ static VALUE partial_io_cb(VALUE rbuf) {
164
164
  }
165
165
  str = StringValuePtr(rstr);
166
166
  cnt = RSTRING_LEN(rstr);
167
- strcpy(reader->tail, str);
168
- reader->read_end = reader->tail + cnt;
167
+ if (cnt > (size_t)(reader->end - reader->tail)) {
168
+ // A misbehaving IO returned more than the requested number of bytes.
169
+ // Copying it in with strcpy() would overflow the reader buffer.
170
+ rb_raise(rb_eIOError, "read returned more than the requested number of bytes");
171
+ }
172
+ memcpy(reader->tail, str, cnt);
173
+ reader->tail[cnt] = '\0';
174
+ reader->read_end = reader->tail + cnt;
169
175
 
170
176
  return Qtrue;
171
177
  }
@@ -184,8 +190,14 @@ static VALUE io_cb(VALUE rbuf) {
184
190
  }
185
191
  str = StringValuePtr(rstr);
186
192
  cnt = RSTRING_LEN(rstr);
187
- strcpy(reader->tail, str);
188
- reader->read_end = reader->tail + cnt;
193
+ if (cnt > (size_t)(reader->end - reader->tail)) {
194
+ // A misbehaving IO returned more than the requested number of bytes.
195
+ // Copying it in with strcpy() would overflow the reader buffer.
196
+ rb_raise(rb_eIOError, "read returned more than the requested number of bytes");
197
+ }
198
+ memcpy(reader->tail, str, cnt);
199
+ reader->tail[cnt] = '\0';
200
+ reader->read_end = reader->tail + cnt;
189
201
 
190
202
  return Qtrue;
191
203
  }
data/ext/oj/rxclass.c CHANGED
@@ -142,3 +142,19 @@ void oj_rxclass_copy(RxClass src, RxClass dest) {
142
142
  }
143
143
  }
144
144
  }
145
+
146
+ // Add the Ruby objects in the chain to an array. The regexps and the classes
147
+ // can be referenced from nowhere else, as with a default match_string option,
148
+ // and are collected unless something holds onto them.
149
+ void oj_rxclass_keep(RxClass rc, VALUE keep) {
150
+ RxC rxc;
151
+
152
+ for (rxc = rc->head; NULL != rxc; rxc = rxc->next) {
153
+ if (Qnil != rxc->rrx) {
154
+ rb_ary_push(keep, rxc->rrx);
155
+ }
156
+ if (Qnil != rxc->clas) {
157
+ rb_ary_push(keep, rxc->clas);
158
+ }
159
+ }
160
+ }
data/ext/oj/rxclass.h CHANGED
@@ -22,5 +22,6 @@ extern int oj_rxclass_append(RxClass rc, const char *expr, VALUE clas);
22
22
  extern VALUE oj_rxclass_match(RxClass rc, const char *str, size_t len);
23
23
  extern void oj_rxclass_copy(RxClass src, RxClass dest);
24
24
  extern void oj_rxclass_rappend(RxClass rc, VALUE rx, VALUE clas);
25
+ extern void oj_rxclass_keep(RxClass rc, VALUE keep);
25
26
 
26
27
  #endif /* OJ_RXCLASS_H */
data/ext/oj/saj.c CHANGED
@@ -82,7 +82,14 @@ inline static void next_non_white(ParseInfo pi) {
82
82
  case '\f':
83
83
  case '\n':
84
84
  case '\r': break;
85
- case '/': skip_comment(pi); break;
85
+ case '/':
86
+ skip_comment(pi);
87
+ /* A comment that is not terminated by a newline ends on the null
88
+ * terminator. Stop here so the loop does not step past it. */
89
+ if ('\0' == *pi->s) {
90
+ return;
91
+ }
92
+ break;
86
93
  default: return;
87
94
  }
88
95
  }
@@ -118,14 +125,15 @@ static void skip_comment(ParseInfo pi) {
118
125
  if ('*' == *pi->s && '/' == *(pi->s + 1)) {
119
126
  pi->s++;
120
127
  return;
121
- } else if ('\0' == *pi->s) {
122
- if (pi->has_error) {
123
- call_error("comment not terminated", pi, __FILE__, __LINE__);
124
- } else {
125
- raise_error("comment not terminated", pi->str, pi->s);
126
- }
127
128
  }
128
129
  }
130
+ /* The loop only ends on the null terminator so the comment was never
131
+ * closed. */
132
+ if (pi->has_error) {
133
+ call_error("comment not terminated", pi, __FILE__, __LINE__);
134
+ } else {
135
+ raise_error("comment not terminated", pi->str, pi->s);
136
+ }
129
137
  } else if ('/' == *pi->s) {
130
138
  for (; 1; pi->s++) {
131
139
  switch (*pi->s) {
@@ -606,6 +614,19 @@ static void saj_parse(VALUE handler, char *json) {
606
614
  }
607
615
  }
608
616
 
617
+ struct _sajArgs {
618
+ VALUE handler;
619
+ char *json;
620
+ };
621
+
622
+ static VALUE protect_saj_parse(VALUE x) {
623
+ struct _sajArgs *args = (struct _sajArgs *)x;
624
+
625
+ saj_parse(args->handler, args->json);
626
+
627
+ return Qnil;
628
+ }
629
+
609
630
  /* call-seq: saj_parse(handler, io)
610
631
  *
611
632
  * Parses an IO stream or file containing an JSON document. Raises an exception
@@ -670,8 +691,17 @@ oj_saj_parse(int argc, VALUE *argv, VALUE self) {
670
691
  rb_raise(rb_eArgError, "saj_parse() expected a String or IO Object.");
671
692
  }
672
693
  }
673
- saj_parse(*argv, json);
674
- OJ_R_FREE(json);
675
-
694
+ {
695
+ // saj_parse() raises on a malformed document so the json buffer has to
696
+ // be freed even when the parse does not return normally.
697
+ struct _sajArgs args = {*argv, json};
698
+ int ex = 0;
699
+
700
+ rb_protect(protect_saj_parse, (VALUE)&args, &ex);
701
+ OJ_R_FREE(json);
702
+ if (0 != ex) {
703
+ rb_jump_tag(ex);
704
+ }
705
+ }
676
706
  return Qnil;
677
707
  }
data/ext/oj/sparse.c CHANGED
@@ -907,6 +907,15 @@ CLEANUP:
907
907
  oj_circ_array_free(pi->circ_array);
908
908
  }
909
909
  stack_cleanup(&pi->stack);
910
+ // A :match_string option builds a chain of regexps for this call that has to
911
+ // be freed. Without the option the member aliases the chain owned by the
912
+ // defaults and must be left alone.
913
+ if (pi->options.str_rx.head != oj_default_options.str_rx.head) {
914
+ oj_rxclass_cleanup(&pi->options.str_rx);
915
+ pi->options.str_rx.head = NULL;
916
+ pi->options.str_rx.tail = NULL;
917
+ }
918
+ oj_free_call_options(&pi->options);
910
919
  if (0 != fd) {
911
920
  #ifdef _WIN32
912
921
  rb_w32_close(fd);
@@ -16,15 +16,29 @@ static void stream_writer_free(void *ptr) {
16
16
  return;
17
17
  }
18
18
  sw = (StreamWriter)ptr;
19
+ oj_options_release(&sw->sw.opts);
19
20
  OJ_R_FREE(sw->sw.out.buf);
20
21
  OJ_R_FREE(sw->sw.types);
21
22
  OJ_R_FREE(ptr);
22
23
  }
23
24
 
25
+ static void stream_writer_mark(void *ptr) {
26
+ if (NULL != ptr) {
27
+ StreamWriter sw = (StreamWriter)ptr;
28
+
29
+ oj_options_mark(&sw->sw.opts);
30
+ // The writer can be the only reference to the stream as in
31
+ // Oj::StreamWriter.new(StringIO.new) so it must be marked.
32
+ if (Qnil != sw->stream) {
33
+ rb_gc_mark(sw->stream);
34
+ }
35
+ }
36
+ }
37
+
24
38
  static const rb_data_type_t oj_stream_writer_type = {
25
39
  "Oj/stream_writer",
26
40
  {
27
- NULL,
41
+ stream_writer_mark,
28
42
  stream_writer_free,
29
43
  NULL,
30
44
  },
@@ -116,6 +130,7 @@ static VALUE stream_writer_new(int argc, VALUE *argv, VALUE self) {
116
130
  oj_str_writer_init(&sw->sw, 4096);
117
131
  sw->flush_limit = 0;
118
132
  }
133
+ oj_options_take_ownership(&sw->sw.opts);
119
134
  sw->sw.out.indent = sw->sw.opts.indent;
120
135
  sw->stream = stream;
121
136
  sw->type = type;
@@ -42,12 +42,18 @@ static void maybe_comma(StrWriter sw) {
42
42
 
43
43
  // Used by stream writer also.
44
44
  void oj_str_writer_init(StrWriter sw, int buf_size) {
45
- sw->opts = oj_default_options;
46
- sw->depth = 0;
47
- sw->types = OJ_R_ALLOC_N(char, 256);
48
- sw->types_end = sw->types + 256;
49
- *sw->types = '\0';
50
- sw->keyWritten = 0;
45
+ sw->opts = oj_default_options;
46
+ // Detach from the match_string regexps owned by the defaults before the
47
+ // options are parsed. A :match_string option then builds a chain of its own
48
+ // that is freed with the writer instead of being appended to the chain the
49
+ // defaults are using.
50
+ sw->opts.str_rx.head = NULL;
51
+ sw->opts.str_rx.tail = NULL;
52
+ sw->depth = 0;
53
+ sw->types = OJ_R_ALLOC_N(char, 256);
54
+ sw->types_end = sw->types + 256;
55
+ *sw->types = '\0';
56
+ sw->keyWritten = 0;
51
57
 
52
58
  if (0 == buf_size) {
53
59
  buf_size = 4096;
@@ -234,15 +240,22 @@ static void string_writer_free(void *ptr) {
234
240
  sw = (StrWriter)ptr;
235
241
 
236
242
  oj_out_free(&sw->out);
243
+ oj_options_release(&sw->opts);
237
244
 
238
245
  OJ_R_FREE(sw->types);
239
246
  OJ_R_FREE(ptr);
240
247
  }
241
248
 
249
+ static void string_writer_mark(void *ptr) {
250
+ if (NULL != ptr) {
251
+ oj_options_mark(&((StrWriter)ptr)->opts);
252
+ }
253
+ }
254
+
242
255
  static const rb_data_type_t oj_string_writer_type = {
243
256
  "Oj/string_writer",
244
257
  {
245
- NULL,
258
+ string_writer_mark,
246
259
  string_writer_free,
247
260
  NULL,
248
261
  },
@@ -279,6 +292,7 @@ static VALUE str_writer_new(int argc, VALUE *argv, VALUE self) {
279
292
  if (1 == argc) {
280
293
  oj_parse_options(argv[0], &sw->opts);
281
294
  }
295
+ oj_options_take_ownership(&sw->opts);
282
296
  sw->out.argc = argc - 1;
283
297
  sw->out.argv = argv + 1;
284
298
  sw->out.indent = sw->opts.indent;
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.3'
3
+ VERSION = '3.17.4'
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.3
4
+ version: 3.17.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Ohler