nosj 0.3.2 → 0.4.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: fad5b3acb2b252b61cf81404a272768c558aa84d68b0ae1ddccacda3b745fb51
4
- data.tar.gz: ed97497088278d3a925017d05d090175323e53c4a8337f9a39943db165b85d37
3
+ metadata.gz: 938bbe04908f0865a510008040c4a1bf8f1e4b1cb80821de3757e3ac078f2e47
4
+ data.tar.gz: feea0d89e6f7559642127a2333d01cc3f93b1d3763e54f6ed620b596a7102d30
5
5
  SHA512:
6
- metadata.gz: 4f96f9eef6dcd421071fdb88e92af5239265576b67ec7a4aab0b7306b704cd5a5c2274cea3dc7730a218216a28c385a2be67e03051bb1ed9ee54704e2e8ae448
7
- data.tar.gz: c5985fbf726d9303b0e0e640fa91eaaf34e0e199839812a5e4790926c44eb952e4337661734c1b739b55a6874d42323fdf3eab903518519cc4717d1c48b06c8b
6
+ metadata.gz: b1ff3aef1231141f9333030016f1ff308a81ca27a1ad8318fadf2ce42c77869756c5674716485c4b5be4ca948290f4450142c59eed76df2894bde460cd0530bc
7
+ data.tar.gz: 5e40f2cfb493c13b546ef3367e02efafb7eea5697181b0daad9efeed89f06db58b0db798cd5cad2f09555ee900c673753595584829ab333bdc125c45eaeca41a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,58 @@
1
+ ## [0.4.1] - 2026-09-25
2
+
3
+ - Fixed a crash in `NOSJ.generate`: an object whose `to_json` or
4
+ `to_s` shrank the array being generated (for example with
5
+ `Array#clear`) made the generator read freed memory, usually a
6
+ segfault. The array length is now re-read for every element, like
7
+ the json gem, so elements a callback appends are emitted too.
8
+ - Fixed memory corruption in `NOSJ.splice`: a replacement value whose
9
+ `to_json` modified the source string, deduplicated a frozen String
10
+ subclass (`-str`), or removed entries from the edits hash could make
11
+ splice read freed memory, copying unrelated heap bytes into the
12
+ result or crashing. All values are now generated before the source
13
+ is read.
14
+ - Fixed `NOSJ.lazy` and `NOSJ.each_line` reading freed memory when the
15
+ source is a frozen String subclass (such as
16
+ `ActiveSupport::SafeBuffer`) or a frozen string carrying instance
17
+ variables, and it is deduplicated with `-str` while the lazy
18
+ document is alive or between lines: Ruby swaps such a string's
19
+ buffer, and the old one kept being read (wrong values, or a crash).
20
+ - Fixed a memory leak in `NOSJ.generate` and `NOSJ.write_file`: an
21
+ exception raised by user code the generator calls bypassed its
22
+ cleanup and leaked its output buffer (megabytes per call after large
23
+ documents). Affected: a raising `respond_to?` or
24
+ `respond_to_missing?`, a raising `to_s` on an encoding-conversion
25
+ error, a raising autoload of `JSON::Fragment` (strict and Rails
26
+ modes), and a raising `Errno` constructor for a failed write. The
27
+ exception still propagates unchanged.
28
+ - Fixed: after a `NoMemoryError` in the middle of a parse (or
29
+ `minify`/`reformat`), the next call on that thread aborted the whole
30
+ process. Per-thread parser state is now recovered instead.
31
+ - `NOSJ::Lazy` nodes now take part in generational GC: holding many
32
+ nodes no longer slows down every minor GC (200,000 live nodes: 2.9 ms
33
+ per minor GC before, 0.1 ms now), and they are freed immediately
34
+ when collected.
35
+ - Fixed: lazy documents opened with `allow_trailing_comma: true` or
36
+ `allow_nan: true` could not be walked. `size`, `keys`, `each`, and
37
+ any lookup that missed or stepped over a trailing comma or a `NaN`
38
+ raised `NOSJ::ParserError`, even though `value` worked.
39
+ - `NOSJ.at_pointer`, `NOSJ.at_pointers`, and `NOSJ.at_pointer_file`
40
+ now honor `allow_nan` and `allow_trailing_comma` while resolving the
41
+ pointer, not only when materializing the matched value.
42
+ - Updated the nosj crate to 0.2.2.
43
+ - Updated dependencies, Magnus bumped to 0.9.0.
44
+
45
+ ## [0.4.0] - 2026-09-05
46
+
47
+ - Ractors: on Ruby 4.0+, `NOSJ.parse`, `NOSJ.generate`, and every
48
+ other entry point (lazy documents, partial parsing, the file APIs,
49
+ NDJSON, patches, reformatting, statistics) can be called from inside
50
+ a Ractor. Before this release each of them raised
51
+ `Ractor::UnsafeError` outside the main Ractor. Values parsed with
52
+ `freeze: true` are Ractor-shareable, so a document parsed in one
53
+ Ractor can be handed to another without copying.
54
+ - Updated Rust dependencies.
55
+
1
56
  ## [0.3.2] - 2026-07-19
2
57
 
3
58
  - **IMPORTANT**—Fixed: the precompiled platform gems linked `libruby`
data/Cargo.lock CHANGED
@@ -17,9 +17,9 @@ dependencies = [
17
17
 
18
18
  [[package]]
19
19
  name = "aho-corasick"
20
- version = "1.1.4"
20
+ version = "1.1.5"
21
21
  source = "registry+https://github.com/rust-lang/crates.io-index"
22
- checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
22
+ checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
23
23
  dependencies = [
24
24
  "memchr",
25
25
  ]
@@ -44,9 +44,9 @@ dependencies = [
44
44
 
45
45
  [[package]]
46
46
  name = "bitflags"
47
- version = "2.13.0"
47
+ version = "2.13.2"
48
48
  source = "registry+https://github.com/rust-lang/crates.io-index"
49
- checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
49
+ checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
50
50
 
51
51
  [[package]]
52
52
  name = "cexpr"
@@ -59,15 +59,15 @@ dependencies = [
59
59
 
60
60
  [[package]]
61
61
  name = "cfg-if"
62
- version = "1.0.4"
62
+ version = "1.0.5"
63
63
  source = "registry+https://github.com/rust-lang/crates.io-index"
64
- checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
64
+ checksum = "4e7648175b45a9a48536d676f68d918270699102aa8dab5496df06904c914600"
65
65
 
66
66
  [[package]]
67
67
  name = "clang-sys"
68
- version = "1.8.1"
68
+ version = "1.9.1"
69
69
  source = "registry+https://github.com/rust-lang/crates.io-index"
70
- checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
70
+ checksum = "157a8ba7b480713b56f4c09fd13fc3e0a22a5dfab8097ba61cbc5feef950788a"
71
71
  dependencies = [
72
72
  "glob",
73
73
  "libc",
@@ -76,15 +76,15 @@ dependencies = [
76
76
 
77
77
  [[package]]
78
78
  name = "either"
79
- version = "1.16.0"
79
+ version = "1.18.0"
80
80
  source = "registry+https://github.com/rust-lang/crates.io-index"
81
- checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
81
+ checksum = "252afb9ae5eaa683babdc6a068b3f5726eb19e05070c731f9b2a23a7c3e8ed34"
82
82
 
83
83
  [[package]]
84
84
  name = "fast-float2"
85
- version = "0.2.3"
85
+ version = "0.2.4"
86
86
  source = "registry+https://github.com/rust-lang/crates.io-index"
87
- checksum = "f8eb564c5c7423d25c886fb561d1e4ee69f72354d16918afa32c08811f6b6a55"
87
+ checksum = "c6e8948ce679d00a02a94739ea185595dca7118ed04feb991127e443bd3d761f"
88
88
 
89
89
  [[package]]
90
90
  name = "getrandom"
@@ -100,9 +100,9 @@ dependencies = [
100
100
 
101
101
  [[package]]
102
102
  name = "glob"
103
- version = "0.3.3"
103
+ version = "0.3.4"
104
104
  source = "registry+https://github.com/rust-lang/crates.io-index"
105
- checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
105
+ checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
106
106
 
107
107
  [[package]]
108
108
  name = "itertools"
@@ -121,9 +121,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
121
121
 
122
122
  [[package]]
123
123
  name = "libc"
124
- version = "0.2.186"
124
+ version = "0.2.189"
125
125
  source = "registry+https://github.com/rust-lang/crates.io-index"
126
- checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
126
+ checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
127
127
 
128
128
  [[package]]
129
129
  name = "libloading"
@@ -137,9 +137,9 @@ dependencies = [
137
137
 
138
138
  [[package]]
139
139
  name = "magnus"
140
- version = "0.8.2"
140
+ version = "0.9.0"
141
141
  source = "registry+https://github.com/rust-lang/crates.io-index"
142
- checksum = "3b36a5b126bbe97eb0d02d07acfeb327036c6319fd816139a49824a83b7f9012"
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.8.0"
152
+ version = "0.9.0"
153
153
  source = "registry+https://github.com/rust-lang/crates.io-index"
154
- checksum = "47607461fd8e1513cb4f2076c197d8092d921a1ea75bd08af97398f593751892"
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.0"
194
+ version = "0.2.2"
195
195
  source = "registry+https://github.com/rust-lang/crates.io-index"
196
- checksum = "d25913644b7871b36095cc3da27daf95ebf9508189c5d8ccfb16da3bdb9f802f"
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.3.2"
203
+ version = "0.4.1"
204
204
  dependencies = [
205
205
  "ahash",
206
206
  "magnus",
@@ -217,18 +217,18 @@ checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
217
217
 
218
218
  [[package]]
219
219
  name = "proc-macro2"
220
- version = "1.0.106"
220
+ version = "1.0.107"
221
221
  source = "registry+https://github.com/rust-lang/crates.io-index"
222
- checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
222
+ checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
223
223
  dependencies = [
224
224
  "unicode-ident",
225
225
  ]
226
226
 
227
227
  [[package]]
228
228
  name = "quote"
229
- version = "1.0.46"
229
+ version = "1.0.47"
230
230
  source = "registry+https://github.com/rust-lang/crates.io-index"
231
- checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368"
231
+ checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
232
232
  dependencies = [
233
233
  "proc-macro2",
234
234
  ]
@@ -241,18 +241,18 @@ checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
241
241
 
242
242
  [[package]]
243
243
  name = "rb-sys"
244
- version = "0.9.128"
244
+ version = "0.9.130"
245
245
  source = "registry+https://github.com/rust-lang/crates.io-index"
246
- checksum = "45ca28513560e56cfb79a62b1fce363c73af170a182024ce880c77ee9429920a"
246
+ checksum = "02faf625bb10ba893e3ae620f19c9fb1b5f8fcae0fe4eb86bb3f2230fad75edb"
247
247
  dependencies = [
248
248
  "rb-sys-build",
249
249
  ]
250
250
 
251
251
  [[package]]
252
252
  name = "rb-sys-build"
253
- version = "0.9.128"
253
+ version = "0.9.130"
254
254
  source = "registry+https://github.com/rust-lang/crates.io-index"
255
- checksum = "ce04b2c55eff3a21aaa623fcc655d94373238e72cac6b3e1a3641ff31649f99a"
255
+ checksum = "05be6f9c86fe5482808162826f6c047d093b3670582296c770de1d94f8066694"
256
256
  dependencies = [
257
257
  "bindgen",
258
258
  "lazy_static",
@@ -271,9 +271,9 @@ checksum = "cca7ad6a7e21e72151d56fe2495a259b5670e204c3adac41ee7ef676ea08117a"
271
271
 
272
272
  [[package]]
273
273
  name = "regex"
274
- version = "1.13.0"
274
+ version = "1.13.1"
275
275
  source = "registry+https://github.com/rust-lang/crates.io-index"
276
- checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2"
276
+ checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
277
277
  dependencies = [
278
278
  "aho-corasick",
279
279
  "memchr",
@@ -283,9 +283,9 @@ dependencies = [
283
283
 
284
284
  [[package]]
285
285
  name = "regex-automata"
286
- version = "0.4.15"
286
+ version = "0.4.18"
287
287
  source = "registry+https://github.com/rust-lang/crates.io-index"
288
- checksum = "1f388202e4b80542a0921078cc23b6333bcf1409c1e3f86404cae4766a6131db"
288
+ checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
289
289
  dependencies = [
290
290
  "aho-corasick",
291
291
  "memchr",
@@ -335,9 +335,9 @@ dependencies = [
335
335
 
336
336
  [[package]]
337
337
  name = "unicode-ident"
338
- version = "1.0.24"
338
+ version = "1.0.26"
339
339
  source = "registry+https://github.com/rust-lang/crates.io-index"
340
- checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
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.54"
371
+ version = "0.8.58"
372
372
  source = "registry+https://github.com/rust-lang/crates.io-index"
373
- checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19"
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.54"
380
+ version = "0.8.58"
381
381
  source = "registry+https://github.com/rust-lang/crates.io-index"
382
- checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5"
382
+ checksum = "595f56e044df4f46a0c9a626f65c3d99eb8488f7e8a8baa12dd76326d9710bf2"
383
383
  dependencies = [
384
384
  "proc-macro2",
385
385
  "quote",
data/README.md CHANGED
@@ -16,6 +16,7 @@ faster than Yajl—[see Benchmarks](#benchmarks).
16
16
  - It does **byte-splicing edits**: `NOSJ.splice` changes a field inside a passing payload 10–51× faster than parse-mutate-generate, and RFC 6902 JSON Patch / RFC 7386 merge patch apply straight to the raw string.
17
17
  - It is **great to debug with**: parse errors carry line, column, and a caret snippet pointing at the break, and `NOSJ.stats` X-rays a mystery blob (depth, value counts, key histogram) faster than parsing it.
18
18
  - It accelerates a **Rails** application in both encoding and decoding.
19
+ - It is **Ractor-safe**: call it from any Ractor on Ruby 4.0+, and parse with `freeze: true` to get shareable results.
19
20
  - It comes **precompiled** (platform gems built with per-platform optimizations,
20
21
  nothing to compile on install).
21
22
  - Otherwise, same API and option names as gem json.
@@ -150,7 +151,8 @@ Example: an early field resolves in ~0.35µs where `JSON.parse(json).dig(...)`
150
151
  costs ~980µs on the same document—three orders of magnitude. A field
151
152
  at the far end of a 570 KB document costs ~71µs, still 13× faster
152
153
  than parse-then-dig. Misses return nil; matched subtrees materialize
153
- with the same options as `parse` (`symbolize_names:`, `freeze:`).
154
+ with the same options as `parse` (`symbolize_names:`, `freeze:`), and
155
+ `allow_nan:`/`allow_trailing_comma:` govern the walk to them too.
154
156
 
155
157
  ### Files API
156
158
 
@@ -300,6 +302,22 @@ NOSJ.parse(%({\n "a": 1,\n "b": }))
300
302
  # ^
301
303
  ```
302
304
 
305
+ ### Ractors
306
+
307
+ On Ruby 4.0+, every entry point works inside a Ractor: parse and
308
+ generate, lazy documents, partial parsing, the file APIs, NDJSON,
309
+ patches, reformatting, and statistics.
310
+
311
+ ```ruby
312
+ workers = Array.new(4) do
313
+ Ractor.new(payload) { |src| NOSJ.parse(src, freeze: true) }
314
+ end
315
+ workers.map(&:value)
316
+ ```
317
+
318
+ `freeze: true` returns deeply frozen, Ractor-shareable values, so a
319
+ document parsed in one Ractor can be handed to another without a copy.
320
+
303
321
  ## Benchmarks
304
322
 
305
323
  Every installed JSON gem, benchmark-ips: AWS EC2 c7a.2xlarge (AMD EPYC 9R14, Zen 4), Ruby 4.0.6 + YJIT, json 2.21.1, Oj 3.17.4, RapidJSON 0.4.0, FastJsonparser 0.6.0, Yajl 1.4.3, PGO build, 2026-07-16. `×N` = times slower than nosj.
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.3.2"
8
+ version = "0.4.1"
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.8", features = ["rb-sys"] }
20
- rb-sys = { version = "0.9.124", default-features = false }
21
- ahash = "0.8.11"
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.0"
28
+ nosj = "0.2.2"
@@ -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.8", features = ["embed", "rb-sys"] }
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" }
@@ -16,7 +16,9 @@ use magnus::{Class, Error, ExceptionClass, Module, Object, RModule, RObject, Rub
16
16
  /// extension is loaded without lib/nosj.rb).
17
17
  pub(crate) fn nosj_exception(ruby: &Ruby, name: &str) -> ExceptionClass {
18
18
  let lookup = || -> Result<ExceptionClass, Error> {
19
- let m: RModule = ruby.define_module("NOSJ")?;
19
+ // A lookup, not define_module: defining is main-Ractor work, and
20
+ // this runs wherever a parse fails.
21
+ let m: RModule = ruby.class_object().const_get("NOSJ")?;
20
22
  m.const_get(name)
21
23
  };
22
24
  lookup().unwrap_or_else(|_| ruby.exception_runtime_error())
@@ -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::RefCell;
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::PULL_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
- static FILE_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
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(rb_sys::rb_syserr_new(errno, cpath.as_ptr())) };
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.with(|cell| {
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
- let result = read_into(&mut buf, &p)
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(&buf).is_err() {
101
+ if std::str::from_utf8(buf).is_err() {
96
102
  return Err(err(ruby, NOT_UTF8.into()));
97
103
  }
98
- materialize(ruby, &buf, &o)
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 = PULL_STATE.with(|cell| {
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::pointer_utf8_unchecked(&map, pointer, &mut state.bufs) }
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()),
@@ -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
- fn error_message(err: &Error) -> String {
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 = unsafe { rb_sys::rb_obj_as_string(exc.as_value().as_raw()) };
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
- let msg = error_message(&err);
45
- Error::new(nosj_exception(ruby, "GeneratorError"), msg)
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!(