nosj 0.4.0 → 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.
@@ -7,7 +7,7 @@ use magnus::{Error, RString, Ruby, Value};
7
7
 
8
8
  use crate::errors::parser_error_at;
9
9
  use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
10
- use crate::state::PULL_STATE;
10
+ use crate::state::with_pull_state;
11
11
 
12
12
  /// Resolve one JSON Pointer against `data`, materializing the matched
13
13
  /// subtree; `nil` when the pointer misses.
@@ -19,12 +19,11 @@ fn at_pointer_impl(
19
19
  ) -> Result<Value, Error> {
20
20
  use magnus::value::ReprValue;
21
21
  let input = utf8_input(ruby, &data)?;
22
- // Resolve first (one PULL_STATE borrow), then materialize (a fresh
23
- // borrow); the resolved slice borrows `input`, not the buffers.
24
- let resolved = PULL_STATE.with(|cell| {
25
- let mut state = cell.borrow_mut();
22
+ // Resolve, then materialize, as two separate uses of the parse
23
+ // state; the resolved slice borrows `input`, not the state.
24
+ let resolved = with_pull_state(|state| {
26
25
  // Safety: coderange verified above.
27
- unsafe { nosj::pointer_utf8_unchecked(input, pointer, &mut state.bufs) }
26
+ unsafe { nosj::pointer_utf8_unchecked_with(input, pointer, &mut state.bufs, o.popts) }
28
27
  });
29
28
  match resolved {
30
29
  Ok(None) => Ok(ruby.qnil().as_value()),
@@ -125,12 +124,11 @@ fn at_pointers_impl(
125
124
  let input = utf8_input(ruby, &data)?;
126
125
  let live: Vec<&str> = pointers.iter().flatten().map(String::as_str).collect();
127
126
 
128
- // Resolve first (one PULL_STATE borrow); the resolved slices borrow
129
- // `input`, not the buffers, so materializing can re-borrow freely.
130
- let resolved = PULL_STATE.with(|cell| {
131
- let mut state = cell.borrow_mut();
127
+ // Resolve first (one use of the parse state); the resolved slices
128
+ // borrow `input`, not the state, so each materializes separately.
129
+ let resolved = with_pull_state(|state| {
132
130
  // Safety: coderange verified by utf8_input.
133
- unsafe { nosj::pointers_utf8_unchecked(input, &live, &mut state.bufs) }
131
+ unsafe { nosj::pointers_utf8_unchecked_with(input, &live, &mut state.bufs, o.popts) }
134
132
  });
135
133
  let mut hits = match resolved {
136
134
  Ok(hits) => hits.into_iter(),
@@ -13,7 +13,7 @@
13
13
  //! canonical spelling (`1.50` becomes `1.5`), and string escapes are
14
14
  //! normalized by the emission kernels.
15
15
 
16
- use std::cell::RefCell;
16
+ use std::cell::Cell;
17
17
 
18
18
  use magnus::value::ReprValue;
19
19
  use magnus::{Error, RString, Ruby, Value};
@@ -23,16 +23,15 @@ use nosj::{FloatFormat, WriteOptions, Writer};
23
23
  use crate::errors::{nesting_error, nosj_exception, parser_error, parser_error_at};
24
24
  use crate::files::with_mapped_file;
25
25
  use crate::gen::opts::{parse_gen_opts, GenConfig, DEFAULT_CONFIG};
26
- use crate::parse::{parse_native_opts, utf8_input};
26
+ use crate::parse::{parse_native_opts, utf8_input, ParseNativeOpts};
27
27
  use crate::patch::finish_string;
28
28
  use crate::sink::SinkAbort;
29
- use crate::state::PULL_STATE;
29
+ use crate::state::{with_pull_state, with_taken};
30
30
 
31
31
  thread_local! {
32
- /// Pooled output buffer: capacity survives across calls. The pipe
33
- /// never calls back into Ruby, so the borrow spans the whole drive
34
- /// without any reentrancy concern.
35
- static PIPE_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
32
+ /// Pooled output buffer: capacity survives across calls (see
33
+ /// `state::with_taken`).
34
+ static PIPE_BUF: Cell<Vec<u8>> = const { Cell::new(Vec::new()) };
36
35
  }
37
36
 
38
37
  /// The event-to-Writer pipe. Structure events forward to the Writer's
@@ -230,37 +229,50 @@ fn write_options(cfg: &GenConfig) -> WriteOptions {
230
229
  w
231
230
  }
232
231
 
232
+ /// Decoded reformat options: parse acceptance plus generate formatting.
233
+ /// Decoded before the source is borrowed, since decoding can run Ruby
234
+ /// (an option value's `to_int`; see `parse::utf8_input`).
235
+ struct ReformatOpts {
236
+ parse: ParseNativeOpts,
237
+ generate: Option<GenConfig>,
238
+ }
239
+
240
+ impl ReformatOpts {
241
+ fn decode(ruby: &Ruby, opts: Value) -> Result<Self, Error> {
242
+ Ok(Self {
243
+ parse: parse_native_opts(ruby, opts)?,
244
+ generate: if opts.is_nil() {
245
+ None
246
+ } else {
247
+ Some(parse_gen_opts(ruby, opts)?.0)
248
+ },
249
+ })
250
+ }
251
+ }
252
+
233
253
  /// Run the pipe over already-UTF-8-vouched bytes.
234
- fn reformat_over(ruby: &Ruby, input: &[u8], opts: Value) -> Result<RString, Error> {
235
- let po = parse_native_opts(ruby, opts)?;
236
- let built;
237
- let gcfg: &GenConfig = if opts.is_nil() {
238
- &DEFAULT_CONFIG
239
- } else {
240
- built = parse_gen_opts(ruby, opts)?.0;
241
- &built
242
- };
254
+ fn reformat_over(ruby: &Ruby, input: &[u8], opts: &ReformatOpts) -> Result<RString, Error> {
255
+ let po = &opts.parse;
256
+ let gcfg = opts.generate.as_ref().unwrap_or(&DEFAULT_CONFIG);
243
257
  let wopts = write_options(gcfg);
244
258
 
245
- PIPE_BUF.with(|cell| {
246
- let mut buf = cell.borrow_mut();
259
+ with_taken(&PIPE_BUF, |buf| {
247
260
  buf.clear();
248
261
  // The output is at least input-sized for minify-shaped runs.
249
262
  buf.reserve(input.len());
250
263
  let mut sink = PipeSink {
251
- w: Writer::new(&mut buf, &wopts),
264
+ w: Writer::new(buf, &wopts),
252
265
  depth: 0,
253
266
  max_nesting: po.max_nesting,
254
267
  mode: gcfg.mode,
255
268
  allow_nan: gcfg.allow_nan,
256
269
  };
257
- let result = PULL_STATE.with(|state_cell| {
258
- let mut state = state_cell.borrow_mut();
270
+ let result = with_pull_state(|state| {
259
271
  // Safety: callers verified UTF-8 (coderange or full scan).
260
272
  unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, po.popts) }
261
273
  });
262
274
  match result {
263
- Ok(()) => finish_string(&buf),
275
+ Ok(()) => finish_string(buf),
264
276
  Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
265
277
  ruby,
266
278
  format!(
@@ -296,8 +308,9 @@ pub fn reformat_native(
296
308
  data: RString,
297
309
  opts: Value,
298
310
  ) -> Result<RString, Error> {
311
+ let opts = ReformatOpts::decode(ruby, opts)?;
299
312
  let input = utf8_input(ruby, &data)?;
300
- reformat_over(ruby, input, opts)
313
+ reformat_over(ruby, input, &opts)
301
314
  }
302
315
 
303
316
  /// `NOSJ.reformat_file_native(path, opts)`: the pipe over a read-only
@@ -309,12 +322,13 @@ pub fn reformat_file_native(
309
322
  opts: Value,
310
323
  ) -> Result<RString, Error> {
311
324
  let p = path.to_string()?;
325
+ let opts = ReformatOpts::decode(ruby, opts)?;
312
326
  // Mapping a zero-length file fails with EINVAL on Linux; route an
313
327
  // empty file to the parser's own "unexpected end of input" so the
314
328
  // error class is deterministic across platforms. Metadata failures
315
329
  // fall through for the mapper's Errno.
316
330
  if std::fs::metadata(&p).is_ok_and(|m| m.len() == 0) {
317
- return reformat_over(ruby, &[], opts);
331
+ return reformat_over(ruby, &[], &opts);
318
332
  }
319
- with_mapped_file(ruby, &p, |map| reformat_over(ruby, &map, opts))
333
+ with_mapped_file(ruby, &p, |map| reformat_over(ruby, &map, &opts))
320
334
  }
data/ext/nosj/src/sink.rs CHANGED
@@ -35,24 +35,21 @@ pub(crate) enum SinkAbort {
35
35
  NonFiniteFloat(&'static str),
36
36
  }
37
37
 
38
- /// `RB_INT2FIX` ported from Ruby's public inline headers: fixnums are
39
- /// `(i << 1) + 1` for `|i| <= LONG_MAX / 2`, and the range is defined by
40
- /// the C `long`, which is 32-bit on Windows (LLP64): fixnums there hold
41
- /// only 31 bits, and tagging anything wider crashes Ruby with
42
- /// "Unnormalized Fixnum value". C extensions get the check inlined by the
43
- /// header; going through the extern `rb_ll2inum` costs an FFI call per
44
- /// integer. Non-fixable values still take the call.
45
- // The widening is an identity on LP64 hosts (clippy flags it there) but
46
- // required on Windows, where c_long is 32-bit.
47
- #[allow(clippy::unnecessary_cast)]
38
+ /// Integer VALUE for `i` via rb-sys's inline `LONG2NUM` (the header
39
+ /// macro: a Fixnum tagged inline when it fits, else a Bignum), which
40
+ /// saves the FFI call per integer that `rb_ll2inum` costs. The fixable
41
+ /// range is defined by the C `long`, which is 32-bit on Windows (LLP64):
42
+ /// fixnums there hold only 31 bits, and tagging anything wider crashes
43
+ /// Ruby with "Unnormalized Fixnum value"; values beyond `long` take
44
+ /// `rb_ll2inum`.
45
+ // The conversion is an identity on LP64 hosts (clippy flags it there)
46
+ // but narrows on Windows, where c_long is 32-bit.
47
+ #[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
48
48
  #[inline(always)]
49
49
  fn int_to_raw(i: i64) -> rb_sys::VALUE {
50
- const FIXABLE_MIN: i64 = (std::os::raw::c_long::MIN / 2) as i64;
51
- const FIXABLE_MAX: i64 = (std::os::raw::c_long::MAX / 2) as i64;
52
- if (FIXABLE_MIN..=FIXABLE_MAX).contains(&i) {
53
- ((i as u64) << 1).wrapping_add(1) as rb_sys::VALUE
54
- } else {
55
- unsafe { rb_sys::rb_ll2inum(i) }
50
+ match std::os::raw::c_long::try_from(i) {
51
+ Ok(l) => rb_sys::macros::LONG2NUM(l),
52
+ Err(_) => unsafe { rb_sys::rb_ll2inum(i) },
56
53
  }
57
54
  }
58
55
 
@@ -10,18 +10,36 @@ use ahash::AHashMap;
10
10
  use magnus::typed_data::Obj;
11
11
  use magnus::{DataTypeFunctions, TypedData};
12
12
  use nosj::Buffers;
13
- use std::cell::RefCell;
13
+ use std::cell::Cell;
14
+ use std::thread::LocalKey;
14
15
 
15
- /// Everything a parse touches, allocated once per thread and reused:
16
- /// nosj's scratch buffers, the interned-key caches, and the GC-marked
17
- /// stacks.
16
+ /// Run `f` on a pooled thread-local value, taken OUT of its cell for the
17
+ /// call and stored back afterwards. Pooled state is never borrowed
18
+ /// across a call that can reach Ruby: any allocation can raise
19
+ /// NoMemoryError, whose longjmp skips these frames, and a RefCell borrow
20
+ /// held across it would stay borrowed for good (under panic=abort, the
21
+ /// next borrow kills the process). A value lost that way is only leaked;
22
+ /// the next call, like a nested one finding the cell empty, starts from
23
+ /// `T::default()`. The outermost call's value is the one stored back.
24
+ pub(crate) fn with_taken<T: Default, R>(
25
+ key: &'static LocalKey<Cell<T>>,
26
+ f: impl FnOnce(&mut T) -> R,
27
+ ) -> R {
28
+ let mut value = key.take();
29
+ let result = f(&mut value);
30
+ key.set(value);
31
+ result
32
+ }
33
+
34
+ /// Everything a parse touches, reused across calls: nosj's scratch
35
+ /// buffers, the interned-key caches, and the GC-marked stacks.
18
36
  pub(crate) struct PullState {
19
37
  pub(crate) bufs: Buffers,
20
38
  pub(crate) keys: AHashMap<Box<str>, rb_sys::VALUE>,
21
39
  /// Separate cache for symbolize_names mode: symbol and string VALUEs
22
40
  /// must never share a map.
23
41
  pub(crate) sym_keys: AHashMap<Box<str>, rb_sys::VALUE>,
24
- /// Leaked once per thread; kept alive + GC-marked via the wrapped
42
+ /// Leaked once per state; kept alive + GC-marked via the wrapped
25
43
  /// handle.
26
44
  pub(crate) vstack: Option<&'static mut VStackShadow>,
27
45
  /// Marked shadow holding the cached key VALUEs; keys are kept alive by
@@ -29,14 +47,36 @@ pub(crate) struct PullState {
29
47
  pub(crate) key_shadow: Option<&'static mut VStackShadow>,
30
48
  }
31
49
 
50
+ impl PullState {
51
+ #[cold]
52
+ fn fresh() -> Box<Self> {
53
+ Box::new(PullState {
54
+ bufs: Buffers::new(),
55
+ keys: AHashMap::with_capacity(256),
56
+ sym_keys: AHashMap::new(),
57
+ vstack: None,
58
+ key_shadow: None,
59
+ })
60
+ }
61
+ }
62
+
32
63
  thread_local! {
33
- pub(crate) static PULL_STATE: RefCell<PullState> = RefCell::new(PullState {
34
- bufs: Buffers::new(),
35
- keys: AHashMap::with_capacity(256),
36
- sym_keys: AHashMap::new(),
37
- vstack: None,
38
- key_shadow: None,
39
- });
64
+ static PULL_STATE: Cell<Option<Box<PullState>>> = const { Cell::new(None) };
65
+ }
66
+
67
+ /// Run `f` on this thread's parse state, taken out like [`with_taken`]
68
+ /// (a state lost to a longjmp leaks its shadows' last VALUEs; a nested
69
+ /// call would start a fresh one). Unlike the generate scratch, parse
70
+ /// bodies never run Ruby code, so the thread cannot hop native threads
71
+ /// mid-call and one thread-local access serves both the take and the
72
+ /// put-back: measured ~5ns per call on tiny documents against two.
73
+ pub(crate) fn with_pull_state<R>(f: impl FnOnce(&mut PullState) -> R) -> R {
74
+ PULL_STATE.with(|cell| {
75
+ let mut state = cell.take().unwrap_or_else(PullState::fresh);
76
+ let result = f(&mut state);
77
+ cell.set(Some(state));
78
+ result
79
+ })
40
80
  }
41
81
 
42
82
  /// GC-marked holder for pending VALUEs.
@@ -48,6 +88,12 @@ pub(crate) struct VStackShadow {
48
88
  /// through [`DataTypeFunctions::mark`] (its trampoline, not ours),
49
89
  /// pinning every pending VALUE with `rb_gc_mark` semantics. The class
50
90
  /// is defined (and made a private constant) at init.
91
+ ///
92
+ /// Deliberately NOT `wb_protected`: parses push VALUEs into the shadow
93
+ /// with plain stores, no write barriers, so an old protected handle
94
+ /// would let the GC miss young values it holds (a use-after-free).
95
+ /// Staying write-barrier-unprotected makes every GC rescan the handle,
96
+ /// which costs nothing measurable: there are one to three per thread.
51
97
  #[derive(TypedData)]
52
98
  #[magnus(class = "NOSJ::ValueStackShadow", mark)]
53
99
  pub(crate) struct ShadowHandle(*const VStackShadow);
@@ -74,7 +120,7 @@ impl DataTypeFunctions for ShadowHandle {
74
120
  }
75
121
  }
76
122
 
77
- /// Create (once per thread) a leaked, GC-marked VStackShadow.
123
+ /// Create (once per owning state) a leaked, GC-marked VStackShadow.
78
124
  pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>) {
79
125
  if slot.is_none() {
80
126
  let ruby = magnus::Ruby::get().expect("called on a Ruby thread");
@@ -83,7 +129,7 @@ pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>)
83
129
  }));
84
130
  let ptr = std::ptr::from_mut::<VStackShadow>(shadow).cast_const();
85
131
  let handle: Obj<ShadowHandle> = ruby.obj_wrap(ShadowHandle(ptr));
86
- magnus::gc::register_mark_object(handle);
132
+ ruby.gc_register_mark_object(handle);
87
133
  *slot = Some(shadow);
88
134
  }
89
135
  }
@@ -12,7 +12,7 @@ use crate::errors::{nesting_error, parser_error, parser_error_at};
12
12
  use crate::files::with_mapped_file;
13
13
  use crate::parse::{parse_native_opts, utf8_input};
14
14
  use crate::sink::SinkAbort;
15
- use crate::state::PULL_STATE;
15
+ use crate::state::with_pull_state;
16
16
 
17
17
  /// What the document's root value was; reported as a Symbol. The
18
18
  /// Default is never observable (a successful pass always saw a root
@@ -256,8 +256,7 @@ fn stats_over(ruby: &Ruby, input: &[u8], opts: Value) -> Result<Value, Error> {
256
256
  },
257
257
  ..StatsSink::default()
258
258
  };
259
- let result = PULL_STATE.with(|cell| {
260
- let mut state = cell.borrow_mut();
259
+ let result = with_pull_state(|state| {
261
260
  // Safety: callers verified UTF-8 (coderange or a full scan).
262
261
  unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
263
262
  });
data/lib/nosj/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NOSJ
4
4
  # The gem version.
5
- VERSION = "0.4.0"
5
+ VERSION = "0.4.1"
6
6
  end
data/lib/nosj.rb CHANGED
@@ -205,7 +205,8 @@ module NOSJ
205
205
 
206
206
  # Partial parsing by JSON Pointer (with the standard +~0+/+~1+
207
207
  # escapes). The matched subtree materializes under the same options
208
- # as {.parse}.
208
+ # as {.parse}; +allow_nan+ and +allow_trailing_comma+ also govern the
209
+ # walk to it.
209
210
  #
210
211
  # @example
211
212
  # NOSJ.at_pointer(json, "/users/3/name") #=> "grace" or nil
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.4.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Yaroslav Markin
@@ -104,7 +104,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
104
104
  - !ruby/object:Gem::Version
105
105
  version: 3.3.11
106
106
  requirements: []
107
- rubygems_version: 4.0.16
107
+ rubygems_version: 4.0.20
108
108
  specification_version: 4
109
109
  summary: An extremely fast JSON parser and generator for Ruby, written in Rust and
110
110
  SIMD-accelerated on every platform.