oj 3.17.6 → 3.17.8

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: 887bd6056f15f0ed95de347bc87d12c00c2de9a071b972f16be14952c408f6a6
4
+ data.tar.gz: af830ec844053ce4c955ac193260d53f5edf88159038c30e5c047b8fd9b334d7
5
5
  SHA512:
6
- metadata.gz: e081fafa7bd5af792fef91dc58612d6ea85bdae7b3d709bbd945773a5ad9d1feec4c88e3844aa403098c3fa267814ef61dcbba5fb4d9ee86a239eecf5b3926fa
7
- data.tar.gz: 339ba6bedb36fb30a382f8cb5037053a4197d4e9364bb11f50b79ec6ec3002cdd687868e140fec1dacabdc9cfa972a45cfcd6cb8a85e4c4148cd484f56278004
6
+ metadata.gz: 1aa3a234183cfd1e86b6a2b08f91ec553a6048405042ca887647e43198a08ab06287329389bb0c0d4f6c73a637eff158e29da7f187ae4778ebab7379984c771d
7
+ data.tar.gz: 6e1604f05a6b48364549b350c9b18c4e9b1c5dd819881d82e172ee466ce9c10ff49fbb60309f56ec951ce99f0c2791ca31b94a12e23ebef2bde40c0d44a0a8c4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,72 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.17.8 - 2026-11-03
4
+
5
+ ### Fixed
6
+
7
+ - Fixed issue #988 by no longer requiring `ostruct` when Oj is loaded, autoloading `JSON::GenericObject` instead. (#1113)
8
+ - Always include `cpuid.h` and fix the cache typedef for older compilers. (#1112)
9
+ - Keep the sign of `-0.0` when dumping in compat and rails modes. (#1115)
10
+ - Fixed big negative integers and big decimals in `Oj::Parser.usual`. (#1116)
11
+ - Reject a value after a number and whitespace in `Oj::Parser`. (#1117)
12
+ - Combine an escaped surrogate pair in `Oj::Parser`. (#1118)
13
+ - Reject an object close after a colon in `Oj::Parser`. (#1119)
14
+ - Accept an exponent right after a zero integer part in `Oj::Parser`. (#1121)
15
+ - Raise for an incomplete top-level number in `Oj::Parser`. (#1122)
16
+ - Reject unescaped control characters in strict and compat mode strings. (#1123)
17
+ - Fixed memory and handle leaks in `Oj.load` with recent winpthreads on Windows. (#1125)
18
+ - Fixed writer state after an excess pop. (#1126)
19
+ - Fixed memory corruption when `^u` names a class that is not a Struct. (#1127)
20
+ - Fixed cached keys and strings containing NUL returning a different value. (#1129)
21
+ - Fixed a segmentation fault on a `create_id` class path through a non-module constant. (#1130)
22
+ - Fixed a segmentation fault with the `Oj::Parser` class cache after GC compaction. (#1131)
23
+ - Fixed a use-after-free of converted non-UTF-8 input in `Oj.load`. (#1132)
24
+ - Fixed a use-after-free of converted non-UTF-8 strings in `Oj.dump`. (#1133)
25
+ - Fixed a segmentation fault when `Oj::Parser#parse` is given a non-String. (#1134)
26
+ - Fixed a segmentation fault when `Oj.safe_load` is given a null StringIO. (#1135)
27
+ - Fixed a segmentation fault and memory leak in `Oj.saj_parse` with IO input. (#1136)
28
+ - Fixed a segmentation fault when `to_json` returns a non-String in custom mode. (#1137)
29
+ - Fixed a segmentation fault when an exception message is not a String in compat mode. (#1138)
30
+ - Fixed a segmentation fault when `raw_json` returns a non-String. (#1139)
31
+ - Fixed a use-after-free of keys in the `Oj::Parser` saj delegate. (#1140)
32
+ - Fixed wrong keys and `IndexError` for `Oj::Parser` saj handlers with only some callbacks. (#1141)
33
+ - Fixed a buffer overflow when dumping nested arrays with a String indent. (#1142)
34
+ - Fixed a segmentation fault when `array_class` is not an Array in compat and custom mode. (#1143)
35
+ - Fixed a double free when closing an `Oj::Doc` inside the open block. (#1144)
36
+ - Fixed buffer leaks and unchecked file sizes when reading special files. (#1145)
37
+ - Fixed reading freed memory when `Oj.load` parses an object through `to_s`. (#1146)
38
+ - Fixed reading a freed buffer when a `StringWriter` is pushed into itself. (#1147)
39
+ - Fixed a segmentation fault and use-after-free when using a closed `Oj::Doc`. (#1148)
40
+ - Fixed reading and writing a different file when a path contains a NUL byte. (#1149)
41
+ - Fixed buffer leaks when a dump to a String, file, or stream raises. (#1152)
42
+ - Fixed unknown class names filling the class cache and loading as a Hash. (#1153)
43
+ - Fixed object mode ranges and exceptions on Ruby 4.1. (#1154)
44
+ - Fixed a segmentation fault after `Oj::Doc#move` fails on a path with `..`. (#1155)
45
+ - Fixed a segmentation fault in wab mode after GC compaction. (#1156)
46
+ - Fixed OpenStruct dumps and loads changing after GC compaction. (#1157)
47
+ - Fixed a segmentation fault when pushing to a writer with `circular: true`. (#1158)
48
+ - Fixed exception loading and Range subclass dumps in custom mode on Ruby 4.1. (#1159)
49
+
50
+ ### Performance
51
+
52
+ - Speed up parsing of object keys that collide in the key cache by using SipHash. (#1150)
53
+ - Speed up dumping ASCII-only strings that are not UTF-8. (#1160)
54
+ - Speed up dumping objects in object and custom modes. (#1161)
55
+ - Speed up dumping plain objects in custom mode. (#1162)
56
+
57
+ ## 3.17.7 - 2026-09-29
58
+
59
+ ### Fixed
60
+ - 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.
61
+ - Speed up hash-heavy documents: skip the 11KB per-parse memset and bulk-insert hashes (#1096)
62
+ - Preserve decimal placement for large parser numbers (#1099)
63
+ - Keep zero digits in oversized parser exponents (#1100)
64
+ - Parse very small usual-mode numbers correctly (#1101)
65
+ - Speed up symbol keys by interning without a temporary String (#1102)
66
+ - Keep Oj's own options out of the hash passed to to_json (#1106)
67
+ - Ensure class-cache mutex is released on exceptions (#1108)
68
+ - Guard __has_include with an #ifdef to support older GCC versions (4.4.7 etc.) (#1111)
69
+
3
70
  ## 3.17.6 - 2026-08-10
4
71
 
5
72
  - 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/cache.c CHANGED
@@ -25,23 +25,41 @@
25
25
  #define CACHE_UNLOCK(c) rb_mutex_unlock((c)->mutex)
26
26
  #endif
27
27
 
28
- // almost the Murmur hash algorithm
29
- #define M 0x5bd1e995
30
-
31
- uint64_t oj_hash_seed = 0;
28
+ #define ROTL(x, b) (uint64_t)(((x) << (b)) | ((x) >> (64 - (b))))
29
+ #define SIPROUND \
30
+ do { \
31
+ v0 += v1; \
32
+ v1 = ROTL(v1, 13); \
33
+ v1 ^= v0; \
34
+ v0 = ROTL(v0, 32); \
35
+ v2 += v3; \
36
+ v3 = ROTL(v3, 16); \
37
+ v3 ^= v2; \
38
+ v0 += v3; \
39
+ v3 = ROTL(v3, 21); \
40
+ v3 ^= v0; \
41
+ v2 += v1; \
42
+ v1 = ROTL(v1, 17); \
43
+ v1 ^= v2; \
44
+ v2 = ROTL(v2, 32); \
45
+ } while (0)
46
+
47
+ static uint64_t hash_key[2];
32
48
 
33
49
  void oj_hash_seed_init(void) {
34
50
  static bool seeded = false;
51
+ VALUE seed;
52
+ VALUE mask;
35
53
 
36
54
  if (seeded) {
37
55
  return;
38
56
  }
39
- seeded = true;
40
- #if HAVE_RB_HASH_START
41
- oj_hash_seed = (uint64_t)rb_hash_start(0);
42
- #else
43
- oj_hash_seed = (uint64_t)NUM2LL(rb_funcall(rb_str_new_cstr("oj"), rb_intern("hash"), 0));
44
- #endif
57
+ seed = rb_funcall(rb_cRandom, rb_intern("new_seed"), 0);
58
+ mask = ULL2NUM(UINT64_MAX);
59
+ hash_key[0] = NUM2ULL(rb_funcall(seed, rb_intern("&"), 1, mask)) ^ (uint64_t)rb_memhash("oj0", 3);
60
+ hash_key[1] = NUM2ULL(rb_funcall(rb_funcall(seed, rb_intern(">>"), 1, INT2FIX(64)), rb_intern("&"), 1, mask)) ^
61
+ (uint64_t)rb_memhash("oj1", 3);
62
+ seeded = true;
45
63
  }
46
64
 
47
65
  typedef struct _slot {
@@ -53,7 +71,7 @@ typedef struct _slot {
53
71
  char key[CACHE_MAX_KEY];
54
72
  } *Slot;
55
73
 
56
- typedef struct _cache {
74
+ struct _cache {
57
75
  volatile Slot *slots;
58
76
  volatile size_t cnt;
59
77
  VALUE (*form)(const char *str, size_t len);
@@ -69,44 +87,43 @@ typedef struct _cache {
69
87
  #endif
70
88
  uint8_t xrate;
71
89
  bool mark;
72
- } *Cache;
90
+ };
73
91
 
74
92
  void cache_set_form(Cache c, VALUE (*form)(const char *str, size_t len)) {
75
93
  c->form = form;
76
94
  }
77
95
 
78
- static uint64_t hash_calc(const uint8_t *key, size_t len) {
79
- const uint8_t *end = key + len;
80
- const uint8_t *endless = key + (len & 0xFFFFFFFC);
81
- uint64_t h = (uint64_t)len ^ oj_hash_seed;
82
- uint64_t k;
83
-
84
- while (key < endless) {
85
- k = (uint64_t)*key++;
86
- k |= (uint64_t)*key++ << 8;
87
- k |= (uint64_t)*key++ << 16;
88
- k |= (uint64_t)*key++ << 24;
89
-
90
- k *= M;
91
- k ^= k >> 24;
92
- h *= M;
93
- h ^= k * M;
94
- }
95
- if (1 < end - key) {
96
- uint16_t k16 = (uint16_t)*key++;
97
-
98
- k16 |= (uint16_t)*key++ << 8;
99
- h ^= k16 << 8;
96
+ uint64_t oj_hash_calc(const uint8_t *key, size_t len) {
97
+ const uint8_t *end = key + (len & ~(size_t)7);
98
+ uint64_t v0 = 0x736f6d6570736575ULL ^ hash_key[0];
99
+ uint64_t v1 = 0x646f72616e646f6dULL ^ hash_key[1];
100
+ uint64_t v2 = 0x6c7967656e657261ULL ^ hash_key[0];
101
+ uint64_t v3 = 0x7465646279746573ULL ^ hash_key[1];
102
+ uint64_t b = (uint64_t)len << 56;
103
+ uint64_t m;
104
+ size_t i;
105
+
106
+ for (; key < end; key += 8) {
107
+ m = 0;
108
+ for (i = 0; i < 8; i++) {
109
+ m |= (uint64_t)key[i] << (8 * i);
110
+ }
111
+ v3 ^= m;
112
+ SIPROUND;
113
+ v0 ^= m;
100
114
  }
101
- if (key < end) {
102
- h ^= *key;
115
+ for (i = 0; i < (len & 7); i++) {
116
+ b |= (uint64_t)key[i] << (8 * i);
103
117
  }
104
- h *= M;
105
- h ^= h >> 13;
106
- h *= M;
107
- h ^= h >> 15;
108
-
109
- return h;
118
+ v3 ^= b;
119
+ SIPROUND;
120
+ v0 ^= b;
121
+ v2 ^= 0xff;
122
+ SIPROUND;
123
+ SIPROUND;
124
+ SIPROUND;
125
+
126
+ return v0 ^ v1 ^ v2 ^ v3;
110
127
  }
111
128
 
112
129
  static void rehash(Cache c) {
@@ -137,7 +154,7 @@ static void rehash(Cache c) {
137
154
  }
138
155
 
139
156
  static VALUE lockless_intern(Cache c, const char *key, size_t len) {
140
- uint64_t h = hash_calc((const uint8_t *)key, len);
157
+ uint64_t h = oj_hash_calc((const uint8_t *)key, len);
141
158
  Slot *bucket = (Slot *)c->slots + (h & c->mask);
142
159
  Slot b;
143
160
  volatile VALUE rkey;
@@ -153,7 +170,7 @@ static VALUE lockless_intern(Cache c, const char *key, size_t len) {
153
170
  }
154
171
  }
155
172
  for (b = *bucket; NULL != b; b = b->next) {
156
- if ((uint8_t)len == b->klen && 0 == strncmp(b->key, key, len)) {
173
+ if ((uint8_t)len == b->klen && 0 == memcmp(b->key, key, len)) {
157
174
  b->use_cnt += 16;
158
175
  return b->val;
159
176
  }
@@ -198,10 +215,10 @@ static VALUE locking_intern(Cache c, const char *key, size_t len) {
198
215
  c->rcnt = 0;
199
216
  }
200
217
  }
201
- h = hash_calc((const uint8_t *)key, len);
218
+ h = oj_hash_calc((const uint8_t *)key, len);
202
219
  bucket = (Slot *)c->slots + (h & c->mask);
203
220
  for (b = *bucket; NULL != b; b = b->next) {
204
- if ((uint8_t)len == b->klen && 0 == strncmp(b->key, key, len)) {
221
+ if ((uint8_t)len == b->klen && 0 == memcmp(b->key, key, len)) {
205
222
  b->use_cnt += 4;
206
223
  CACHE_UNLOCK(c);
207
224
 
@@ -230,7 +247,7 @@ static VALUE locking_intern(Cache c, const char *key, size_t len) {
230
247
  // Lock again to add the new entry.
231
248
  CACHE_LOCK(c);
232
249
  if (old_size != c->size) {
233
- h = hash_calc((const uint8_t *)key, len);
250
+ h = oj_hash_calc((const uint8_t *)key, len);
234
251
  bucket = (Slot *)c->slots + (h & c->mask);
235
252
  }
236
253
  b->next = *bucket;
data/ext/oj/cache.h CHANGED
@@ -10,7 +10,7 @@
10
10
 
11
11
  #define CACHE_MAX_KEY 35
12
12
 
13
- extern uint64_t oj_hash_seed;
13
+ extern uint64_t oj_hash_calc(const uint8_t *key, size_t len);
14
14
 
15
15
  extern void oj_hash_seed_init(void);
16
16
 
data/ext/oj/code.c CHANGED
@@ -3,6 +3,8 @@
3
3
 
4
4
  #include "code.h"
5
5
 
6
+ #include <string.h>
7
+
6
8
  #include "dump.h"
7
9
 
8
10
  inline static VALUE resolve_classname(VALUE mod, const char *classname) {
@@ -45,18 +47,44 @@ static VALUE path2class(const char *name) {
45
47
  return resolve_classname(clas, class_name);
46
48
  }
47
49
 
50
+ // Resolves the class lazily and returns Qundef while it is not defined, as
51
+ // OpenStruct is not until ostruct is required. A miss is checked again on
52
+ // every call so a class loaded later is found by whichever dump comes next,
53
+ // through any entry point. For a top-level name that check is a single
54
+ // constant table probe, so it costs about the same as a cached miss would.
55
+ VALUE oj_code_class(Code c) {
56
+ if (Qnil != c->clas && Qundef != c->clas) {
57
+ return c->clas;
58
+ }
59
+ if (0 == c->id && NULL == strstr(c->name, "::")) {
60
+ c->id = rb_intern(c->name);
61
+ }
62
+ if (0 == c->id) {
63
+ c->clas = path2class(c->name);
64
+ } else if (rb_const_defined_at(rb_cObject, c->id)) {
65
+ c->clas = rb_const_get_at(rb_cObject, c->id);
66
+ } else {
67
+ c->clas = Qundef;
68
+ }
69
+ if (Qnil != c->clas && Qundef != c->clas) {
70
+ rb_gc_register_address(&c->clas);
71
+ }
72
+ return c->clas;
73
+ }
74
+
75
+ inline static bool code_is(Code c, VALUE clas) {
76
+ if (Qnil != c->clas && Qundef != c->clas) {
77
+ return clas == c->clas;
78
+ }
79
+ return clas == oj_code_class(c);
80
+ }
81
+
48
82
  bool oj_code_dump(Code codes, VALUE obj, int depth, Out out) {
49
83
  VALUE clas = rb_obj_class(obj);
50
84
  Code c = codes;
51
85
 
52
86
  for (; NULL != c->name; c++) {
53
- if (Qundef == c->clas) { // indicates not defined
54
- continue;
55
- }
56
- if (Qnil == c->clas) {
57
- c->clas = path2class(c->name);
58
- }
59
- if (clas == c->clas && c->active) {
87
+ if (c->active && code_is(c, clas)) {
60
88
  c->encode(obj, depth, out);
61
89
  return true;
62
90
  }
@@ -69,13 +97,7 @@ oj_code_load(Code codes, VALUE clas, VALUE args) {
69
97
  Code c = codes;
70
98
 
71
99
  for (; NULL != c->name; c++) {
72
- if (Qundef == c->clas) { // indicates not defined
73
- continue;
74
- }
75
- if (Qnil == c->clas) {
76
- c->clas = path2class(c->name);
77
- }
78
- if (clas == c->clas) {
100
+ if (code_is(c, clas)) {
79
101
  if (NULL == c->decode) {
80
102
  break;
81
103
  }
@@ -89,13 +111,7 @@ void oj_code_set_active(Code codes, VALUE clas, bool active) {
89
111
  Code c = codes;
90
112
 
91
113
  for (; NULL != c->name; c++) {
92
- if (Qundef == c->clas) { // indicates not defined
93
- continue;
94
- }
95
- if (Qnil == c->clas) {
96
- c->clas = path2class(c->name);
97
- }
98
- if (clas == c->clas || Qnil == clas) {
114
+ if (Qnil == clas || code_is(c, clas)) {
99
115
  c->active = active;
100
116
  if (Qnil != clas) {
101
117
  break;
@@ -108,13 +124,7 @@ bool oj_code_has(Code codes, VALUE clas, bool encode) {
108
124
  Code c = codes;
109
125
 
110
126
  for (; NULL != c->name; c++) {
111
- if (Qundef == c->clas) { // indicates not defined
112
- continue;
113
- }
114
- if (Qnil == c->clas) {
115
- c->clas = path2class(c->name);
116
- }
117
- if (clas == c->clas) {
127
+ if (code_is(c, clas)) {
118
128
  if (encode) {
119
129
  return c->active && NULL != c->encode;
120
130
  } else {
data/ext/oj/code.h CHANGED
@@ -17,6 +17,7 @@ typedef struct _code {
17
17
  EncodeFunc encode;
18
18
  DecodeFunc decode;
19
19
  bool active; // For compat mode.
20
+ ID id; // Interned name, set on first lookup of a top-level class.
20
21
  } *Code;
21
22
 
22
23
  // Used by encode functions.
@@ -28,6 +29,7 @@ typedef struct _attr {
28
29
  VALUE time;
29
30
  } *Attr;
30
31
 
32
+ extern VALUE oj_code_class(Code c);
31
33
  extern bool oj_code_dump(Code codes, VALUE obj, int depth, Out out);
32
34
  extern VALUE oj_code_load(Code codes, VALUE clas, VALUE args);
33
35
  extern void oj_code_set_active(Code codes, VALUE clas, bool active);
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
@@ -136,18 +159,21 @@ static VALUE start_array(ParseInfo pi) {
136
159
  return rb_ary_new();
137
160
  }
138
161
 
162
+ void oj_compat_array_append_other(VALUE a, VALUE value) {
163
+ if (rb_cArray == rb_obj_class(a)) {
164
+ rb_ary_push(a, value);
165
+ } else {
166
+ // The array_class may not be an Array, and the json gem calls << even
167
+ // for Array subclasses.
168
+ rb_funcall(a, rb_intern("<<"), 1, value);
169
+ }
170
+ }
171
+
139
172
  static void array_append_num(ParseInfo pi, NumInfo ni) {
140
173
  Val parent = stack_peek(&pi->stack);
141
174
  volatile VALUE rval = oj_num_as_value(ni);
142
175
 
143
- if (!oj_use_array_alt && rb_cArray != rb_obj_class(parent->val)) {
144
- // The rb_ary_push would still work but the unit tests for the json
145
- // gem require the less efficient << method be called to push the
146
- // values.
147
- rb_funcall(parent->val, rb_intern("<<"), 1, rval);
148
- } else {
149
- rb_ary_push(parent->val, rval);
150
- }
176
+ oj_compat_array_append(parent->val, rval);
151
177
  TRACE_PARSE_CALL(pi->options.trace, "append_number", pi, rval);
152
178
  }
153
179
 
@@ -158,26 +184,32 @@ static void array_append_cstr(ParseInfo pi, const char *str, size_t len, const c
158
184
  VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, len);
159
185
 
160
186
  if (Qnil != clas) {
161
- rb_ary_push(stack_peek(&pi->stack)->val, rb_funcall(clas, oj_json_create_id, 1, rstr));
187
+ oj_compat_array_append(stack_peek(&pi->stack)->val, rb_funcall(clas, oj_json_create_id, 1, rstr));
162
188
  return;
163
189
  }
164
190
  }
165
- rb_ary_push(stack_peek(&pi->stack)->val, rstr);
191
+ oj_compat_array_append(stack_peek(&pi->stack)->val, rstr);
166
192
  TRACE_PARSE_CALL(pi->options.trace, "append_string", pi, rstr);
167
193
  }
168
194
 
195
+ static void array_append_value(ParseInfo pi, VALUE value) {
196
+ oj_compat_array_append(stack_peek(&pi->stack)->val, value);
197
+ TRACE_PARSE_CALL(pi->options.trace, "append_value", pi, value);
198
+ }
199
+
169
200
  void oj_set_compat_callbacks(ParseInfo pi) {
170
201
  oj_set_strict_callbacks(pi);
171
- pi->start_hash = start_hash;
172
- pi->end_hash = end_hash;
173
- pi->hash_set_cstr = hash_set_cstr;
174
- pi->hash_set_num = hash_set_num;
175
- pi->hash_set_value = hash_set_value;
176
- pi->add_num = add_num;
177
- pi->add_cstr = add_cstr;
178
- pi->array_append_cstr = array_append_cstr;
179
- pi->start_array = start_array;
180
- pi->array_append_num = array_append_num;
202
+ pi->start_hash = start_hash;
203
+ pi->end_hash = end_hash;
204
+ pi->hash_set_cstr = hash_set_cstr;
205
+ pi->hash_set_num = hash_set_num;
206
+ pi->hash_set_value = hash_set_value;
207
+ pi->add_num = add_num;
208
+ pi->add_cstr = add_cstr;
209
+ pi->array_append_cstr = array_append_cstr;
210
+ pi->array_append_value = array_append_value;
211
+ pi->start_array = start_array;
212
+ pi->array_append_num = array_append_num;
181
213
  }
182
214
 
183
215
  VALUE
data/ext/oj/custom.c CHANGED
@@ -492,6 +492,7 @@ static VALUE dump_common(VALUE obj, int depth, Out out) {
492
492
  rs = rb_funcall2(obj, oj_to_json_id, out->argc, out->argv);
493
493
  }
494
494
  TRACE(out->opts->trace, "to_json", obj, depth + 1, TraceRubyOut);
495
+ StringValue(rs);
495
496
  s = RSTRING_PTR(rs);
496
497
  len = RSTRING_LEN(rs);
497
498
 
@@ -565,7 +566,7 @@ static int dump_attr_cb(ID key, VALUE value, VALUE ov) {
565
566
  } else if (Yes == out->opts->ignore_under && '@' == *attr && '_' == attr[1]) {
566
567
  return ST_CONTINUE;
567
568
  }
568
- if (0 == strcmp("bt", attr) || 0 == strcmp("mesg", attr)) {
569
+ if ('@' != *attr && (0 == strcmp("bt", attr) || 0 == strcmp("mesg", attr) || 0 == strcmp("bt_locations", attr))) {
569
570
  return ST_CONTINUE;
570
571
  }
571
572
  assure_size(out, size);
@@ -707,6 +708,7 @@ static void dump_array(VALUE a, int depth, Out out, bool as_ok) {
707
708
  cnt--;
708
709
  for (i = 0; i <= cnt; i++) {
709
710
  if (out->opts->dump_opts.use) {
711
+ assure_size(out, size);
710
712
  if (0 < out->opts->dump_opts.array_size) {
711
713
  APPEND_CHARS(out->cur, out->opts->dump_opts.array_nl, out->opts->dump_opts.array_size);
712
714
  }
@@ -724,7 +726,7 @@ static void dump_array(VALUE a, int depth, Out out, bool as_ok) {
724
726
  *out->cur++ = ',';
725
727
  }
726
728
  }
727
- size = depth * out->indent + 1;
729
+ size = indent_len(out, depth, out->opts->dump_opts.array_size) + 1;
728
730
  assure_size(out, size);
729
731
  if (out->opts->dump_opts.use) {
730
732
  if (0 < out->opts->dump_opts.array_size) {
@@ -816,6 +818,18 @@ static void dump_struct(VALUE obj, int depth, Out out, bool as_ok) {
816
818
  oj_dump_custom_val(v, d3, out, true);
817
819
  *out->cur++ = ',';
818
820
  }
821
+ if (2 == cnt && rb_obj_is_kind_of(obj, rb_cRange)) {
822
+ VALUE beg;
823
+ VALUE end;
824
+ int excl;
825
+
826
+ rb_range_values(obj, &beg, &end, &excl);
827
+ assure_size(out, size + 7);
828
+ fill_indent(out, d3);
829
+ APPEND_CHARS(out->cur, "\"excl\":", 7);
830
+ oj_dump_custom_val(excl ? Qtrue : Qfalse, d3, out, true);
831
+ *out->cur++ = ',';
832
+ }
819
833
  out->cur--;
820
834
  *out->cur++ = '}';
821
835
  *out->cur = '\0';
@@ -904,6 +918,9 @@ static void hash_set_cstr(ParseInfo pi, Val kval, const char *str, size_t len, c
904
918
  if (Qundef == kval->key_val && Yes == pi->options.create_ok && NULL != pi->options.create_id &&
905
919
  *pi->options.create_id == *key && (size_t)pi->options.create_id_len == klen &&
906
920
  0 == strncmp(pi->options.create_id, key, klen)) {
921
+ if (oj_is_backtrace_name(str, len)) {
922
+ return;
923
+ }
907
924
  parent->clas = oj_name2class(pi, str, len, false, rb_eArgError);
908
925
  if (2 == klen && '^' == *key && 'o' == key[1]) {
909
926
  if (Qundef != parent->clas) {
@@ -1014,7 +1031,7 @@ static void array_append_num(ParseInfo pi, NumInfo ni) {
1014
1031
  Val parent = stack_peek(&pi->stack);
1015
1032
  volatile VALUE rval = oj_num_as_value(ni);
1016
1033
 
1017
- rb_ary_push(parent->val, rval);
1034
+ oj_compat_array_append(parent->val, rval);
1018
1035
  TRACE_PARSE_CALL(pi->options.trace, "append_number", pi, rval);
1019
1036
  }
1020
1037
 
@@ -1025,16 +1042,32 @@ static void array_append_cstr(ParseInfo pi, const char *str, size_t len, const c
1025
1042
  VALUE clas = oj_rxclass_match(&pi->options.str_rx, str, len);
1026
1043
 
1027
1044
  if (Qnil != clas) {
1028
- rb_ary_push(stack_peek(&pi->stack)->val, rb_funcall(clas, oj_json_create_id, 1, rstr));
1045
+ oj_compat_array_append(stack_peek(&pi->stack)->val, rb_funcall(clas, oj_json_create_id, 1, rstr));
1029
1046
  return;
1030
1047
  }
1031
1048
  }
1032
- rb_ary_push(stack_peek(&pi->stack)->val, rstr);
1049
+ oj_compat_array_append(stack_peek(&pi->stack)->val, rstr);
1033
1050
  TRACE_PARSE_CALL(pi->options.trace, "append_string", pi, rstr);
1034
1051
  }
1035
1052
 
1053
+ // Compat mode defers Hash creation to its end_hash (bulk insert from the
1054
+ // buffered pairs) but the custom callbacks below fill the Hash as they go,
1055
+ // so hashes must exist from the opening brace here.
1056
+ static VALUE start_hash(ParseInfo pi) {
1057
+ volatile VALUE h;
1058
+
1059
+ if (Qnil != pi->options.hash_class) {
1060
+ h = rb_class_new_instance(0, NULL, pi->options.hash_class);
1061
+ } else {
1062
+ h = rb_hash_new();
1063
+ }
1064
+ TRACE_PARSE_IN(pi->options.trace, "start_hash", pi);
1065
+ return h;
1066
+ }
1067
+
1036
1068
  void oj_set_custom_callbacks(ParseInfo pi) {
1037
1069
  oj_set_compat_callbacks(pi);
1070
+ pi->start_hash = start_hash;
1038
1071
  pi->hash_set_cstr = hash_set_cstr;
1039
1072
  pi->end_hash = end_hash;
1040
1073
  pi->hash_set_num = hash_set_num;