oj 3.17.5 → 3.17.6

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: 62e3d04685cbc7e063801fd45fc2e9c86a3695ac1fff9235d49944fb19a1ba50
4
- data.tar.gz: 98cd36664b62b1d774998a5c39d4ea1610fbeb5a4d789de2bd80894bf9121fbe
3
+ metadata.gz: af3a1f2b343eefacdaeb1f82d4b48fe67bece49e4aae63ad46aa878f726ae3de
4
+ data.tar.gz: 8cd6e1ac300706f39d434cbbba52ea780a96b761ba2090d3d3624185bc021819
5
5
  SHA512:
6
- metadata.gz: '029e6458460f15ae5bcbf26bb25ff959b5c3a0b70cf04b5eded0b2ff50930bbd3470b51410590fbf6621640590257360e0d20b290af8c28efbb31ee0a602998b'
7
- data.tar.gz: c2442be3533074487f3fa7b7e84fac719748cd3f99c40217dc6f6dbc09600e491b2925905f400e2ac7de5639d78d15f76f01f5865a41dda2f963e6ccfd59223c
6
+ metadata.gz: e081fafa7bd5af792fef91dc58612d6ea85bdae7b3d709bbd945773a5ad9d1feec4c88e3844aa403098c3fa267814ef61dcbba5fb4d9ee86a239eecf5b3926fa
7
+ data.tar.gz: 339ba6bedb36fb30a382f8cb5037053a4197d4e9364bb11f50b79ec6ec3002cdd687868e140fec1dacabdc9cfa972a45cfcd6cb8a85e4c4148cd484f56278004
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # CHANGELOG
2
2
 
3
+ ## 3.17.6 - 2026-08-10
4
+
5
+ - 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)
6
+
3
7
  ## 3.17.5 - 2026-07-31
4
8
 
5
9
  Most of this release comes out of a security review of the C extension. There are no API changes.
data/ext/oj/oj.c CHANGED
@@ -836,12 +836,14 @@ static const char *make_only_value(VALUE v) {
836
836
  return NULL;
837
837
  }
838
838
 
839
- static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
840
- Options copts = (Options)opts;
841
- size_t len;
839
+ // Apply one key and value to copts. Returns true if the key named an option Oj
840
+ // knows and false if it did not. An unrecognized key is still ignored rather
841
+ // than an error; the return only lets a caller tell the two cases apart.
842
+ static bool parse_one_option(VALUE k, VALUE v, Options copts) {
843
+ size_t len;
842
844
 
843
845
  if (set_yesno_options(k, v, copts)) {
844
- return ST_CONTINUE;
846
+ return true;
845
847
  }
846
848
  if (oj_indent_sym == k) {
847
849
  switch (rb_type(v)) {
@@ -967,7 +969,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
967
969
  }
968
970
  } else if (bigdecimal_load_sym == k) {
969
971
  if (Qnil == v) {
970
- return ST_CONTINUE;
972
+ return true;
971
973
  }
972
974
 
973
975
  if (bigdecimal_sym == v || Qtrue == v) {
@@ -983,7 +985,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
983
985
  }
984
986
  } else if (compat_bigdecimal_sym == k) {
985
987
  if (Qnil == v) {
986
- return ST_CONTINUE;
988
+ return true;
987
989
  }
988
990
  copts->compat_bigdec = (Qtrue == v);
989
991
  } else if (oj_decimal_class_sym == k) {
@@ -1078,7 +1080,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1078
1080
  }
1079
1081
  } else if (nan_sym == k) {
1080
1082
  if (Qnil == v) {
1081
- return ST_CONTINUE;
1083
+ return true;
1082
1084
  }
1083
1085
  if (null_sym == v) {
1084
1086
  copts->dump_opts.nan_dump = NullNan;
@@ -1095,7 +1097,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1095
1097
  }
1096
1098
  } else if (omit_nil_sym == k) {
1097
1099
  if (Qnil == v) {
1098
- return ST_CONTINUE;
1100
+ return true;
1099
1101
  }
1100
1102
  if (Qtrue == v) {
1101
1103
  copts->dump_opts.omit_nil = true;
@@ -1106,7 +1108,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1106
1108
  }
1107
1109
  } else if (omit_null_byte_sym == k) {
1108
1110
  if (Qnil == v) {
1109
- return ST_CONTINUE;
1111
+ return true;
1110
1112
  }
1111
1113
  if (Qtrue == v) {
1112
1114
  copts->dump_opts.omit_null_byte = true;
@@ -1167,7 +1169,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1167
1169
  }
1168
1170
  } else if (integer_range_sym == k) {
1169
1171
  if (Qnil == v) {
1170
- return ST_CONTINUE;
1172
+ return true;
1171
1173
  }
1172
1174
  if (rb_obj_class(v) == rb_cRange) {
1173
1175
  VALUE min = rb_funcall(v, oj_begin_id, 0);
@@ -1198,7 +1200,7 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1198
1200
  }
1199
1201
  } else if (symbol_keys_sym == k || oj_symbolize_names_sym == k) {
1200
1202
  if (Qnil == v) {
1201
- return ST_CONTINUE;
1203
+ return true;
1202
1204
  }
1203
1205
  copts->sym_key = (Qtrue == v) ? Yes : No;
1204
1206
 
@@ -1233,21 +1235,51 @@ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1233
1235
  OJ_R_FREE((void *)copts->dump_opts.except);
1234
1236
  }
1235
1237
  copts->dump_opts.except = make_only_value(v);
1238
+ } else {
1239
+ return false;
1240
+ }
1241
+ return true;
1242
+ }
1243
+
1244
+ // Collects the return of parse_one_option() over every key in a hash.
1245
+ struct _optsParse {
1246
+ Options copts;
1247
+ bool consumed;
1248
+ };
1249
+
1250
+ static int parse_options_cb(VALUE k, VALUE v, VALUE opts) {
1251
+ struct _optsParse *op = (struct _optsParse *)opts;
1252
+
1253
+ if (parse_one_option(k, v, op->copts)) {
1254
+ op->consumed = true;
1236
1255
  }
1237
1256
  return ST_CONTINUE;
1238
1257
  }
1239
1258
 
1240
- void oj_parse_options(VALUE ropts, Options copts) {
1259
+ // Apply the options in ropts to copts and report whether any key was
1260
+ // recognized. A hash of nothing but unknown keys leaves copts untouched and
1261
+ // returns false.
1262
+ bool oj_parse_options_consumed(VALUE ropts, Options copts) {
1263
+ struct _optsParse op = {copts, false};
1264
+
1241
1265
  if (T_HASH != rb_type(ropts)) {
1242
- return;
1266
+ return false;
1267
+ }
1268
+ rb_hash_foreach(ropts, parse_options_cb, (VALUE)&op);
1269
+ if (Qnil != rb_hash_lookup(ropts, match_string_sym)) {
1270
+ op.consumed = true;
1243
1271
  }
1244
- rb_hash_foreach(ropts, parse_options_cb, (VALUE)copts);
1245
1272
  oj_parse_opt_match_string(&copts->str_rx, ropts);
1246
1273
 
1247
1274
  copts->dump_opts.use = (0 < copts->dump_opts.indent_size || 0 < copts->dump_opts.after_size ||
1248
1275
  0 < copts->dump_opts.before_size || 0 < copts->dump_opts.hash_size ||
1249
1276
  0 < copts->dump_opts.array_size);
1250
- return;
1277
+
1278
+ return op.consumed;
1279
+ }
1280
+
1281
+ void oj_parse_options(VALUE ropts, Options copts) {
1282
+ oj_parse_options_consumed(ropts, copts);
1251
1283
  }
1252
1284
 
1253
1285
  // Free the option buffers that oj_parse_options() allocated for a single call.
data/ext/oj/oj.h CHANGED
@@ -257,6 +257,7 @@ extern VALUE oj_custom_parse_cstr(int argc, VALUE *argv, char *json, size_t len)
257
257
 
258
258
  extern bool oj_hash_has_key(VALUE hash, VALUE key);
259
259
  extern void oj_parse_options(VALUE ropts, Options copts);
260
+ extern bool oj_parse_options_consumed(VALUE ropts, Options copts);
260
261
  extern void oj_free_call_options(Options copts);
261
262
  extern void oj_options_take_ownership(Options copts);
262
263
  extern void oj_options_release(Options copts);
data/ext/oj/rails.c CHANGED
@@ -765,11 +765,15 @@ static VALUE encoder_new(int argc, VALUE *argv, VALUE self) {
765
765
  e->arg = rb_hash_new();
766
766
  }
767
767
  // An encoder given no options of its own reads oj_default_options at encode
768
- // time rather than copying it here. ActiveSupport keeps one option-less
769
- // encoder for the life of the process, so a copy taken now would freeze
770
- // every later Oj.default_options and ActiveSupport::JSON::Encoding change
771
- // out of it - including the time_precision that set_encoder itself writes
772
- // after ActiveSupport has already built and cached that encoder.
768
+ // time rather than copying it here. ActiveSupport keeps such an encoder for
769
+ // the life of the process, so a copy taken now would freeze every later
770
+ // Oj.default_options and ActiveSupport::JSON::Encoding change out of it -
771
+ // including the time_precision that set_encoder itself writes after
772
+ // ActiveSupport has already built and cached that encoder.
773
+ //
774
+ // A hash counts as options only if at least one key names an option Oj
775
+ // knows. Non-empty is not enough: ActiveSupport 8.1 caches a second encoder
776
+ // built with {escape: false}, a key Oj has never had.
773
777
  e->own_opts = T_HASH == rb_type(e->arg) && 0 < RHASH_SIZE(e->arg);
774
778
  if (e->own_opts) {
775
779
  e->opts = oj_default_options;
@@ -778,9 +782,13 @@ static VALUE encoder_new(int argc, VALUE *argv, VALUE self) {
778
782
  // its own that is freed with the encoder.
779
783
  e->opts.str_rx.head = NULL;
780
784
  e->opts.str_rx.tail = NULL;
781
- oj_parse_options(e->arg, &e->opts);
785
+ e->own_opts = oj_parse_options_consumed(e->arg, &e->opts);
786
+ }
787
+ if (e->own_opts) {
782
788
  oj_options_take_ownership(&e->opts);
783
789
  } else {
790
+ // Nothing to free: every option that allocates is reached through a
791
+ // recognized key, and ownership is not taken until after that check.
784
792
  memset(&e->opts, 0, sizeof(e->opts));
785
793
  }
786
794
 
@@ -1176,8 +1184,6 @@ static VALUE rails_set_encoder(VALUE self) {
1176
1184
  } else {
1177
1185
  rb_raise(rb_eStandardError, "ActiveSupport not loaded.");
1178
1186
  }
1179
- rb_funcall(active, rb_intern("json_encoder="), 1, encoder_class);
1180
-
1181
1187
  json = rb_const_get_at(active, rb_intern("JSON"));
1182
1188
  encoding = rb_const_get_at(json, rb_intern("Encoding"));
1183
1189
 
@@ -1204,6 +1210,12 @@ static VALUE rails_set_encoder(VALUE self) {
1204
1210
  rb_define_module_function(encoding, "time_precision=", rails_time_precision, 1);
1205
1211
  rb_gv_set("$VERBOSE", verbose);
1206
1212
 
1213
+ // Last, because ActiveSupport builds and caches encoders inside
1214
+ // json_encoder= and they should see the settings above already in the
1215
+ // defaults. escape_html and xml_time are read ahead of it for the same
1216
+ // reason.
1217
+ rb_funcall(active, rb_intern("json_encoder="), 1, encoder_class);
1218
+
1207
1219
  return Qnil;
1208
1220
  }
1209
1221
 
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.5'
3
+ VERSION = '3.17.6'
4
4
  end
data/pages/Rails.md CHANGED
@@ -91,6 +91,10 @@ The classes that can be put in optimized mode and are optimized when
91
91
 
92
92
  Both `Oj::Rails` and each `Oj::Rails::Encoder` have `optimize()`, `deoptimize()`, and `optimized?()` methods. The module level methods change the setting for the whole process. An encoder follows those process wide settings until `optimize()` or `deoptimize()` is called on the encoder itself. From then on the encoder keeps a set of its own and later module level changes do not reach it.
93
93
 
94
+ Options work the same way. An encoder reads `Oj.default_options` when it encodes, so later changes to the defaults reach it, unless it was given options of its own, in which case it keeps the copy it took when it was created. An encoder counts as having options of its own only if the hash it was built with names at least one option Oj knows. A hash of nothing but keys Oj does not have, such as the `escape: false` ActiveSupport 8.1 builds one of its cached encoders with, leaves the encoder on the defaults.
95
+
96
+ Once a class is optimized its `as_json()` method is no longer called, so an application that defines its own `as_json()` on an optimized class, `BigDecimal` for example, will not see it used. Call `Oj::Rails.deoptimize(BigDecimal)` after `Oj.optimize_rails` to put that one class back on the `as_json()` path and leave the rest optimized.
97
+
94
98
  The ActiveSupport decoder is the `JSON.parse()` method. Calling the
95
99
  `Oj::Rails.set_decoder()` method replaces that method with the Oj equivalent.
96
100
 
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.5
4
+ version: 3.17.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter Ohler