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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +44 -0
- data/Cargo.lock +17 -17
- data/README.md +2 -1
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- 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/ruby.rs +45 -22
- data/ext/nosj/src/gen/walker.rs +56 -73
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/parse.rs +10 -6
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +39 -25
- data/ext/nosj/src/sink.rs +13 -16
- data/ext/nosj/src/state.rs +60 -14
- data/ext/nosj/src/stats.rs +2 -3
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +2 -1
- metadata +2 -2
data/ext/nosj/src/pointer.rs
CHANGED
|
@@ -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::
|
|
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
|
|
23
|
-
//
|
|
24
|
-
let resolved =
|
|
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::
|
|
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
|
|
129
|
-
// `input`, not the
|
|
130
|
-
let resolved =
|
|
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::
|
|
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(),
|
data/ext/nosj/src/reformat.rs
CHANGED
|
@@ -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::
|
|
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::
|
|
29
|
+
use crate::state::{with_pull_state, with_taken};
|
|
30
30
|
|
|
31
31
|
thread_local! {
|
|
32
|
-
/// Pooled output buffer: capacity survives across calls
|
|
33
|
-
///
|
|
34
|
-
|
|
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:
|
|
235
|
-
let po =
|
|
236
|
-
let
|
|
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
|
|
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(
|
|
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 =
|
|
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(
|
|
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
|
-
/// `
|
|
39
|
-
///
|
|
40
|
-
/// the
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
// The
|
|
46
|
-
//
|
|
47
|
-
#[allow(clippy::
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
|
data/ext/nosj/src/state.rs
CHANGED
|
@@ -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::
|
|
13
|
+
use std::cell::Cell;
|
|
14
|
+
use std::thread::LocalKey;
|
|
14
15
|
|
|
15
|
-
///
|
|
16
|
-
///
|
|
17
|
-
///
|
|
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
|
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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
|
-
|
|
132
|
+
ruby.gc_register_mark_object(handle);
|
|
87
133
|
*slot = Some(shadow);
|
|
88
134
|
}
|
|
89
135
|
}
|
data/ext/nosj/src/stats.rs
CHANGED
|
@@ -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::
|
|
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 =
|
|
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
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.
|
|
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.
|
|
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.
|