nosj 0.4.0 → 0.5.0
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 +4 -4
- data/CHANGELOG.md +105 -0
- data/Cargo.lock +17 -17
- data/README.md +32 -15
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- data/ext/nosj/fuzz/src/prelude.rb +20 -44
- data/ext/nosj/src/files.rs +21 -20
- data/ext/nosj/src/gen/errors.rs +12 -10
- data/ext/nosj/src/gen/mod.rs +8 -10
- data/ext/nosj/src/gen/opts.rs +76 -141
- data/ext/nosj/src/gen/ruby.rs +50 -22
- data/ext/nosj/src/gen/walker.rs +219 -102
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lib.rs +4 -0
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/locate.rs +162 -0
- data/ext/nosj/src/opt_reader.rs +170 -0
- data/ext/nosj/src/parse.rs +170 -99
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +83 -140
- data/ext/nosj/src/sink.rs +193 -60
- data/ext/nosj/src/state.rs +65 -14
- data/ext/nosj/src/stats.rs +35 -33
- data/lib/nosj/json.rb +105 -39
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +36 -22
- data/sig/nosj.rbs +4 -3
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 354e310be9d2d56119d2db1f9ad13052c9a6bc8fba5c8d7b7993b78abf345e1e
|
|
4
|
+
data.tar.gz: b21acc76a56a10a56c62c370ff1dfcaa8dc263a96958fcac6c4f3c213c4bb542
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c672e8653239dc1600ce19b4b38c31ae298dbf40a79449fe12ccf05335144eed7d210a115e87a3a60e8b534b868d3c0080079072ed21a8e67b398e2e8d7e8fc4
|
|
7
|
+
data.tar.gz: 22aadd62a736d86fe310ed099d995fe6006e149103eb95d41d128adc6164e6c2f64c2de49e6652437bccefbef70379b27cc3700d45fcad7ae1e2003ac6f05f41
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,108 @@
|
|
|
1
|
+
## [0.5.0] - 2026-09-25
|
|
2
|
+
|
|
3
|
+
**json 3.0 compatibility.** nosj now matches the behavior of the json
|
|
4
|
+
gem 3.0, and stays compatible with both the json 2.x and 3.0
|
|
5
|
+
interfaces: `NOSJ.parse`, `NOSJ.generate`, and the rest of nosj's own
|
|
6
|
+
API follow json 3.0 semantics, while the `nosj/json` drop-in follows
|
|
7
|
+
whichever json gem your application has installed, 2.x or 3.0, down to
|
|
8
|
+
its calling conventions and the documents it accepts. Upgrading json
|
|
9
|
+
(or not) is your choice; nosj works with either.
|
|
10
|
+
|
|
11
|
+
Behavior changes in nosj's own API, for input that was accepted
|
|
12
|
+
before:
|
|
13
|
+
|
|
14
|
+
- Duplicate object keys raise `NOSJ::ParserError` (positioned at the
|
|
15
|
+
object repeating the key, like json 3) in `parse`, `load_file`,
|
|
16
|
+
`valid?` (returns false), `minify`/`reformat`, and everything that
|
|
17
|
+
materializes values. `allow_duplicate_key: true` restores the old
|
|
18
|
+
behavior: the last value wins (in `minify`, repeated keys pass
|
|
19
|
+
through).
|
|
20
|
+
- Lone UTF-16 surrogates such as `"\udc00"` raise `NOSJ::ParserError`
|
|
21
|
+
everywhere, trailing ones included (they used to decode to raw
|
|
22
|
+
WTF-8 bytes, and `minify` re-escaped them).
|
|
23
|
+
- `generate` raises `NOSJ::GeneratorError` for keys that render alike
|
|
24
|
+
(`{"a" => 1, :a => 2}`, `{1 => 1, "1" => 2}`), with json 3's exact
|
|
25
|
+
message; `allow_duplicate_key: true` emits them as before. Hashes
|
|
26
|
+
whose keys are all of one kind are never checked. The Rails encoder
|
|
27
|
+
is unchanged.
|
|
28
|
+
- `stats` still describes such documents rather than refusing them.
|
|
29
|
+
- Unknown options raise ArgumentError with json 3's message
|
|
30
|
+
(`unknown keyword: foo`) in every entry point, instead of being
|
|
31
|
+
ignored. json options nosj does not implement (`object_class`,
|
|
32
|
+
`array_class`, `decimal_class`, `on_load`, `create_additions`,
|
|
33
|
+
`allow_comments`, `allow_control_characters`,
|
|
34
|
+
`allow_invalid_escape`, `sort_keys`, `as_json`) raise unless falsy;
|
|
35
|
+
`on_load` and the newer ones used to be silently ignored.
|
|
36
|
+
`escape_slash` is gone, as in json 3: use `script_safe`.
|
|
37
|
+
`quirks_mode` is no longer accepted. `stats` takes only the options
|
|
38
|
+
it documents (`max_nesting`, `allow_nan`, `allow_trailing_comma`).
|
|
39
|
+
|
|
40
|
+
The `nosj/json` drop-in follows the installed json gem:
|
|
41
|
+
|
|
42
|
+
- With json 3.0, `JSON.parse` takes keyword options only, `JSON.dump`
|
|
43
|
+
uses json 3's defaults (nesting capped at 100), and whatever json 3
|
|
44
|
+
refuses (`quirks_mode`, `escape_slash`, `create_additions`, unknown
|
|
45
|
+
options, positional option hashes) raises exactly as json 3 raises.
|
|
46
|
+
- With json 2.x, everything behaves as before, including json 2's
|
|
47
|
+
acceptance of duplicate keys, lone surrogates, and comments, and
|
|
48
|
+
its handling of keys that render alike.
|
|
49
|
+
- Whenever the fast path refuses a call, the installed gem runs it
|
|
50
|
+
again and decides: exceptions are now the gem's own (message,
|
|
51
|
+
`json_path`, `invalid_object`) rather than nosj's messages re-raised
|
|
52
|
+
as JSON classes. The second pass happens on failures only, and a
|
|
53
|
+
`generate` run twice this way calls `to_json` again on the objects
|
|
54
|
+
visited before the refusal.
|
|
55
|
+
- Fixed: `JSON.dump` raised NameError (`_dump_default_options`) with
|
|
56
|
+
json older than 2.11, which includes the json bundled with Ruby 3.3
|
|
57
|
+
and 3.4.
|
|
58
|
+
- Fixed: `JSON.generate` (and `pretty_generate`, `dump`) with both
|
|
59
|
+
`ascii_only` and `script_safe` raised ArgumentError; that combination,
|
|
60
|
+
which nosj does not implement, now goes to the gem.
|
|
61
|
+
|
|
62
|
+
## [0.4.1] - 2026-09-25
|
|
63
|
+
|
|
64
|
+
- Fixed a crash in `NOSJ.generate`: an object whose `to_json` or
|
|
65
|
+
`to_s` shrank the array being generated (for example with
|
|
66
|
+
`Array#clear`) made the generator read freed memory, usually a
|
|
67
|
+
segfault. The array length is now re-read for every element, like
|
|
68
|
+
the json gem, so elements a callback appends are emitted too.
|
|
69
|
+
- Fixed memory corruption in `NOSJ.splice`: a replacement value whose
|
|
70
|
+
`to_json` modified the source string, deduplicated a frozen String
|
|
71
|
+
subclass (`-str`), or removed entries from the edits hash could make
|
|
72
|
+
splice read freed memory, copying unrelated heap bytes into the
|
|
73
|
+
result or crashing. All values are now generated before the source
|
|
74
|
+
is read.
|
|
75
|
+
- Fixed `NOSJ.lazy` and `NOSJ.each_line` reading freed memory when the
|
|
76
|
+
source is a frozen String subclass (such as
|
|
77
|
+
`ActiveSupport::SafeBuffer`) or a frozen string carrying instance
|
|
78
|
+
variables, and it is deduplicated with `-str` while the lazy
|
|
79
|
+
document is alive or between lines: Ruby swaps such a string's
|
|
80
|
+
buffer, and the old one kept being read (wrong values, or a crash).
|
|
81
|
+
- Fixed a memory leak in `NOSJ.generate` and `NOSJ.write_file`: an
|
|
82
|
+
exception raised by user code the generator calls bypassed its
|
|
83
|
+
cleanup and leaked its output buffer (megabytes per call after large
|
|
84
|
+
documents). Affected: a raising `respond_to?` or
|
|
85
|
+
`respond_to_missing?`, a raising `to_s` on an encoding-conversion
|
|
86
|
+
error, a raising autoload of `JSON::Fragment` (strict and Rails
|
|
87
|
+
modes), and a raising `Errno` constructor for a failed write. The
|
|
88
|
+
exception still propagates unchanged.
|
|
89
|
+
- Fixed: after a `NoMemoryError` in the middle of a parse (or
|
|
90
|
+
`minify`/`reformat`), the next call on that thread aborted the whole
|
|
91
|
+
process. Per-thread parser state is now recovered instead.
|
|
92
|
+
- `NOSJ::Lazy` nodes now take part in generational GC: holding many
|
|
93
|
+
nodes no longer slows down every minor GC (200,000 live nodes: 2.9 ms
|
|
94
|
+
per minor GC before, 0.1 ms now), and they are freed immediately
|
|
95
|
+
when collected.
|
|
96
|
+
- Fixed: lazy documents opened with `allow_trailing_comma: true` or
|
|
97
|
+
`allow_nan: true` could not be walked. `size`, `keys`, `each`, and
|
|
98
|
+
any lookup that missed or stepped over a trailing comma or a `NaN`
|
|
99
|
+
raised `NOSJ::ParserError`, even though `value` worked.
|
|
100
|
+
- `NOSJ.at_pointer`, `NOSJ.at_pointers`, and `NOSJ.at_pointer_file`
|
|
101
|
+
now honor `allow_nan` and `allow_trailing_comma` while resolving the
|
|
102
|
+
pointer, not only when materializing the matched value.
|
|
103
|
+
- Updated the nosj crate to 0.2.2.
|
|
104
|
+
- Updated dependencies, Magnus bumped to 0.9.0.
|
|
105
|
+
|
|
1
106
|
## [0.4.0] - 2026-09-05
|
|
2
107
|
|
|
3
108
|
- Ractors: on Ruby 4.0+, `NOSJ.parse`, `NOSJ.generate`, and every
|
data/Cargo.lock
CHANGED
|
@@ -44,9 +44,9 @@ dependencies = [
|
|
|
44
44
|
|
|
45
45
|
[[package]]
|
|
46
46
|
name = "bitflags"
|
|
47
|
-
version = "2.13.
|
|
47
|
+
version = "2.13.2"
|
|
48
48
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
49
|
-
checksum = "
|
|
49
|
+
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
|
50
50
|
|
|
51
51
|
[[package]]
|
|
52
52
|
name = "cexpr"
|
|
@@ -59,9 +59,9 @@ dependencies = [
|
|
|
59
59
|
|
|
60
60
|
[[package]]
|
|
61
61
|
name = "cfg-if"
|
|
62
|
-
version = "1.0.
|
|
62
|
+
version = "1.0.5"
|
|
63
63
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
64
|
-
checksum = "
|
|
64
|
+
checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
|
|
65
65
|
|
|
66
66
|
[[package]]
|
|
67
67
|
name = "clang-sys"
|
|
@@ -137,9 +137,9 @@ dependencies = [
|
|
|
137
137
|
|
|
138
138
|
[[package]]
|
|
139
139
|
name = "magnus"
|
|
140
|
-
version = "0.
|
|
140
|
+
version = "0.9.0"
|
|
141
141
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
142
|
-
checksum = "
|
|
142
|
+
checksum = "379d50b6eea4ca84074ccd3e551eb9b07f7865194d5b3198325317e9ef78e5e9"
|
|
143
143
|
dependencies = [
|
|
144
144
|
"magnus-macros",
|
|
145
145
|
"rb-sys",
|
|
@@ -149,9 +149,9 @@ dependencies = [
|
|
|
149
149
|
|
|
150
150
|
[[package]]
|
|
151
151
|
name = "magnus-macros"
|
|
152
|
-
version = "0.
|
|
152
|
+
version = "0.9.0"
|
|
153
153
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
154
|
-
checksum = "
|
|
154
|
+
checksum = "4ca70566af98184bb92d4d9853daaf533c1a3418b9570d0547ea44970035f3fe"
|
|
155
155
|
dependencies = [
|
|
156
156
|
"proc-macro2",
|
|
157
157
|
"quote",
|
|
@@ -191,16 +191,16 @@ dependencies = [
|
|
|
191
191
|
|
|
192
192
|
[[package]]
|
|
193
193
|
name = "nosj"
|
|
194
|
-
version = "0.2.
|
|
194
|
+
version = "0.2.2"
|
|
195
195
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
196
|
-
checksum = "
|
|
196
|
+
checksum = "3d398b0019f2d8bf39b10de6038451b0f3d96a9e3111748097192d8f57d6f120"
|
|
197
197
|
dependencies = [
|
|
198
198
|
"fast-float2",
|
|
199
199
|
]
|
|
200
200
|
|
|
201
201
|
[[package]]
|
|
202
202
|
name = "nosj_native"
|
|
203
|
-
version = "0.
|
|
203
|
+
version = "0.5.0"
|
|
204
204
|
dependencies = [
|
|
205
205
|
"ahash",
|
|
206
206
|
"magnus",
|
|
@@ -335,9 +335,9 @@ dependencies = [
|
|
|
335
335
|
|
|
336
336
|
[[package]]
|
|
337
337
|
name = "unicode-ident"
|
|
338
|
-
version = "1.0.
|
|
338
|
+
version = "1.0.26"
|
|
339
339
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
340
|
-
checksum = "
|
|
340
|
+
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
|
|
341
341
|
|
|
342
342
|
[[package]]
|
|
343
343
|
name = "version_check"
|
|
@@ -368,18 +368,18 @@ checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
|
|
|
368
368
|
|
|
369
369
|
[[package]]
|
|
370
370
|
name = "zerocopy"
|
|
371
|
-
version = "0.8.
|
|
371
|
+
version = "0.8.58"
|
|
372
372
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
373
|
-
checksum = "
|
|
373
|
+
checksum = "c17e8fafad82b542ff3717217ecdc736231b59e387768c9630123b4ce4d2db44"
|
|
374
374
|
dependencies = [
|
|
375
375
|
"zerocopy-derive",
|
|
376
376
|
]
|
|
377
377
|
|
|
378
378
|
[[package]]
|
|
379
379
|
name = "zerocopy-derive"
|
|
380
|
-
version = "0.8.
|
|
380
|
+
version = "0.8.58"
|
|
381
381
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
382
|
-
checksum = "
|
|
382
|
+
checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
|
|
383
383
|
dependencies = [
|
|
384
384
|
"proc-macro2",
|
|
385
385
|
"quote",
|
data/README.md
CHANGED
|
@@ -65,6 +65,10 @@ through nosj:
|
|
|
65
65
|
require "nosj/json"
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
It works with json 2.x and 3.0 alike and follows whichever your app
|
|
69
|
+
has installed: its calling conventions, its defaults, and the documents
|
|
70
|
+
it accepts.
|
|
71
|
+
|
|
68
72
|
In a Bundler app (Rails included) that can live entirely in the
|
|
69
73
|
Gemfile you can do this:
|
|
70
74
|
|
|
@@ -108,9 +112,11 @@ The `json` gem API, on the `NOSJ` module:
|
|
|
108
112
|
|
|
109
113
|
```ruby
|
|
110
114
|
NOSJ.parse(src, symbolize_names: true) # also: freeze, max_nesting,
|
|
111
|
-
# allow_nan, allow_trailing_comma
|
|
115
|
+
# allow_nan, allow_trailing_comma,
|
|
116
|
+
# allow_duplicate_key
|
|
112
117
|
NOSJ.generate(obj) # indent, space, object_nl, ...,
|
|
113
|
-
NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict
|
|
118
|
+
NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict,
|
|
119
|
+
# allow_duplicate_key
|
|
114
120
|
```
|
|
115
121
|
|
|
116
122
|
### Lazy documents
|
|
@@ -151,7 +157,8 @@ Example: an early field resolves in ~0.35µs where `JSON.parse(json).dig(...)`
|
|
|
151
157
|
costs ~980µs on the same document—three orders of magnitude. A field
|
|
152
158
|
at the far end of a 570 KB document costs ~71µs, still 13× faster
|
|
153
159
|
than parse-then-dig. Misses return nil; matched subtrees materialize
|
|
154
|
-
with the same options as `parse` (`symbolize_names:`, `freeze:`)
|
|
160
|
+
with the same options as `parse` (`symbolize_names:`, `freeze:`), and
|
|
161
|
+
`allow_nan:`/`allow_trailing_comma:` govern the walk to them too.
|
|
155
162
|
|
|
156
163
|
### Files API
|
|
157
164
|
|
|
@@ -188,10 +195,11 @@ NOSJ.reformat_file("big.json") # straight off a memory map
|
|
|
188
195
|
```
|
|
189
196
|
|
|
190
197
|
Output is exactly `generate(parse(json))`—canonical number spellings,
|
|
191
|
-
normalized escapes, same formatting options—
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
`
|
|
198
|
+
normalized escapes, same formatting options—and it accepts exactly what
|
|
199
|
+
`parse` does. Acceptance options apply too:
|
|
200
|
+
`minify(src, allow_trailing_comma: true)` normalizes the commas away,
|
|
201
|
+
and under `allow_duplicate_key: true` repeated keys pass through (a
|
|
202
|
+
reformatter must not silently drop data).
|
|
195
203
|
|
|
196
204
|
### Byte-splicing edits and JSON Patch
|
|
197
205
|
|
|
@@ -402,19 +410,28 @@ Reproduce with `rake bench` (the parity-gated comparison, after a PGO retrain—
|
|
|
402
410
|
|
|
403
411
|
## Switching from the json gem
|
|
404
412
|
|
|
405
|
-
You mostly don't have to do anything.
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
413
|
+
You mostly don't have to do anything. `NOSJ.*` follows json 3.0,
|
|
414
|
+
whichever json your app runs; the `nosj/json` drop-in follows the
|
|
415
|
+
installed one, 2.x or 3.0. Some differences:
|
|
416
|
+
|
|
417
|
+
- json 3.0 semantics: duplicate keys, lone surrogates, JS comments,
|
|
418
|
+
and invalid UTF-8 are errors (`allow_duplicate_key: true` restores
|
|
419
|
+
last-key-wins), keys that render alike raise in `generate`, and
|
|
420
|
+
unknown options raise ArgumentError. With json 2.x installed, the
|
|
421
|
+
drop-in keeps json 2's leniency: when nosj refuses a call, the
|
|
422
|
+
installed gem runs it and has the last word.
|
|
423
|
+
- The json options nosj doesn't implement (`create_additions`,
|
|
424
|
+
`object_class`, `array_class`, `decimal_class`, `on_load`,
|
|
425
|
+
`allow_comments`, `allow_control_characters`,
|
|
426
|
+
`allow_invalid_escape`, `sort_keys`, `as_json`) raise ArgumentError
|
|
427
|
+
unless falsy; the drop-in passes them to the original gem.
|
|
412
428
|
- Unlike `Array#dig`, negative indices in `NOSJ.dig` return nil (JSON
|
|
413
429
|
Pointer has no equivalent).
|
|
414
430
|
- Parse errors raise `NOSJ::ParserError` (`NOSJ::NestingError` past
|
|
415
431
|
`max_nesting`, like the gem); messages use byte offsets rather than
|
|
416
432
|
the gem's phrasing, and the exception carries `#line`, `#column`,
|
|
417
|
-
and a caret `#snippet`.
|
|
433
|
+
and a caret `#snippet`. Through the drop-in, exceptions are the
|
|
434
|
+
installed gem's own (message, `json_path`, `invalid_object`).
|
|
418
435
|
|
|
419
436
|
Everything else—including the gem's exact float formatting, which is
|
|
420
437
|
not the shortest-round-trip form most libraries emit—matches
|
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.
|
|
8
|
+
version = "0.5.0"
|
|
9
9
|
edition = "2021"
|
|
10
10
|
authors = ["Yaroslav Markin <yaroslav@markin.net>"]
|
|
11
11
|
license = "MIT"
|
|
@@ -16,13 +16,13 @@ name = "nosj"
|
|
|
16
16
|
crate-type = ["cdylib", "rlib"]
|
|
17
17
|
|
|
18
18
|
[dependencies]
|
|
19
|
-
magnus = { version = "0.
|
|
20
|
-
rb-sys = { version = "0.9.
|
|
21
|
-
ahash = "0.8.
|
|
19
|
+
magnus = { version = "0.9", features = ["rb-sys"] }
|
|
20
|
+
rb-sys = { version = "0.9.130", default-features = false }
|
|
21
|
+
ahash = "0.8.12"
|
|
22
22
|
# Read-only file mapping for the file entry points (load_lazy_file,
|
|
23
23
|
# at_pointer_file, dig_file): pages never touched are never read.
|
|
24
|
-
memmap2 = "0.9"
|
|
24
|
+
memmap2 = "0.9.11"
|
|
25
25
|
# First-party SIMD JSON parse/generate library (github.com/yaroslav/nosj).
|
|
26
26
|
# For coordinated crate+gem work, temporarily flip to
|
|
27
27
|
# { path = "../../../nosj" } and restore before any commit or release.
|
|
28
|
-
nosj = "0.2.
|
|
28
|
+
nosj = "0.2.2"
|
data/ext/nosj/fuzz/Cargo.toml
CHANGED
|
@@ -13,7 +13,7 @@ libfuzzer-sys = "0.4"
|
|
|
13
13
|
# crate. Only this harness enables "embed" (rb-sys/link-ruby): fuzz
|
|
14
14
|
# binaries host their own VM, while the extension must never link
|
|
15
15
|
# libruby (its symbols resolve from the host process at load time).
|
|
16
|
-
magnus = { version = "0.
|
|
16
|
+
magnus = { version = "0.9", features = ["embed", "rb-sys"] }
|
|
17
17
|
# The extension as an rlib; its lib target is named `nosj` (the bundle
|
|
18
18
|
# name), so rename the dependency to keep fuzz code readable.
|
|
19
19
|
nosj_ext = { path = "..", package = "nosj_native" }
|
|
@@ -48,7 +48,14 @@ module NOSJFuzz
|
|
|
48
48
|
rescue *PARSE_FAIL
|
|
49
49
|
false
|
|
50
50
|
end
|
|
51
|
-
|
|
51
|
+
# stats describes documents rather than refusing them: it accepts
|
|
52
|
+
# everything parse accepts, plus duplicate keys and lone surrogates
|
|
53
|
+
# (json 3 refusals parse makes on top of the grammar).
|
|
54
|
+
raise "stats refused what parse accepted" if status == :ok && !stats_ok
|
|
55
|
+
if status == :err && stats_ok &&
|
|
56
|
+
try_parse(s, {allow_duplicate_key: true})[0] != :ok && !s.include?("\\u")
|
|
57
|
+
raise "stats accepted what parse refused for a grammar reason"
|
|
58
|
+
end
|
|
52
59
|
|
|
53
60
|
min_status, min = begin
|
|
54
61
|
[:ok, NOSJ.reformat_native(s, nil)]
|
|
@@ -59,24 +66,17 @@ module NOSJFuzz
|
|
|
59
66
|
end
|
|
60
67
|
|
|
61
68
|
if status == :err
|
|
62
|
-
# The pipe may abort with GeneratorError (
|
|
63
|
-
#
|
|
64
|
-
#
|
|
69
|
+
# The pipe may abort with GeneratorError (a non-finite float)
|
|
70
|
+
# before the parser reaches whatever made the whole document
|
|
71
|
+
# unparseable; any refusal is a refusal.
|
|
65
72
|
raise "reformat accepted what parse refused" unless [:err, :generator].include?(min_status)
|
|
66
73
|
return
|
|
67
74
|
end
|
|
68
75
|
if min_status == :generator
|
|
69
76
|
# On a document parse accepts, only an overflow-to-Infinity
|
|
70
|
-
# float (1e999, a 300-digit integer with a small exponent
|
|
71
|
-
#
|
|
72
|
-
|
|
73
|
-
# allow_nan separates them: it lifts the float refusal but not
|
|
74
|
-
# the key refusal, and the key requires a \u escape.
|
|
75
|
-
begin
|
|
76
|
-
NOSJ.reformat_native(s, {allow_nan: true})
|
|
77
|
-
rescue NOSJ::GeneratorError
|
|
78
|
-
raise "GeneratorError without a \\u escape in source" unless s.include?("\\u")
|
|
79
|
-
end
|
|
77
|
+
# float (1e999, a 300-digit integer with a small exponent) may
|
|
78
|
+
# abort the pipe, and allow_nan lifts exactly that refusal.
|
|
79
|
+
stage("reformat with allow_nan") { NOSJ.reformat_native(s, {allow_nan: true}) }
|
|
80
80
|
return
|
|
81
81
|
end
|
|
82
82
|
raise "reformat refused what parse accepted" unless min_status == :ok
|
|
@@ -140,11 +140,9 @@ 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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
return # WTF-8 strings (lone surrogates) are not generable
|
|
147
|
-
end
|
|
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) }
|
|
148
146
|
back = []
|
|
149
147
|
NOSJ.each_line_native(ndjson, nil) { |v| back << v }
|
|
150
148
|
raise "generate_lines does not round-trip" unless back.eql?(yielded)
|
|
@@ -168,16 +166,11 @@ module NOSJFuzz
|
|
|
168
166
|
doc = utf8(doc_bytes)
|
|
169
167
|
spec_status, spec = try_parse(utf8(spec_bytes))
|
|
170
168
|
return unless spec_status == :ok
|
|
171
|
-
# Replacement values with broken encoding (lone surrogates) raise
|
|
172
|
-
# GeneratorError on insertion; the reference cannot mirror that.
|
|
173
|
-
return unless deep_valid_encoding?(spec)
|
|
174
169
|
|
|
170
|
+
# Parsing refuses duplicate keys (raw-byte resolution would see the
|
|
171
|
+
# first occurrence, the tree the last) and lone surrogates, so an
|
|
172
|
+
# accepted tree and spec are free of both.
|
|
175
173
|
tree_status, tree = try_parse(doc)
|
|
176
|
-
# Duplicate keys: raw-byte resolution sees the first occurrence,
|
|
177
|
-
# tree materialization keeps the last; the two sides cannot agree.
|
|
178
|
-
if tree_status == :ok && NOSJ.stats_native(doc, nil)[:keys] != count_keys(tree)
|
|
179
|
-
return
|
|
180
|
-
end
|
|
181
174
|
|
|
182
175
|
case spec
|
|
183
176
|
when Hash
|
|
@@ -380,21 +373,4 @@ module NOSJFuzz
|
|
|
380
373
|
else v
|
|
381
374
|
end
|
|
382
375
|
end
|
|
383
|
-
|
|
384
|
-
def deep_valid_encoding?(v)
|
|
385
|
-
case v
|
|
386
|
-
when String then v.valid_encoding?
|
|
387
|
-
when Array then v.all? { |e| deep_valid_encoding?(e) }
|
|
388
|
-
when Hash then v.all? { |k, e| deep_valid_encoding?(k) && deep_valid_encoding?(e) }
|
|
389
|
-
else true
|
|
390
|
-
end
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def count_keys(v)
|
|
394
|
-
case v
|
|
395
|
-
when Hash then v.size + v.sum { |_, e| count_keys(e) }
|
|
396
|
-
when Array then v.sum { |e| count_keys(e) }
|
|
397
|
-
else 0
|
|
398
|
-
end
|
|
399
|
-
end
|
|
400
376
|
end
|
data/ext/nosj/src/files.rs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
//! I/O failures raise the mapped `Errno::*` exception, like `File`
|
|
11
11
|
//! methods do.
|
|
12
12
|
|
|
13
|
-
use std::cell::
|
|
13
|
+
use std::cell::Cell;
|
|
14
14
|
use std::fs;
|
|
15
15
|
use std::io::Read;
|
|
16
16
|
|
|
@@ -22,14 +22,15 @@ use crate::gen;
|
|
|
22
22
|
use crate::lazy::{self, DocBytes};
|
|
23
23
|
use crate::parse::{err, materialize, materialize_at, parse_native_opts, span_of, ParseNativeOpts};
|
|
24
24
|
use crate::pointer::path_to_pointer;
|
|
25
|
-
use crate::state::
|
|
25
|
+
use crate::state::{with_pull_state, with_taken};
|
|
26
26
|
|
|
27
27
|
const NOT_UTF8: &str = "input is not valid UTF-8";
|
|
28
28
|
|
|
29
29
|
thread_local! {
|
|
30
30
|
/// Reused read buffer for `load_file`: capacity survives across
|
|
31
|
-
/// calls, so a hot loop over files allocates nothing
|
|
32
|
-
|
|
31
|
+
/// calls, so a hot loop over files allocates nothing (see
|
|
32
|
+
/// `state::with_taken`).
|
|
33
|
+
static FILE_BUF: Cell<Vec<u8>> = const { Cell::new(Vec::new()) };
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/// On Unix the raw OS error IS the errno `rb_syserr_new` expects.
|
|
@@ -58,6 +59,9 @@ fn errno_of(e: &std::io::Error) -> Option<i32> {
|
|
|
58
59
|
|
|
59
60
|
/// Map an I/O failure onto the matching `Errno::*` exception (class
|
|
60
61
|
/// parity with `File.read`/`File.write`; the message carries the path).
|
|
62
|
+
/// Constructing it runs the class's `initialize` (overridable Ruby), so
|
|
63
|
+
/// the call is protected; a raise there propagates in its place, as it
|
|
64
|
+
/// would from `File.read`.
|
|
61
65
|
fn io_error(ruby: &Ruby, path: &str, e: &std::io::Error) -> Error {
|
|
62
66
|
use magnus::rb_sys::FromRawValue;
|
|
63
67
|
let Some(errno) = errno_of(e) else {
|
|
@@ -66,8 +70,13 @@ fn io_error(ruby: &Ruby, path: &str, e: &std::io::Error) -> Error {
|
|
|
66
70
|
let Ok(cpath) = std::ffi::CString::new(path) else {
|
|
67
71
|
return runtime_error(ruby, format!("{e} - {path}"));
|
|
68
72
|
};
|
|
73
|
+
let raw =
|
|
74
|
+
match magnus::rb_sys::protect(|| unsafe { rb_sys::rb_syserr_new(errno, cpath.as_ptr()) }) {
|
|
75
|
+
Ok(raw) => raw,
|
|
76
|
+
Err(raised) => return raised,
|
|
77
|
+
};
|
|
69
78
|
// SAFETY: rb_syserr_new returns a live Errno exception instance.
|
|
70
|
-
let exc = unsafe { Value::from_raw(
|
|
79
|
+
let exc = unsafe { Value::from_raw(raw) };
|
|
71
80
|
match magnus::Exception::from_value(exc) {
|
|
72
81
|
Some(exc) => exc.into(),
|
|
73
82
|
None => runtime_error(ruby, format!("{e} - {path}")),
|
|
@@ -84,23 +93,16 @@ pub fn load_file_native(
|
|
|
84
93
|
) -> Result<Value, Error> {
|
|
85
94
|
let o = parse_native_opts(ruby, opts)?;
|
|
86
95
|
let p = path.to_string()?;
|
|
87
|
-
FILE_BUF
|
|
88
|
-
let mut buf = cell
|
|
89
|
-
.try_borrow_mut()
|
|
90
|
-
.map_or_else(|_| Vec::new(), |mut b| std::mem::take(&mut *b));
|
|
96
|
+
with_taken(&FILE_BUF, |buf| {
|
|
91
97
|
buf.clear();
|
|
92
|
-
|
|
98
|
+
read_into(buf, &p)
|
|
93
99
|
.map_err(|e| io_error(ruby, &p, &e))
|
|
94
100
|
.and_then(|()| {
|
|
95
|
-
if std::str::from_utf8(
|
|
101
|
+
if std::str::from_utf8(buf).is_err() {
|
|
96
102
|
return Err(err(ruby, NOT_UTF8.into()));
|
|
97
103
|
}
|
|
98
|
-
materialize(ruby,
|
|
99
|
-
})
|
|
100
|
-
if let Ok(mut slot) = cell.try_borrow_mut() {
|
|
101
|
-
*slot = buf;
|
|
102
|
-
}
|
|
103
|
-
result
|
|
104
|
+
materialize(ruby, buf, &o)
|
|
105
|
+
})
|
|
104
106
|
})
|
|
105
107
|
}
|
|
106
108
|
|
|
@@ -190,10 +192,9 @@ fn resolve_file_pointer(
|
|
|
190
192
|
o: &ParseNativeOpts,
|
|
191
193
|
) -> Result<Value, Error> {
|
|
192
194
|
with_mapped_file(ruby, path, |map| {
|
|
193
|
-
let resolved =
|
|
194
|
-
let mut state = cell.borrow_mut();
|
|
195
|
+
let resolved = with_pull_state(|state| {
|
|
195
196
|
// SAFETY: UTF-8 checked by with_mapped_file.
|
|
196
|
-
unsafe { nosj::
|
|
197
|
+
unsafe { nosj::pointer_utf8_unchecked_with(&map, pointer, &mut state.bufs, o.popts) }
|
|
197
198
|
});
|
|
198
199
|
match resolved {
|
|
199
200
|
Ok(None) => Ok(ruby.qnil().as_value()),
|
data/ext/nosj/src/gen/errors.rs
CHANGED
|
@@ -7,7 +7,7 @@ use magnus::rb_sys::AsRawValue;
|
|
|
7
7
|
use magnus::value::ReprValue;
|
|
8
8
|
use magnus::{Error, Ruby};
|
|
9
9
|
|
|
10
|
-
use super::ruby::rstring_bytes;
|
|
10
|
+
use super::ruby::{protected_to_s, rstring_bytes};
|
|
11
11
|
use crate::errors::nosj_exception;
|
|
12
12
|
|
|
13
13
|
pub(super) enum GenFail {
|
|
@@ -24,26 +24,28 @@ pub(super) enum GenFail {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/// The exception's `to_s` (its message), matching what the gem embeds
|
|
27
|
-
/// when it wraps a secondary exception.
|
|
28
|
-
|
|
27
|
+
/// when it wraps a secondary exception. Protected: an exception class
|
|
28
|
+
/// may override `to_s`, and a raise there propagates in place of the
|
|
29
|
+
/// GeneratorError (as it does from the gem).
|
|
30
|
+
fn error_message(err: &Error) -> Result<String, Error> {
|
|
29
31
|
if let ErrorType::Exception(exc) = err.error_type() {
|
|
30
|
-
let s =
|
|
32
|
+
let s = protected_to_s(exc.as_value().as_raw())?;
|
|
31
33
|
// Safety: rb_obj_as_string returns a T_STRING; the bytes are
|
|
32
34
|
// copied into an owned String before any further Ruby call.
|
|
33
35
|
let bytes = unsafe { rstring_bytes(s) };
|
|
34
|
-
return String::from_utf8_lossy(bytes).into_owned();
|
|
36
|
+
return Ok(String::from_utf8_lossy(bytes).into_owned());
|
|
35
37
|
}
|
|
36
|
-
err.to_string()
|
|
38
|
+
Ok(err.to_string())
|
|
37
39
|
}
|
|
38
40
|
|
|
39
41
|
pub(super) fn raise_fail(ruby: &Ruby, fail: GenFail) -> Error {
|
|
40
42
|
match fail {
|
|
41
43
|
GenFail::Reraise(err) => err,
|
|
42
44
|
GenFail::Generator(msg) => Error::new(nosj_exception(ruby, "GeneratorError"), msg),
|
|
43
|
-
GenFail::GeneratorFrom(err) => {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
45
|
+
GenFail::GeneratorFrom(err) => match error_message(&err) {
|
|
46
|
+
Ok(msg) => Error::new(nosj_exception(ruby, "GeneratorError"), msg),
|
|
47
|
+
Err(raised) => raised,
|
|
48
|
+
},
|
|
47
49
|
GenFail::Nesting(limit) => Error::new(
|
|
48
50
|
nosj_exception(ruby, "NestingError"),
|
|
49
51
|
format!(
|
data/ext/nosj/src/gen/mod.rs
CHANGED
|
@@ -31,6 +31,7 @@ use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
|
31
31
|
use magnus::{Error, RString, Ruby, Value};
|
|
32
32
|
use std::cell::Cell;
|
|
33
33
|
|
|
34
|
+
use crate::state::with_taken;
|
|
34
35
|
use errors::raise_fail;
|
|
35
36
|
use keys::GenKeyCache;
|
|
36
37
|
pub(crate) use ruby::warm_up;
|
|
@@ -57,6 +58,7 @@ struct GenScratch {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
impl GenScratch {
|
|
61
|
+
#[cold]
|
|
60
62
|
fn fresh() -> Box<Self> {
|
|
61
63
|
Box::new(GenScratch {
|
|
62
64
|
buf: Vec::new(),
|
|
@@ -70,17 +72,13 @@ thread_local! {
|
|
|
70
72
|
static GEN_SCRATCH: Cell<Option<Box<GenScratch>>> = const { Cell::new(None) };
|
|
71
73
|
}
|
|
72
74
|
|
|
73
|
-
/// Run `f` on this thread's scratch
|
|
74
|
-
///
|
|
75
|
-
/// one
|
|
76
|
-
/// the old fallback arm's did).
|
|
75
|
+
/// Run `f` on this thread's scratch (see [`with_taken`]). A recursive
|
|
76
|
+
/// generate stores its own fresh scratch meanwhile; the outermost call's
|
|
77
|
+
/// warm one replaces it (the replaced key cache keeps its shadow).
|
|
77
78
|
fn with_scratch<R>(f: impl FnOnce(&mut GenScratch) -> R) -> R {
|
|
78
|
-
|
|
79
|
-
.
|
|
80
|
-
|
|
81
|
-
let result = f(&mut scratch);
|
|
82
|
-
GEN_SCRATCH.with(|cell| cell.set(Some(scratch)));
|
|
83
|
-
result
|
|
79
|
+
with_taken(&GEN_SCRATCH, |slot| {
|
|
80
|
+
f(slot.get_or_insert_with(GenScratch::fresh))
|
|
81
|
+
})
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
/// `NOSJ.generate(obj, opts = nil)`, registered as a variadic native
|