nosj 0.5.0 → 0.5.1

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: 354e310be9d2d56119d2db1f9ad13052c9a6bc8fba5c8d7b7993b78abf345e1e
4
- data.tar.gz: b21acc76a56a10a56c62c370ff1dfcaa8dc263a96958fcac6c4f3c213c4bb542
3
+ metadata.gz: f6c2ec4836b21fafff5a18244b4089e632b39ed4dd48cdbb57d9ab4529beea73
4
+ data.tar.gz: 544ce5c54ab3d6c4300b720d6462af4f02f3427868a6fa94b5cc960b9d2704f4
5
5
  SHA512:
6
- metadata.gz: c672e8653239dc1600ce19b4b38c31ae298dbf40a79449fe12ccf05335144eed7d210a115e87a3a60e8b534b868d3c0080079072ed21a8e67b398e2e8d7e8fc4
7
- data.tar.gz: 22aadd62a736d86fe310ed099d995fe6006e149103eb95d41d128adc6164e6c2f64c2de49e6652437bccefbef70379b27cc3700d45fcad7ae1e2003ac6f05f41
6
+ metadata.gz: 777e339c9ab64479dad1811b33da20bd30e545afdc390a32b70ac1c92c1574cfd7f7c04a5c70f87dd3d1469520970387c50db68af3b3b18c849d3705c92274ee
7
+ data.tar.gz: 8a099e1880b3b6efd6f28311636fbd4e29fb4aca4c3a8c19e5e4603c5fed3cd1936e53547fde34821e701eaf09b77dcc83f44317bd9af4d29d0fe307bc85ea52
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## [0.5.1] - 2026-10-06
2
+
3
+ - Fixed: `NOSJ.patch` read a `path` without the leading slash (`"a"`
4
+ instead of `"/a"`) as the root pointer, so `add`, `copy`, and `move`
5
+ silently replaced the whole document with the value instead of
6
+ raising, and `remove` reported `cannot remove the root`. Such paths
7
+ now raise the same `ArgumentError` as `NOSJ.at_pointer`
8
+ (`JSON Pointer must be empty or start with '/'`) in every op. The
9
+ bug dates back to 0.3.0. Found by fuzzing.
10
+
1
11
  ## [0.5.0] - 2026-09-25
2
12
 
3
13
  **json 3.0 compatibility.** nosj now matches the behavior of the json
data/Cargo.lock CHANGED
@@ -200,7 +200,7 @@ dependencies = [
200
200
 
201
201
  [[package]]
202
202
  name = "nosj_native"
203
- version = "0.5.0"
203
+ version = "0.5.1"
204
204
  dependencies = [
205
205
  "ahash",
206
206
  "magnus",
data/ext/nosj/Cargo.toml CHANGED
@@ -5,7 +5,7 @@
5
5
  # required by lib/nosj.rb as "nosj/nosj" (Init_nosj set explicitly in
6
6
  # lib.rs). Gem name, module, and API are plain nosj.
7
7
  name = "nosj_native"
8
- version = "0.5.0"
8
+ version = "0.5.1"
9
9
  edition = "2021"
10
10
  authors = ["Yaroslav Markin <yaroslav@markin.net>"]
11
11
  license = "MIT"
@@ -140,11 +140,19 @@ module NOSJFuzz
140
140
  raise "yielded values diverge from per-line parses" unless yielded.eql?(reference)
141
141
 
142
142
  if status == :ok && !yielded.empty?
143
- # Parsed values are always generable: parse refuses lone
144
- # surrogates, the only source of unencodable strings.
145
- ndjson = stage("generate_lines") { NOSJ.generate_lines_native(yielded, nil) }
143
+ # Parse refuses lone surrogates, the only unencodable strings, but
144
+ # an overflow float (1e999, 124.4e044754) parses to Infinity, which
145
+ # generates only under allow_nan; it lifts exactly that refusal,
146
+ # and the round-trip then reads Infinity back under it too.
147
+ opts = nil
148
+ ndjson = begin
149
+ NOSJ.generate_lines_native(yielded, nil)
150
+ rescue NOSJ::GeneratorError
151
+ opts = {allow_nan: true}
152
+ stage("generate_lines with allow_nan") { NOSJ.generate_lines_native(yielded, opts) }
153
+ end
146
154
  back = []
147
- NOSJ.each_line_native(ndjson, nil) { |v| back << v }
155
+ NOSJ.each_line_native(ndjson, opts) { |v| back << v }
148
156
  raise "generate_lines does not round-trip" unless back.eql?(yielded)
149
157
  end
150
158
  nil
@@ -227,6 +235,7 @@ module NOSJFuzz
227
235
  # --- the pure-Ruby reference implementations ------------------------
228
236
 
229
237
  def ref_splice(tree, edits)
238
+ edits.each_value { |value| generable(value) }
230
239
  token_lists = edits.keys.map { |ptr| ptr_tokens(ptr) }
231
240
  token_lists.combination(2) do |a, b|
232
241
  short, long = (a.length <= b.length) ? [a, b] : [b, a]
@@ -262,8 +271,8 @@ module NOSJFuzz
262
271
  path = op["path"]
263
272
  raise RefFail unless path.is_a?(String)
264
273
  case name
265
- when "add" then ref_add(box, ptr_tokens(path), fetch_value(op))
266
- when "replace" then ref_replace(box, ptr_tokens(path), fetch_value(op))
274
+ when "add" then ref_add(box, ptr_tokens(path), generable(fetch_value(op)))
275
+ when "replace" then ref_replace(box, ptr_tokens(path), generable(fetch_value(op)))
267
276
  when "remove" then ref_remove(box, ptr_tokens(path))
268
277
  when "move"
269
278
  from = op["from"]
@@ -366,6 +375,24 @@ module NOSJFuzz
366
375
  op["value"]
367
376
  end
368
377
 
378
+ # splice, add, and replace generate their values with default options,
379
+ # which refuse non-finite floats, and an overflow literal in the spec
380
+ # (1e999) parses to Infinity. test compares, and move/copy splice raw
381
+ # document bytes, so a 1e999 already in the document passes there.
382
+ def generable(value)
383
+ raise RefFail unless finite_floats?(value)
384
+ value
385
+ end
386
+
387
+ def finite_floats?(v)
388
+ case v
389
+ when Float then v.finite?
390
+ when Array then v.all? { |e| finite_floats?(e) }
391
+ when Hash then v.each_value.all? { |e| finite_floats?(e) }
392
+ else true
393
+ end
394
+ end
395
+
369
396
  def deep_dup(v)
370
397
  case v
371
398
  when Hash then v.transform_values { |e| deep_dup(e) }
@@ -119,11 +119,25 @@ fn container_children(
119
119
  }
120
120
 
121
121
  /// Split an RFC 6901 pointer into (parent, unescaped last token).
122
- /// Returns None for the root pointer "".
123
- fn split_pointer(pointer: &str) -> Option<(&str, String)> {
124
- let cut = pointer.rfind('/')?;
122
+ /// Returns None for the root pointer "" only: any other pointer must
123
+ /// start with '/', else the crate's own InvalidPointer error raises as
124
+ /// ArgumentError, exactly like `span_at`. (A slash-free path once read
125
+ /// as the root, and `add` replaced the whole document.)
126
+ fn split_pointer<'p>(ruby: &Ruby, pointer: &'p str) -> Result<Option<(&'p str, String)>, Error> {
127
+ if pointer.is_empty() {
128
+ return Ok(None);
129
+ }
130
+ if !pointer.starts_with('/') {
131
+ // Byte 0 is where the crate reports a missing leading slash.
132
+ let invalid = nosj::ParseError {
133
+ offset: 0,
134
+ kind: nosj::ErrorKind::InvalidPointer,
135
+ };
136
+ return Err(arg_error(ruby, invalid.to_string()));
137
+ }
138
+ let cut = pointer.rfind('/').unwrap_or(0);
125
139
  let token = pointer[cut + 1..].replace("~1", "/").replace("~0", "~");
126
- Some((&pointer[..cut], token))
140
+ Ok(Some((&pointer[..cut], token)))
127
141
  }
128
142
 
129
143
  /// RFC 6902 array index token: digits only, no leading zeros.
@@ -211,7 +225,7 @@ fn op_add(
211
225
  value: &Insert,
212
226
  cfg: &GenConfig,
213
227
  ) -> Result<Vec<u8>, Error> {
214
- let Some((parent, token)) = split_pointer(path) else {
228
+ let Some((parent, token)) = split_pointer(ruby, path)? else {
215
229
  // Root: the value becomes the entire document.
216
230
  return apply_edit(ruby, doc, 0, doc.len(), value, cfg);
217
231
  };
@@ -295,7 +309,7 @@ fn extend_insert(
295
309
  /// RFC 6902 `remove`: the entry disappears along with its key and one
296
310
  /// separating comma; surrounding formatting stays.
297
311
  fn op_remove(ruby: &Ruby, doc: &[u8], path: &str) -> Result<Vec<u8>, Error> {
298
- let Some((parent, token)) = split_pointer(path) else {
312
+ let Some((parent, token)) = split_pointer(ruby, path)? else {
299
313
  return Err(patch_error(ruby, "cannot remove the root".into()));
300
314
  };
301
315
  let Some((pstart, pend)) = span_at(ruby, doc, parent)? else {
data/lib/nosj/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NOSJ
4
4
  # The gem version.
5
- VERSION = "0.5.0"
5
+ VERSION = "0.5.1"
6
6
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nosj
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin