oj 3.17.3 → 3.17.5

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: 62e3d04685cbc7e063801fd45fc2e9c86a3695ac1fff9235d49944fb19a1ba50
4
+ data.tar.gz: 98cd36664b62b1d774998a5c39d4ea1610fbeb5a4d789de2bd80894bf9121fbe
5
5
  SHA512:
6
- metadata.gz: 60d445fd27bbea120359c21d0c1b0ac1d4fbbd678c8fd28efe620706d382a2f0cc434645967c9bac0b6d08d1065fe99dc4b15e2623d98ca0e15a61eb591a993b
7
- data.tar.gz: b7736fd3a7b27f98ef3e4df05464000849403ff33a3f82ecd6d8c51b40d0c2753d23ed14c078eb26506d504de991e35ebe68e5703225c80aab6ebbd28b7a80dc
6
+ metadata.gz: '029e6458460f15ae5bcbf26bb25ff959b5c3a0b70cf04b5eded0b2ff50930bbd3470b51410590fbf6621640590257360e0d20b290af8c28efbb31ee0a602998b'
7
+ data.tar.gz: c2442be3533074487f3fa7b7e84fac719748cd3f99c40217dc6f6dbc09600e491b2925905f400e2ac7de5639d78d15f76f01f5865a41dda2f963e6ccfd59223c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,80 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.17.5 - 2026-07-31
4
+
5
+ Most of this release comes out of a security review of the C extension. There are no API changes.
6
+
7
+ ### Memory safety
8
+
9
+ - Fixed hash flooding by seeding the key hash per process. The hash was decided entirely at compile time, so a document of colliding keys turned key interning into O(n^2) and left the process slow for every later parse. (#1053)
10
+ - Fixed a one byte buffer over-read on a document that ends with a backslash inside an unterminated string. (#1054)
11
+ - Fixed two length underflows that made an empty `^m` value and a key of exactly 65,536 bytes read forward until they left mapped memory. (#1055)
12
+ - Fixed a SEGV from type confusion in class path resolution. A name segment that resolved to something other than a class or module was dereferenced as one. (#1056)
13
+ - Fixed uninitialized stack memory being read into the message of an unresolved class name error. (#1057)
14
+ - Fixed output buffer overflows when writing indentation, where several callers of `fill_indent()` had not assured the buffer first. (#1058)
15
+ - Fixed the `Oj::Doc` path depth limit being checked one level too late. (#1061)
16
+ - Fixed the use-after-free of a key when the value stack grows. (#1062)
17
+ - Fixed the buffer over-read when a document ends where a key belongs. (#1063)
18
+ - Fixed the id from a circular reference being used unchecked. (#1064)
19
+ - Fixed the SEGV dumping an exception that was never raised, where the missing backtrace was read as an Array. (#1068)
20
+ - Fixed the stack buffer overflow writing the error location. (#1071)
21
+ - Fixed a read error in `Oj::Parser#file` being taken as a read of `SIZE_MAX` bytes, which wrote in front of the buffer and then reparsed it forever without checking for interrupts. (#1072)
22
+ - Fixed a `:float_format` too long for the 64 byte buffer copying stack bytes past the end of it into the output. (#1073)
23
+ - Fixed the double free of the default `create_id` when a call passes `:create_id => nil`. (#1074)
24
+ - Fixed the buffer over-read of an odd attribute name and the leak of its arguments. (#1075)
25
+ - Fixed the SEGV reading a truncated document with `Oj::Parser#file` and the descriptor it leaked. (#1076)
26
+ - Fixed the SEGV from a malformed number in `Oj::Doc`. (#1077)
27
+ - Fixed the stack exhaustion from a long key with `:only` or `:except`. (#1078)
28
+ - Fixed the odd module match being decided by uninitialized memory. (#1079)
29
+ - Fixed `:float_format` accepting directives it has no argument for. (#1083)
30
+
31
+ ### Memory leaks
32
+
33
+ - Fixed the leak of an escaped hash key in the streaming parser. (#1067)
34
+ - Fixed the leak of the parser `create_id` when it is set a second time. (#1085)
35
+ - Fixed the leak of the parser when `Oj::Parser.new` or `Oj::Parser.safe` raises on an argument it does not take. (#1086)
36
+
37
+ ### Parsing and dumping
38
+
39
+ - Fixed a safe parser limit of 4 being ignored. (#1065)
40
+ - Fixed the exponent bound being checked after it is narrowed. (#1066)
41
+ - Fixed a document being rejected when it opens with a number of 4093 digits or more read through a file descriptor or an IO, and the reader buffer that leaked when it grew. (#1069)
42
+ - Fixed the safe parser limits being dropped when an option is set after the parser is built. (#1070)
43
+ - Fixed the `:custom` mode dump of an exception that was never raised opening with `{,`. (#1082)
44
+ - Fixed the `:wab` mode time load being an offset out on Windows, where `mktime()` stood in for `timegm()` and returned -1 for any time before the epoch. (#1088)
45
+ - Fixed `Oj::Doc` dropping the exponent of a number written without a decimal point, so `1e3` loaded as `1`. (#1089)
46
+
47
+ ### Rails
48
+
49
+ - Fixed a `to_json(only:)` or `to_json(except:)` dropping the `include_root_in_json` wrapper key and the keys added by `:methods`. (#1039)
50
+ - Fixed the `as_json` options reaching only the first element of a container, so `to_json(only:)`, `to_json(methods:)` and any custom `as_json(opts)` took effect on one row and nowhere else. (#1040)
51
+ - An encoder now reads the global optimized class table and options instead of copying them, saving about 6.7KB of malloc per `ActiveSupport::JSON.encode`, and picks up later `Oj.default_options` and `ActiveSupport::JSON::Encoding` changes. (#1049)
52
+ - Fixed `as_json` being handed the encoder's own options hash. ActiveSupport allows `as_json` to write into it, and ActiveSupport 8.1 caches one encoder per process, so a single such `as_json` could leave `:only` set for every later `to_json`. (#1050)
53
+
54
+ ### Documentation
55
+
56
+ - Renamed the documented `:skip_null_byte` option to `:omit_null_byte`, which is the name that works. (#1041)
57
+ - Dropped the `--with-sse42` install option from InstallOptions.md. SIMD support is detected at runtime. (#1042)
58
+ - Corrected the documented minimum Ruby version from 2.4+ to 2.7+. (#1043)
59
+ - Corrected the Parser.md benchmark conclusions to reference `:usual` rather than `:saj`. (#1044)
60
+ - Corrected Custom.md and Options.md, which called `:custom` the default mode. The default is `:object`. (#1045)
61
+ - Corrected the documented type of the `:safe` option from String to Boolean. (#1046)
62
+ - Corrected Security.md, which said no methods are ever called on an Object created by a load. `exception`, `set_backtrace` and `replace` are. The page now also says which mode `Oj.load` uses. (#1080)
63
+
64
+ ### Tests and CI
65
+
66
+ - Added the ActiveSupport 8 test suite. The Rakefile picked the suite by major version, so the rails_8 job had been running zero ActiveSupport tests. (#1047)
67
+ - Added the ActiveSupport 8.1 test suite and pinned the #985 regression. (#1051)
68
+ - Fixed `rake` never running the `test/test_*.rb` suite. The `Rake::TestTask` that defined `test` was commented out, so `test_all` invoked a synthesized no-op file task. (#1084)
69
+ - Added a job timeout to the workflows. (#1087)
70
+ - Removed the pre-1970 time test skips on Windows, which were left from a 32 bit Ruby limit that no longer applies. (#1090)
71
+
72
+ ## 3.17.4 - 2026-07-14
73
+
74
+ - Fixed issue in fast.c where rescuing from a panic did not reset the where counter.
75
+
76
+ - Multiple fixes thanks to @Watson1978.
77
+
3
78
  ## 3.17.3 - 2026-06-04
4
79
 
5
80
  - Fixed issue in intern.c and fast.c.
data/ext/oj/cache.c CHANGED
@@ -28,6 +28,22 @@
28
28
  // almost the Murmur hash algorithm
29
29
  #define M 0x5bd1e995
30
30
 
31
+ uint64_t oj_hash_seed = 0;
32
+
33
+ void oj_hash_seed_init(void) {
34
+ static bool seeded = false;
35
+
36
+ if (seeded) {
37
+ return;
38
+ }
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
45
+ }
46
+
31
47
  typedef struct _slot {
32
48
  struct _slot *next;
33
49
  VALUE val;
@@ -62,7 +78,7 @@ void cache_set_form(Cache c, VALUE (*form)(const char *str, size_t len)) {
62
78
  static uint64_t hash_calc(const uint8_t *key, size_t len) {
63
79
  const uint8_t *end = key + len;
64
80
  const uint8_t *endless = key + (len & 0xFFFFFFFC);
65
- uint64_t h = (uint64_t)len;
81
+ uint64_t h = (uint64_t)len ^ oj_hash_seed;
66
82
  uint64_t k;
67
83
 
68
84
  while (key < endless) {
data/ext/oj/cache.h CHANGED
@@ -6,9 +6,14 @@
6
6
 
7
7
  #include <ruby.h>
8
8
  #include <stdbool.h>
9
+ #include <stdint.h>
9
10
 
10
11
  #define CACHE_MAX_KEY 35
11
12
 
13
+ extern uint64_t oj_hash_seed;
14
+
15
+ extern void oj_hash_seed_init(void);
16
+
12
17
  struct _cache;
13
18
  typedef struct _cache *Cache;
14
19
 
data/ext/oj/circarray.c CHANGED
@@ -25,8 +25,12 @@ void oj_circ_array_free(CircArray ca) {
25
25
  OJ_R_FREE(ca);
26
26
  }
27
27
 
28
+ // Objects are numbered in encounter order starting at 1, so the only id that
29
+ // can extend the array is one past the highest already seen. Anything further
30
+ // is not a reference to an object the document can contain and must not be
31
+ // used to size an allocation.
28
32
  void oj_circ_array_set(CircArray ca, VALUE obj, unsigned long id) {
29
- if (0 < id && 0 != ca) {
33
+ if (0 != ca && 0 < id && id <= ca->cnt + 1) {
30
34
  unsigned long i;
31
35
 
32
36
  if (ca->size < id) {
@@ -57,7 +61,7 @@ VALUE
57
61
  oj_circ_array_get(CircArray ca, unsigned long id) {
58
62
  VALUE obj = Qnil;
59
63
 
60
- if (id <= ca->cnt && 0 != ca) {
64
+ if (0 != ca && 0 < id && id <= ca->cnt) {
61
65
  obj = ca->objs[id - 1];
62
66
  }
63
67
  return obj;
data/ext/oj/compat.c CHANGED
@@ -14,11 +14,11 @@
14
14
 
15
15
  static void hash_set_cstr(ParseInfo pi, Val kval, const char *str, size_t len, const char *orig) {
16
16
  const char *key = kval->key;
17
- int klen = kval->klen;
17
+ size_t klen = kval->klen;
18
18
  Val parent = stack_peek(&pi->stack);
19
19
 
20
20
  if (Qundef == kval->key_val && Yes == pi->options.create_ok && NULL != pi->options.create_id &&
21
- *pi->options.create_id == *key && (int)pi->options.create_id_len == klen &&
21
+ *pi->options.create_id == *key && (size_t)pi->options.create_id_len == klen &&
22
22
  0 == strncmp(pi->options.create_id, key, klen)) {
23
23
  parent->classname = oj_strndup(str, len);
24
24
  parent->clen = len;
data/ext/oj/custom.c CHANGED
@@ -640,7 +640,9 @@ static void dump_obj_attrs(VALUE obj, VALUE clas, slot_t id, int depth, Out out)
640
640
  if (rb_obj_is_kind_of(obj, rb_eException)) {
641
641
  volatile VALUE rv;
642
642
 
643
- if (',' != *(out->cur - 1)) {
643
+ // An exception that was never raised has no instance variables, so
644
+ // without the class name nothing has been written since the '{'.
645
+ if (',' != *(out->cur - 1) && '{' != *(out->cur - 1)) {
644
646
  *out->cur++ = ',';
645
647
  }
646
648
  // message
@@ -896,11 +898,11 @@ void oj_dump_custom_val(VALUE obj, int depth, Out out, bool as_ok) {
896
898
 
897
899
  static void hash_set_cstr(ParseInfo pi, Val kval, const char *str, size_t len, const char *orig) {
898
900
  const char *key = kval->key;
899
- int klen = kval->klen;
901
+ size_t klen = kval->klen;
900
902
  Val parent = stack_peek(&pi->stack);
901
903
 
902
904
  if (Qundef == kval->key_val && Yes == pi->options.create_ok && NULL != pi->options.create_id &&
903
- *pi->options.create_id == *key && (int)pi->options.create_id_len == klen &&
905
+ *pi->options.create_id == *key && (size_t)pi->options.create_id_len == klen &&
904
906
  0 == strncmp(pi->options.create_id, key, klen)) {
905
907
  parent->clas = oj_name2class(pi, str, len, false, rb_eArgError);
906
908
  if (2 == klen && '^' == *key && 'o' == key[1]) {
data/ext/oj/dump.c CHANGED
@@ -1456,10 +1456,11 @@ void oj_dump_raw(const char *str, size_t cnt, Out out) {
1456
1456
  }
1457
1457
 
1458
1458
  void oj_out_init(Out out) {
1459
- out->buf = out->stack_buffer;
1460
- out->cur = out->buf;
1461
- out->end = out->buf + sizeof(out->stack_buffer) - BUFFER_EXTRA;
1462
- out->allocated = false;
1459
+ out->buf = out->stack_buffer;
1460
+ out->cur = out->buf;
1461
+ out->end = out->buf + sizeof(out->stack_buffer) - BUFFER_EXTRA;
1462
+ out->allocated = false;
1463
+ out->key_filter_off = false;
1463
1464
  }
1464
1465
 
1465
1466
  void oj_out_free(Out out) {
@@ -1731,6 +1732,11 @@ void oj_dump_float(VALUE obj, int depth, Out out, bool as_ok) {
1731
1732
  size_t oj_dump_float_printf(char *buf, size_t blen, VALUE obj, double d, const char *format) {
1732
1733
  size_t cnt = snprintf(buf, blen, format, d);
1733
1734
 
1735
+ // snprintf() returns the length the output would have needed, which is
1736
+ // more than it wrote when the format asks for more room than buf has.
1737
+ if (blen <= cnt) {
1738
+ cnt = blen - 1;
1739
+ }
1734
1740
  // Round off issues at 16 significant digits so check for obvious ones of
1735
1741
  // 0001 and 9999.
1736
1742
  if (17 <= cnt && (0 == strcmp("0001", buf + cnt - 4) || 0 == strcmp("9999", buf + cnt - 4))) {
@@ -1742,6 +1748,21 @@ size_t oj_dump_float_printf(char *buf, size_t blen, VALUE obj, double d, const c
1742
1748
  return cnt;
1743
1749
  }
1744
1750
 
1751
+ // The only and except lists are the keys joined with a ':' and wrapped in one
1752
+ // at each end, so a key is in a list when ":key:" is in it. Matching in place
1753
+ // finds what searching the list for a copy of the key with the colons on it
1754
+ // found, without a buffer sized by the key.
1755
+ static bool key_listed(const char *list, const char *key, size_t klen) {
1756
+ const char *s;
1757
+
1758
+ for (s = list; NULL != (s = strchr(s, ':')); s++) {
1759
+ if (0 == strncmp(s + 1, key, klen) && ':' == s[klen + 1]) {
1760
+ return true;
1761
+ }
1762
+ }
1763
+ return false;
1764
+ }
1765
+
1745
1766
  bool oj_key_skip(VALUE key, const char *only, const char *except) {
1746
1767
  const char *skey;
1747
1768
 
@@ -1751,15 +1772,9 @@ bool oj_key_skip(VALUE key, const char *only, const char *except) {
1751
1772
  default: skey = NULL; break;
1752
1773
  }
1753
1774
  if (NULL != skey && '\0' != *skey) {
1754
- size_t size = strlen(skey);
1755
- char *buf = alloca(size + 3);
1756
-
1757
- buf[0] = ':';
1758
- strcpy(buf + 1, skey);
1759
- buf[size + 1] = ':';
1760
- buf[size + 2] = '\0';
1775
+ size_t klen = strlen(skey);
1761
1776
 
1762
- return ((NULL != only && NULL == strstr(only, buf)) || (NULL != except && NULL != strstr(except, buf)));
1777
+ return ((NULL != only && !key_listed(only, skey, klen)) || (NULL != except && key_listed(except, skey, klen)));
1763
1778
  }
1764
1779
  return NULL != only;
1765
1780
  }
data/ext/oj/dump.h CHANGED
@@ -70,9 +70,19 @@ inline static void assure_size(Out out, size_t len) {
70
70
  }
71
71
  }
72
72
 
73
+ // Bytes that fill_indent(), or the dump_opts equivalent used when :indent is a
74
+ // String, writes at this depth. nl_size is array_size or hash_size.
75
+ inline static size_t indent_len(Out out, int depth, size_t nl_size) {
76
+ if (out->opts->dump_opts.use) {
77
+ return depth * out->opts->dump_opts.indent_size + nl_size;
78
+ }
79
+ return depth * out->indent + 1;
80
+ }
81
+
73
82
  inline static void fill_indent(Out out, int cnt) {
74
83
  if (0 < out->indent) {
75
84
  cnt *= out->indent;
85
+ assure_size(out, cnt + 1);
76
86
  *out->cur++ = '\n';
77
87
  memset(out->cur, ' ', cnt);
78
88
  out->cur += cnt;
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);
@@ -298,9 +299,10 @@ static ID message_id = 0;
298
299
  static ID backtrace_id = 0;
299
300
 
300
301
  static void exception_alt(VALUE obj, int depth, Out out) {
301
- int d3 = depth + 2;
302
- size_t size = d3 * out->indent + 2;
303
- size_t sep_len = out->opts->dump_opts.before_size + out->opts->dump_opts.after_size + 2;
302
+ int d3 = depth + 2;
303
+ size_t size = d3 * out->indent + 2;
304
+ size_t sep_len = out->opts->dump_opts.before_size + out->opts->dump_opts.after_size + 2;
305
+ volatile VALUE bt;
304
306
 
305
307
  if (0 == message_id) {
306
308
  message_id = rb_intern("message");
@@ -331,7 +333,12 @@ static void exception_alt(VALUE obj, int depth, Out out) {
331
333
  if (0 < out->opts->dump_opts.after_size) {
332
334
  APPEND_CHARS(out->cur, out->opts->dump_opts.after_sep, out->opts->dump_opts.after_size);
333
335
  }
334
- dump_array(rb_funcall(obj, backtrace_id, 0), depth, out, false);
336
+ bt = rb_funcall(obj, backtrace_id, 0);
337
+ if (T_ARRAY == rb_type(bt)) {
338
+ dump_array(bt, depth, out, false);
339
+ } else {
340
+ oj_dump_nil(Qnil, depth, out, false);
341
+ }
335
342
  fill_indent(out, depth);
336
343
  *out->cur++ = '}';
337
344
  *out->cur = '\0';
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/extconf.rb CHANGED
@@ -32,6 +32,7 @@ have_func('pthread_mutex_init')
32
32
  have_func('getrlimit', 'sys/resource.h')
33
33
  have_func('rb_enc_interned_str')
34
34
  have_func('rb_ext_ractor_safe', 'ruby.h')
35
+ have_func('rb_hash_start', 'ruby.h')
35
36
 
36
37
  dflags['OJ_DEBUG'] = true unless ENV['OJ_DEBUG'].nil?
37
38
 
data/ext/oj/fast.c CHANGED
@@ -92,12 +92,35 @@ 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
  }
99
106
  }
100
107
 
108
+ inline static bool is_value_end(char c) {
109
+ switch (c) {
110
+ case '\0':
111
+ case ' ':
112
+ case '\t':
113
+ case '\f':
114
+ case '\n':
115
+ case '\r':
116
+ case '/':
117
+ case ',':
118
+ case ']':
119
+ case '}': return true;
120
+ default: return false;
121
+ }
122
+ }
123
+
101
124
  inline static char *ulong_fill(char *s, size_t num) {
102
125
  char buf[32];
103
126
  char *b = buf + sizeof(buf) - 1;
@@ -210,10 +233,11 @@ static void skip_comment(ParseInfo pi) {
210
233
  if ('*' == *pi->s && '/' == *(pi->s + 1)) {
211
234
  pi->s++;
212
235
  return;
213
- } else if ('\0' == *pi->s) {
214
- raise_error("comment not terminated", pi->str, pi->s);
215
236
  }
216
237
  }
238
+ // The loop only ends on the null terminator so the comment was never
239
+ // closed.
240
+ raise_error("comment not terminated", pi->str, pi->s);
217
241
  } else if ('/' == *pi->s) {
218
242
  for (; 1; pi->s++) {
219
243
  switch (*pi->s) {
@@ -455,27 +479,42 @@ static Leaf read_num(ParseInfo pi) {
455
479
  char *start = pi->s;
456
480
  int type = T_FIXNUM;
457
481
  Leaf leaf;
482
+ int dcnt = 0;
483
+ int ecnt = 1;
458
484
 
459
- if ('-' == *pi->s) {
485
+ // read_next() sends a leading '+' here as well, and Oj.load() takes one.
486
+ if ('-' == *pi->s || '+' == *pi->s) {
460
487
  pi->s++;
461
488
  }
462
489
  // digits
463
490
  for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
491
+ dcnt++;
464
492
  }
465
493
  if ('.' == *pi->s) {
466
494
  type = T_FLOAT;
467
495
  pi->s++;
468
496
  for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
497
+ dcnt++;
469
498
  }
470
499
  }
471
500
  if ('e' == *pi->s || 'E' == *pi->s) {
501
+ // An exponent makes it a float whether or not a '.' was seen. Without
502
+ // this leaf_fixnum_value() stops reading at the 'e' and the exponent is
503
+ // dropped.
504
+ type = T_FLOAT;
472
505
  pi->s++;
473
506
  if ('-' == *pi->s || '+' == *pi->s) {
474
507
  pi->s++;
475
508
  }
476
- for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
509
+ for (ecnt = 0; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
510
+ ecnt++;
477
511
  }
478
512
  }
513
+ // A float is converted with rb_cstr_to_dbl(), which has to be given
514
+ // something it can convert.
515
+ if (T_FLOAT == type && (0 == dcnt || 0 == ecnt || !is_value_end(*pi->s))) {
516
+ raise_error("invalid number", pi->str, pi->s);
517
+ }
479
518
  leaf = leaf_new(pi->doc, type);
480
519
  leaf->str = start;
481
520
 
@@ -662,7 +701,11 @@ static void doc_free(Doc doc) {
662
701
  static VALUE protect_open_proc(VALUE x) {
663
702
  ParseInfo pi = (ParseInfo)x;
664
703
 
665
- pi->doc->data = read_next(pi); // parse
704
+ pi->doc->data = read_next(pi); // parse
705
+ // read_obj() and read_array() terminate each value where it ends. The top
706
+ // level value has no closing delimiter to do that on, so a number leaf runs
707
+ // to the end of the document unless it is terminated here.
708
+ *pi->s = '\0';
666
709
  *pi->doc->where = pi->doc->data;
667
710
  pi->doc->where = pi->doc->where_path;
668
711
  if (rb_block_given_p()) {
@@ -710,21 +753,23 @@ static void mark_doc(void *ptr) {
710
753
  }
711
754
  #ifdef HAVE_RB_GC_MARK_MOVABLE
712
755
  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;
756
+ if (NULL != leaf) {
757
+ switch (leaf->value_type) {
758
+ case COL_VAL:
759
+ if (NULL != leaf->elements) {
760
+ Leaf first = leaf->elements->next;
761
+ Leaf e = first;
718
762
 
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;
763
+ do {
764
+ compact_leaf(e);
765
+ e = e->next;
766
+ } while (e != first);
767
+ }
768
+ break;
769
+ case RUBY_VAL: leaf->value = rb_gc_location(leaf->value); break;
726
770
 
727
- default: break;
771
+ default: break;
772
+ }
728
773
  }
729
774
  }
730
775
 
@@ -790,14 +835,11 @@ static VALUE parse_json(VALUE clas, char *json, bool given) {
790
835
  doc->self = self;
791
836
  result = rb_protect(protect_open_proc, (VALUE)&pi, &ex);
792
837
  if (given || 0 != ex) {
838
+ // The doc is detached from its wrapper so the GC will not free it.
839
+ // doc_free() releases both the doc and its json buffer (doc->json), so
840
+ // the caller must not free json separately.
793
841
  DATA_PTR(doc->self) = NULL;
794
- // TBD is this needed?
795
- /*
796
842
  doc_free(pi.doc);
797
- if (0 != ex) { // will jump so caller will not free
798
- OJ_R_FREE(json);
799
- }
800
- */
801
843
  } else {
802
844
  result = doc->self;
803
845
  }
@@ -865,7 +907,7 @@ static bool key_match(const char *pat, const char *key, int plen) {
865
907
  static Leaf get_leaf(Leaf *stack, Leaf *lp, const char *path) {
866
908
  Leaf leaf = *lp;
867
909
 
868
- if (MAX_STACK <= lp - stack) {
910
+ if (MAX_STACK <= (lp + 1) - stack) {
869
911
  rb_raise(rb_const_get_at(Oj, rb_intern("DepthError")), "Path too deep. Limit is %d levels.", MAX_STACK);
870
912
  }
871
913
  if ('\0' != *path) {
@@ -946,6 +988,7 @@ static void each_leaf(Doc doc, VALUE self) {
946
988
 
947
989
  doc->where++;
948
990
  if (MAX_STACK <= doc->where - doc->where_path) {
991
+ doc->where--;
949
992
  rb_raise(rb_const_get_at(Oj, rb_intern("DepthError")), "Path too deep. Limit is %d levels.", MAX_STACK);
950
993
  }
951
994
  do {
@@ -964,7 +1007,7 @@ static void each_leaf(Doc doc, VALUE self) {
964
1007
  }
965
1008
 
966
1009
  static int move_step(Doc doc, const char *path, int loc) {
967
- if (MAX_STACK <= doc->where - doc->where_path) {
1010
+ if (MAX_STACK <= (doc->where + 1) - doc->where_path) {
968
1011
  rb_raise(rb_const_get_at(Oj, rb_intern("DepthError")), "Path too deep. Limit is %d levels.", MAX_STACK);
969
1012
  }
970
1013
  if ('\0' == *path) {
@@ -972,8 +1015,9 @@ static int move_step(Doc doc, const char *path, int loc) {
972
1015
  } else {
973
1016
  Leaf leaf;
974
1017
 
1018
+ // A document with no root (empty or comment only JSON) has no leaf to
1019
+ // step from so the path can not be located.
975
1020
  if (0 == doc->where || 0 == (leaf = *doc->where)) {
976
- printf("*** Internal error at %s\n", path);
977
1021
  return loc;
978
1022
  }
979
1023
  if ('.' == *path && '.' == *(path + 1)) {
@@ -1104,12 +1148,7 @@ static VALUE doc_open(VALUE clas, VALUE str) {
1104
1148
 
1105
1149
  memcpy(json, StringValuePtr(str), len);
1106
1150
  obj = parse_json(clas, json, given);
1107
- // TBD is this needed
1108
- /*
1109
- if (given) {
1110
- OJ_R_FREE(json);
1111
- }
1112
- */
1151
+ // json is owned by the doc and freed by doc_free(); do not free it here.
1113
1152
  return obj;
1114
1153
  }
1115
1154
 
@@ -1141,7 +1180,9 @@ static VALUE doc_open_file(VALUE clas, VALUE filename) {
1141
1180
  int given = rb_block_given_p();
1142
1181
 
1143
1182
  path = StringValuePtr(filename);
1144
- if (0 == (f = fopen(path, "r"))) {
1183
+ // Open in binary mode. On Windows a text mode read translates CRLF into LF
1184
+ // so fewer bytes are read than the file size reported by ftell().
1185
+ if (0 == (f = fopen(path, "rb"))) {
1145
1186
  rb_raise(rb_eIOError, "%s", strerror(errno));
1146
1187
  }
1147
1188
  fseek(f, 0, SEEK_END);
@@ -1159,12 +1200,7 @@ static VALUE doc_open_file(VALUE clas, VALUE filename) {
1159
1200
  fclose(f);
1160
1201
  json[len] = '\0';
1161
1202
  obj = parse_json(clas, json, given);
1162
- // TBD is this needed
1163
- /*
1164
- if (given) {
1165
- OJ_R_FREE(json);
1166
- }
1167
- */
1203
+ // json is owned by the doc and freed by doc_free(); do not free it here.
1168
1204
  return obj;
1169
1205
  }
1170
1206
 
@@ -1263,10 +1299,14 @@ static VALUE doc_path(VALUE self) {
1263
1299
  * #=> nil
1264
1300
  */
1265
1301
  static VALUE doc_local_key(VALUE self) {
1266
- Doc doc = self_doc(self);
1267
- Leaf leaf = *doc->where;
1268
- volatile VALUE key = Qnil;
1302
+ Doc doc = self_doc(self);
1303
+ Leaf leaf;
1304
+ volatile VALUE key = Qnil;
1269
1305
 
1306
+ if (NULL == doc->where || NULL == *doc->where) {
1307
+ return Qnil;
1308
+ }
1309
+ leaf = *doc->where;
1270
1310
  if (T_HASH == leaf->parent_type) {
1271
1311
  key = rb_utf8_str_new_cstr(leaf->key);
1272
1312
  } else if (T_ARRAY == leaf->parent_type) {
@@ -1421,6 +1461,9 @@ static VALUE doc_each_leaf(int argc, VALUE *argv, VALUE self) {
1421
1461
  return Qnil;
1422
1462
  }
1423
1463
  }
1464
+ if (NULL == doc->where || NULL == *doc->where) {
1465
+ return Qnil;
1466
+ }
1424
1467
  each_leaf(doc, self);
1425
1468
  if (0 < wlen) {
1426
1469
  memcpy(doc->where_path, save_path, sizeof(Leaf) * (wlen + 1));