nosj 0.2.0 → 0.3.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 +88 -0
- data/Cargo.lock +3 -3
- data/README.md +215 -19
- data/ext/nosj/Cargo.toml +2 -2
- data/ext/nosj/src/errors.rs +171 -0
- data/ext/nosj/src/files.rs +29 -7
- data/ext/nosj/src/gen/errors.rs +2 -9
- data/ext/nosj/src/gen/mod.rs +156 -12
- data/ext/nosj/src/gen/opts.rs +87 -9
- data/ext/nosj/src/gen/ruby.rs +51 -0
- data/ext/nosj/src/gen/walker.rs +97 -59
- data/ext/nosj/src/lazy.rs +42 -22
- data/ext/nosj/src/lib.rs +30 -0
- data/ext/nosj/src/lines.rs +90 -0
- data/ext/nosj/src/parse.rs +50 -14
- data/ext/nosj/src/patch.rs +547 -0
- data/ext/nosj/src/pointer.rs +12 -5
- data/ext/nosj/src/reformat.rs +301 -0
- data/ext/nosj/src/sink.rs +6 -0
- data/ext/nosj/src/stats.rs +301 -0
- data/lib/nosj/json.rb +30 -6
- data/lib/nosj/multi_json.rb +1 -1
- data/lib/nosj/rails.rb +76 -0
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +351 -5
- data/sig/nosj.rbs +66 -0
- metadata +10 -2
data/ext/nosj/src/gen/walker.rs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//! Compact and pretty modes are one const-generic body, so the compact
|
|
4
4
|
//! hot path carries no formatting branches.
|
|
5
5
|
|
|
6
|
-
use nosj::emit::{self, EscapeMode};
|
|
6
|
+
use nosj::emit::{self, copy_short_raw, EscapeMode};
|
|
7
7
|
use rb_sys::macros::{
|
|
8
8
|
FIX2LONG, FIXNUM_P, FLONUM_P, RARRAY_CONST_PTR, RARRAY_LEN, RB_BUILTIN_TYPE, RHASH_SIZE,
|
|
9
9
|
STATIC_SYM_P,
|
|
@@ -14,52 +14,16 @@ use super::errors::GenFail;
|
|
|
14
14
|
use super::keys::GenKeyCache;
|
|
15
15
|
use super::opts::GenConfig;
|
|
16
16
|
use super::ruby::{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
QTRUE,
|
|
17
|
+
is_json_fragment, is_special_const, protected_as_json, protected_encode_utf8,
|
|
18
|
+
protected_to_json, protected_to_s, rstring_bytes, str_coderange, str_enc_index, to_json_id,
|
|
19
|
+
utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/// rare enough for the memcpy fallback.
|
|
28
|
-
///
|
|
29
|
-
/// # Safety
|
|
30
|
-
///
|
|
31
|
-
/// `n` readable bytes at `src`, `n` writable bytes at `dst`, and the
|
|
32
|
-
/// ranges must not overlap.
|
|
33
|
-
#[inline(always)]
|
|
34
|
-
unsafe fn copy_short_raw(src: *const u8, dst: *mut u8, n: usize) {
|
|
35
|
-
/// One overlapping pair: word-size chunks at offset 0 and at
|
|
36
|
-
/// `n - size` cover `size..=2*size` bytes; the overlapped middle
|
|
37
|
-
/// is written twice with identical data.
|
|
38
|
-
macro_rules! word_pair {
|
|
39
|
-
($t:ty) => {{
|
|
40
|
-
const SIZE: usize = size_of::<$t>();
|
|
41
|
-
dst.cast::<$t>()
|
|
42
|
-
.write_unaligned(src.cast::<$t>().read_unaligned());
|
|
43
|
-
dst.add(n - SIZE)
|
|
44
|
-
.cast::<$t>()
|
|
45
|
-
.write_unaligned(src.add(n - SIZE).cast::<$t>().read_unaligned());
|
|
46
|
-
}};
|
|
47
|
-
}
|
|
48
|
-
if n >= 16 {
|
|
49
|
-
if n <= 32 {
|
|
50
|
-
word_pair!(u128);
|
|
51
|
-
} else {
|
|
52
|
-
std::ptr::copy_nonoverlapping(src, dst, n);
|
|
53
|
-
}
|
|
54
|
-
} else if n >= 8 {
|
|
55
|
-
word_pair!(u64);
|
|
56
|
-
} else if n >= 4 {
|
|
57
|
-
word_pair!(u32);
|
|
58
|
-
} else if n >= 2 {
|
|
59
|
-
word_pair!(u16);
|
|
60
|
-
} else if n == 1 {
|
|
61
|
-
*dst = *src;
|
|
62
|
-
}
|
|
22
|
+
/// Whether keys escaped under `mode` may be cached: the cached bytes
|
|
23
|
+
/// bake in the escape mode, so the scratch keeps one cache per
|
|
24
|
+
/// cacheable mode and hands `Gen` the matching one.
|
|
25
|
+
pub(super) fn mode_cacheable(mode: EscapeMode) -> bool {
|
|
26
|
+
matches!(mode, EscapeMode::Standard | EscapeMode::HtmlSafe)
|
|
63
27
|
}
|
|
64
28
|
|
|
65
29
|
pub(super) struct Gen<'a> {
|
|
@@ -69,6 +33,7 @@ pub(super) struct Gen<'a> {
|
|
|
69
33
|
pub(super) fail: Option<GenFail>,
|
|
70
34
|
/// Pre-escaped key cache, borrowed for the whole document (one
|
|
71
35
|
/// thread-local borrow per generate call instead of one per key).
|
|
36
|
+
/// Always the cache matching `cfg.mode` (see [`mode_cacheable`]).
|
|
72
37
|
pub(super) keys: &'a mut GenKeyCache,
|
|
73
38
|
}
|
|
74
39
|
|
|
@@ -149,6 +114,11 @@ impl Gen<'_> {
|
|
|
149
114
|
emit::write_f64(&mut *self.out, f);
|
|
150
115
|
return Ok(());
|
|
151
116
|
}
|
|
117
|
+
if self.cfg.rails {
|
|
118
|
+
// ActiveSupport's Float#as_json: non-finite floats are null.
|
|
119
|
+
self.out.extend_from_slice(b"null");
|
|
120
|
+
return Ok(());
|
|
121
|
+
}
|
|
152
122
|
let name = if f.is_nan() {
|
|
153
123
|
"NaN"
|
|
154
124
|
} else if f > 0.0 {
|
|
@@ -166,13 +136,14 @@ impl Gen<'_> {
|
|
|
166
136
|
}
|
|
167
137
|
|
|
168
138
|
/// Emit an object key from the pre-escaped cache. Only frozen string
|
|
169
|
-
/// keys in
|
|
170
|
-
/// content behind the VALUE can't change, and the cached bytes bake
|
|
171
|
-
/// the escape mode
|
|
139
|
+
/// keys in a cacheable escape mode qualify: frozen guarantees the
|
|
140
|
+
/// content behind the VALUE can't change, and the cached bytes bake
|
|
141
|
+
/// in the escape mode, so each cacheable mode gets its own cache
|
|
142
|
+
/// instance from the scratch (see [`mode_cacheable`]).
|
|
172
143
|
fn emit_key_cached(&mut self, k: VALUE) -> Result<(), ()> {
|
|
173
144
|
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
174
145
|
let frozen = unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0;
|
|
175
|
-
if !frozen || self.cfg.mode
|
|
146
|
+
if !frozen || !mode_cacheable(self.cfg.mode) {
|
|
176
147
|
return self.emit_rstring_quoted(k);
|
|
177
148
|
}
|
|
178
149
|
if let Some(bytes) = self.keys.get(k) {
|
|
@@ -190,13 +161,13 @@ impl Gen<'_> {
|
|
|
190
161
|
}
|
|
191
162
|
|
|
192
163
|
/// The pre-escaped bytes for `k` when the cache may serve it:
|
|
193
|
-
/// frozen string key,
|
|
194
|
-
///
|
|
195
|
-
///
|
|
164
|
+
/// frozen string key, cacheable escape mode (see
|
|
165
|
+
/// [`Gen::emit_key_cached`]). An associated fn over the split-out
|
|
166
|
+
/// fields so callers keep `self.out` free.
|
|
196
167
|
#[inline(always)]
|
|
197
168
|
fn cached_key_bytes<'k>(cfg: &GenConfig, keys: &'k GenKeyCache, k: VALUE) -> Option<&'k [u8]> {
|
|
198
169
|
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
199
|
-
if cfg.mode
|
|
170
|
+
if mode_cacheable(cfg.mode)
|
|
200
171
|
&& !is_special_const(k)
|
|
201
172
|
&& unsafe { RB_BUILTIN_TYPE(k) } == ruby_value_type::RUBY_T_STRING
|
|
202
173
|
&& unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0
|
|
@@ -303,11 +274,20 @@ impl Gen<'_> {
|
|
|
303
274
|
}
|
|
304
275
|
}
|
|
305
276
|
|
|
306
|
-
/// Non-native type: strict raises
|
|
307
|
-
///
|
|
308
|
-
///
|
|
309
|
-
|
|
277
|
+
/// Non-native type: strict raises (except `JSON::Fragment`, which
|
|
278
|
+
/// the gem splices even under strict); otherwise `to_json` if the
|
|
279
|
+
/// object responds (result appended verbatim), else `to_s` as a
|
|
280
|
+
/// JSON string, which is exactly what the gem's `Object#to_json`
|
|
281
|
+
/// does. Rails mode recurses through `as_json` instead
|
|
282
|
+
/// (JSONGemEncoder#jsonify).
|
|
283
|
+
fn emit_fallback<const PRETTY: bool>(&mut self, raw: VALUE, depth: usize) -> Result<(), ()> {
|
|
284
|
+
if self.cfg.rails {
|
|
285
|
+
return self.emit_rails_fallback::<PRETTY>(raw, depth);
|
|
286
|
+
}
|
|
310
287
|
if self.cfg.strict {
|
|
288
|
+
if is_json_fragment(raw) {
|
|
289
|
+
return self.splice_to_json(raw);
|
|
290
|
+
}
|
|
311
291
|
let name = unsafe {
|
|
312
292
|
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
313
293
|
.to_string_lossy()
|
|
@@ -341,6 +321,64 @@ impl Gen<'_> {
|
|
|
341
321
|
}
|
|
342
322
|
}
|
|
343
323
|
|
|
324
|
+
/// Splice `raw`'s `to_json` result verbatim: the JSON::Fragment
|
|
325
|
+
/// path (pre-rendered JSON, trusted like the gem trusts it).
|
|
326
|
+
fn splice_to_json(&mut self, raw: VALUE) -> Result<(), ()> {
|
|
327
|
+
match protected_to_json(raw) {
|
|
328
|
+
Ok(json)
|
|
329
|
+
if !is_special_const(json)
|
|
330
|
+
&& unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING =>
|
|
331
|
+
{
|
|
332
|
+
self.append_rstring_raw(json);
|
|
333
|
+
Ok(())
|
|
334
|
+
}
|
|
335
|
+
Ok(_) => {
|
|
336
|
+
self.fail = Some(GenFail::Generator(
|
|
337
|
+
"JSON::Fragment#to_json did not return a String".to_string(),
|
|
338
|
+
));
|
|
339
|
+
Err(())
|
|
340
|
+
}
|
|
341
|
+
Err(exc) => {
|
|
342
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
343
|
+
Err(())
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/// Rails-mode fallback, mirroring JSONGemEncoder#jsonify:
|
|
349
|
+
/// fragments splice through (like ActiveSupport passes them to the
|
|
350
|
+
/// gem); everything else is asked for its as_json representation
|
|
351
|
+
/// (no arguments; only the top-level value receives the encoder
|
|
352
|
+
/// options), which is emitted in its place. An as_json returning
|
|
353
|
+
/// the receiver would recurse forever, so it raises instead.
|
|
354
|
+
fn emit_rails_fallback<const PRETTY: bool>(
|
|
355
|
+
&mut self,
|
|
356
|
+
raw: VALUE,
|
|
357
|
+
depth: usize,
|
|
358
|
+
) -> Result<(), ()> {
|
|
359
|
+
if is_json_fragment(raw) {
|
|
360
|
+
return self.splice_to_json(raw);
|
|
361
|
+
}
|
|
362
|
+
match protected_as_json(raw) {
|
|
363
|
+
Ok(json) if json == raw => {
|
|
364
|
+
let name = unsafe {
|
|
365
|
+
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
366
|
+
.to_string_lossy()
|
|
367
|
+
.into_owned()
|
|
368
|
+
};
|
|
369
|
+
self.fail = Some(GenFail::Generator(format!(
|
|
370
|
+
"{name}#as_json returned the receiver"
|
|
371
|
+
)));
|
|
372
|
+
Err(())
|
|
373
|
+
}
|
|
374
|
+
Ok(json) => self.emit_value::<PRETTY>(json, depth),
|
|
375
|
+
Err(exc) => {
|
|
376
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
377
|
+
Err(())
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
344
382
|
fn nesting_check(&mut self, inner: usize) -> Result<(), ()> {
|
|
345
383
|
if self.cfg.max_nesting > 0 && inner > self.cfg.max_nesting {
|
|
346
384
|
self.fail = Some(GenFail::Nesting(self.cfg.max_nesting));
|
|
@@ -601,7 +639,7 @@ impl Gen<'_> {
|
|
|
601
639
|
let s = unsafe { rb_sys::rb_sym2str(raw) };
|
|
602
640
|
self.emit_rstring_quoted(s)
|
|
603
641
|
}
|
|
604
|
-
_ => self.emit_fallback(raw),
|
|
642
|
+
_ => self.emit_fallback::<PRETTY>(raw, depth),
|
|
605
643
|
};
|
|
606
644
|
}
|
|
607
645
|
if FIXNUM_P(raw) {
|
|
@@ -633,6 +671,6 @@ impl Gen<'_> {
|
|
|
633
671
|
let s = unsafe { rb_sys::rb_sym2str(raw) };
|
|
634
672
|
return self.emit_rstring_quoted(s);
|
|
635
673
|
}
|
|
636
|
-
self.emit_fallback(raw)
|
|
674
|
+
self.emit_fallback::<PRETTY>(raw, depth)
|
|
637
675
|
}
|
|
638
676
|
}
|
data/ext/nosj/src/lazy.rs
CHANGED
|
@@ -17,7 +17,8 @@ use magnus::typed_data::Obj;
|
|
|
17
17
|
use magnus::value::ReprValue;
|
|
18
18
|
use magnus::{DataTypeFunctions, Error, RArray, RString, Ruby, TypedData, Value};
|
|
19
19
|
|
|
20
|
-
use crate::
|
|
20
|
+
use crate::errors::parser_error_at;
|
|
21
|
+
use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
|
|
21
22
|
use crate::pointer::{path_to_pointer, push_escaped_token};
|
|
22
23
|
use crate::state::PULL_STATE;
|
|
23
24
|
|
|
@@ -105,20 +106,19 @@ impl LazyNode {
|
|
|
105
106
|
/// Wrap a resolved raw-value slice: containers become new lazy nodes,
|
|
106
107
|
/// scalars materialize now. `sub` must be a subslice of `doc.bytes`.
|
|
107
108
|
fn resolved_to_value(ruby: &Ruby, doc: &Arc<DocInner>, sub: &[u8]) -> Result<Value, Error> {
|
|
109
|
+
let (start, end) = span_of(doc.bytes(), sub);
|
|
108
110
|
match sub.first().copied() {
|
|
109
111
|
Some(k) if k == KIND_OBJECT || k == KIND_ARRAY => {
|
|
110
|
-
let base = doc.bytes().as_ptr() as usize;
|
|
111
|
-
let start = sub.as_ptr() as usize - base;
|
|
112
112
|
let node = LazyNode {
|
|
113
113
|
doc: Arc::clone(doc),
|
|
114
114
|
start,
|
|
115
|
-
end
|
|
115
|
+
end,
|
|
116
116
|
kind: k,
|
|
117
117
|
};
|
|
118
118
|
let obj: Obj<LazyNode> = ruby.obj_wrap(node);
|
|
119
119
|
Ok(obj.as_value())
|
|
120
120
|
}
|
|
121
|
-
_ =>
|
|
121
|
+
_ => materialize_at(ruby, doc.bytes(), start, end, &doc.opts),
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
124
|
|
|
@@ -139,7 +139,7 @@ fn resolve_in_span(ruby: &Ruby, node: &LazyNode, pointer: &str) -> Result<Value,
|
|
|
139
139
|
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
140
140
|
Err(Error::new(ruby.exception_arg_error(), e.to_string()))
|
|
141
141
|
}
|
|
142
|
-
Err(e) => Err(
|
|
142
|
+
Err(e) => Err(reader_err(ruby, node, e)),
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
145
|
|
|
@@ -188,7 +188,12 @@ pub(crate) fn wrap_root(
|
|
|
188
188
|
let doc = Arc::new(DocInner { bytes, opts });
|
|
189
189
|
let input = doc.bytes();
|
|
190
190
|
let Some(start) = input.iter().position(|b| !WS.contains(b)) else {
|
|
191
|
-
return Err(
|
|
191
|
+
return Err(parser_error_at(
|
|
192
|
+
ruby,
|
|
193
|
+
input,
|
|
194
|
+
input.len(),
|
|
195
|
+
"unexpected end of input".into(),
|
|
196
|
+
));
|
|
192
197
|
};
|
|
193
198
|
let end = input.iter().rposition(|b| !WS.contains(b)).unwrap() + 1;
|
|
194
199
|
let sub = &doc.bytes()[start..end];
|
|
@@ -245,7 +250,13 @@ pub fn lazy_at_pointer(
|
|
|
245
250
|
/// `__materialize`: the whole span as plain Ruby values, under the
|
|
246
251
|
/// document's parse options.
|
|
247
252
|
pub fn lazy_materialize(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<Value, Error> {
|
|
248
|
-
|
|
253
|
+
materialize_at(
|
|
254
|
+
ruby,
|
|
255
|
+
rb_self.doc.bytes(),
|
|
256
|
+
rb_self.start,
|
|
257
|
+
rb_self.end,
|
|
258
|
+
&rb_self.doc.opts,
|
|
259
|
+
)
|
|
249
260
|
}
|
|
250
261
|
|
|
251
262
|
/// `__kind`: `:object` or `:array`.
|
|
@@ -261,8 +272,11 @@ pub fn lazy_byte_size(_ruby: &Ruby, rb_self: Obj<LazyNode>) -> usize {
|
|
|
261
272
|
rb_self.end - rb_self.start
|
|
262
273
|
}
|
|
263
274
|
|
|
264
|
-
|
|
265
|
-
|
|
275
|
+
/// A walk failure inside `node`'s span: the reader's offset is
|
|
276
|
+
/// span-relative, so shift by the node's start to report an absolute
|
|
277
|
+
/// document position.
|
|
278
|
+
fn reader_err(ruby: &Ruby, node: &LazyNode, e: nosj::ParseError) -> Error {
|
|
279
|
+
parser_error_at(ruby, node.doc.bytes(), node.start + e.offset, e.to_string())
|
|
266
280
|
}
|
|
267
281
|
|
|
268
282
|
/// `__keys`: the object's decoded keys, one Reader walk, values skipped.
|
|
@@ -278,8 +292,11 @@ pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
|
278
292
|
let mut state = cell.borrow_mut();
|
|
279
293
|
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
280
294
|
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
281
|
-
r.next_node().map_err(|e| reader_err(ruby, e))?;
|
|
282
|
-
let mut has = match r
|
|
295
|
+
r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
296
|
+
let mut has = match r
|
|
297
|
+
.object_first_key()
|
|
298
|
+
.map_err(|e| reader_err(ruby, &rb_self, e))?
|
|
299
|
+
{
|
|
283
300
|
Some(k) => {
|
|
284
301
|
out.push(ruby.str_new(k))?;
|
|
285
302
|
true
|
|
@@ -287,8 +304,11 @@ pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
|
287
304
|
None => false,
|
|
288
305
|
};
|
|
289
306
|
while has {
|
|
290
|
-
r.skip_value().map_err(|e| reader_err(ruby, e))?;
|
|
291
|
-
has = match r
|
|
307
|
+
r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
308
|
+
has = match r
|
|
309
|
+
.object_next_key()
|
|
310
|
+
.map_err(|e| reader_err(ruby, &rb_self, e))?
|
|
311
|
+
{
|
|
292
312
|
Some(k) => {
|
|
293
313
|
out.push(ruby.str_new(k))?;
|
|
294
314
|
true
|
|
@@ -308,27 +328,27 @@ pub fn lazy_size(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<usize, Error> {
|
|
|
308
328
|
let mut state = cell.borrow_mut();
|
|
309
329
|
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
310
330
|
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
311
|
-
r.next_node().map_err(|e| reader_err(ruby, e))?;
|
|
331
|
+
r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
312
332
|
let mut n = 0usize;
|
|
313
333
|
if rb_self.kind == KIND_OBJECT {
|
|
314
334
|
let mut has = r
|
|
315
335
|
.object_first_key()
|
|
316
|
-
.map_err(|e| reader_err(ruby, e))?
|
|
336
|
+
.map_err(|e| reader_err(ruby, &rb_self, e))?
|
|
317
337
|
.is_some();
|
|
318
338
|
while has {
|
|
319
339
|
n += 1;
|
|
320
|
-
r.skip_value().map_err(|e| reader_err(ruby, e))?;
|
|
340
|
+
r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
321
341
|
has = r
|
|
322
342
|
.object_next_key()
|
|
323
|
-
.map_err(|e| reader_err(ruby, e))?
|
|
343
|
+
.map_err(|e| reader_err(ruby, &rb_self, e))?
|
|
324
344
|
.is_some();
|
|
325
345
|
}
|
|
326
346
|
} else {
|
|
327
|
-
let mut has = r.array_first().map_err(|e| reader_err(ruby, e))?;
|
|
347
|
+
let mut has = r.array_first().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
328
348
|
while has {
|
|
329
349
|
n += 1;
|
|
330
|
-
r.skip_value().map_err(|e| reader_err(ruby, e))?;
|
|
331
|
-
has = r.array_next().map_err(|e| reader_err(ruby, e))?;
|
|
350
|
+
r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
351
|
+
has = r.array_next().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
332
352
|
}
|
|
333
353
|
}
|
|
334
354
|
Ok(n)
|
|
@@ -383,7 +403,7 @@ pub fn lazy_children(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Erro
|
|
|
383
403
|
}
|
|
384
404
|
Ok(out)
|
|
385
405
|
});
|
|
386
|
-
let descs = descs.map_err(|e| reader_err(ruby, e))?;
|
|
406
|
+
let descs = descs.map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
387
407
|
|
|
388
408
|
let out = ruby.ary_new_capa(descs.len());
|
|
389
409
|
for d in descs {
|
data/ext/nosj/src/lib.rs
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
//! demand over shared document bytes).
|
|
10
10
|
//! - `files.rs`: file entry points (load_file, write_file, and the
|
|
11
11
|
//! mmap-backed load_lazy_file / at_pointer_file / dig_file).
|
|
12
|
+
//! - `stats.rs`: document statistics (NOSJ.stats / stats_file) from a
|
|
13
|
+
//! counting sink pass.
|
|
12
14
|
//! - `sink.rs`: the VALUE-building and validation sinks with their
|
|
13
15
|
//! interned-key caches.
|
|
14
16
|
//! - `state.rs`: per-thread reusable state and the GC-marked value
|
|
@@ -16,13 +18,18 @@
|
|
|
16
18
|
//! - `gen/`: generation (JSON.generate-compatible walker, options,
|
|
17
19
|
//! key cache, protect shims, error mapping).
|
|
18
20
|
|
|
21
|
+
pub mod errors;
|
|
19
22
|
pub mod files;
|
|
20
23
|
pub mod gen;
|
|
21
24
|
pub mod lazy;
|
|
25
|
+
pub mod lines;
|
|
22
26
|
pub mod parse;
|
|
27
|
+
pub mod patch;
|
|
23
28
|
pub mod pointer;
|
|
29
|
+
pub mod reformat;
|
|
24
30
|
pub mod sink;
|
|
25
31
|
pub mod state;
|
|
32
|
+
pub mod stats;
|
|
26
33
|
|
|
27
34
|
use magnus::{method, prelude::*, Error, Ruby};
|
|
28
35
|
|
|
@@ -57,6 +64,25 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
57
64
|
method!(files::at_pointer_file_native, 3),
|
|
58
65
|
)?;
|
|
59
66
|
module.define_singleton_method("dig_file_native", method!(files::dig_file_native, 2))?;
|
|
67
|
+
module.define_singleton_method("stats_native", method!(stats::stats_native, 2))?;
|
|
68
|
+
module.define_singleton_method("stats_file_native", method!(stats::stats_file_native, 2))?;
|
|
69
|
+
module.define_singleton_method("each_line_native", method!(lines::each_line_native, 2))?;
|
|
70
|
+
module.define_singleton_method(
|
|
71
|
+
"each_line_file_native",
|
|
72
|
+
method!(lines::each_line_file_native, 2),
|
|
73
|
+
)?;
|
|
74
|
+
module.define_singleton_method(
|
|
75
|
+
"generate_lines_native",
|
|
76
|
+
method!(gen::generate_lines_native, 2),
|
|
77
|
+
)?;
|
|
78
|
+
module.define_singleton_method("write_lines_native", method!(files::write_lines_native, 3))?;
|
|
79
|
+
module.define_singleton_method("splice_native", method!(patch::splice_native, 3))?;
|
|
80
|
+
module.define_singleton_method("patch_native", method!(patch::patch_native, 3))?;
|
|
81
|
+
module.define_singleton_method("reformat_native", method!(reformat::reformat_native, 2))?;
|
|
82
|
+
module.define_singleton_method(
|
|
83
|
+
"reformat_file_native",
|
|
84
|
+
method!(reformat::reformat_file_native, 2),
|
|
85
|
+
)?;
|
|
60
86
|
let lazy_class = module.define_class("Lazy", ruby.class_object())?;
|
|
61
87
|
// Nodes are only born from NOSJ.lazy / lazy resolution.
|
|
62
88
|
lazy_class.undef_default_alloc_func();
|
|
@@ -70,6 +96,10 @@ fn init(ruby: &Ruby) -> Result<(), Error> {
|
|
|
70
96
|
lazy_class.define_method("__size", method!(lazy::lazy_size, 0))?;
|
|
71
97
|
lazy_class.define_method("__children", method!(lazy::lazy_children, 0))?;
|
|
72
98
|
module.define_singleton_method("generate_native", method!(gen::generate_native, 2))?;
|
|
99
|
+
module.define_singleton_method(
|
|
100
|
+
"generate_rails_native",
|
|
101
|
+
method!(gen::generate_rails_native, 3),
|
|
102
|
+
)?;
|
|
73
103
|
// `generate` itself is native and variadic: the json gem routes
|
|
74
104
|
// its `generate` through a Ruby frame into C, so skipping our own
|
|
75
105
|
// forwarder frame is a straight per-call win on small documents.
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
//! NDJSON / JSON Lines: `NOSJ.each_line` walks a newline-delimited
|
|
2
|
+
//! document yielding one parsed value per line, and the file form does
|
|
3
|
+
//! it over a read-only memory map. A raw newline can never occur
|
|
4
|
+
//! INSIDE a JSON value (control characters must be escaped), so line
|
|
5
|
+
//! splitting is exact framing, and one-value-per-line is what the
|
|
6
|
+
//! format requires: a second value on a line fails as trailing
|
|
7
|
+
//! garbage, with an absolute document position on the error.
|
|
8
|
+
|
|
9
|
+
use magnus::value::ReprValue;
|
|
10
|
+
use magnus::{Error, RString, Ruby, Value};
|
|
11
|
+
|
|
12
|
+
use crate::files::with_mapped_file;
|
|
13
|
+
use crate::parse::{materialize_at, parse_native_opts, utf8_input, ParseNativeOpts};
|
|
14
|
+
|
|
15
|
+
/// Blank-line whitespace (the newline itself is the separator). Lines
|
|
16
|
+
/// holding only this are skipped, per the NDJSON convention that
|
|
17
|
+
/// parsers ignore empty lines (trailing newlines at EOF are universal).
|
|
18
|
+
const LINE_WS: [u8; 3] = *b" \t\r";
|
|
19
|
+
|
|
20
|
+
fn blank(line: &[u8]) -> bool {
|
|
21
|
+
line.iter().all(|b| LINE_WS.contains(b))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/// Yield one parsed value per non-blank line. Each line parses through
|
|
25
|
+
/// the shared sink machinery against the FULL source, so a malformed
|
|
26
|
+
/// line raises the rich ParserError whose `#line` is the physical
|
|
27
|
+
/// NDJSON line number. The thread state is borrowed per line, never
|
|
28
|
+
/// across a yield: the block is free to call back into NOSJ.
|
|
29
|
+
fn walk_lines(ruby: &Ruby, source: &[u8], o: &ParseNativeOpts) -> Result<(), Error> {
|
|
30
|
+
let mut pos = 0;
|
|
31
|
+
while pos < source.len() {
|
|
32
|
+
let line_end = source[pos..]
|
|
33
|
+
.iter()
|
|
34
|
+
.position(|&b| b == b'\n')
|
|
35
|
+
.map_or(source.len(), |p| pos + p);
|
|
36
|
+
if !blank(&source[pos..line_end]) {
|
|
37
|
+
let value = materialize_at(ruby, source, pos, line_end, o)?;
|
|
38
|
+
let _: Value = ruby.yield_value(value)?;
|
|
39
|
+
}
|
|
40
|
+
pos = line_end + 1;
|
|
41
|
+
}
|
|
42
|
+
Ok(())
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// `NOSJ.each_line(source, opts) { |value| }`: the Ruby wrapper
|
|
46
|
+
/// guarantees a block (and builds the Enumerator otherwise).
|
|
47
|
+
pub fn each_line_native(
|
|
48
|
+
ruby: &Ruby,
|
|
49
|
+
_rb_self: Value,
|
|
50
|
+
data: RString,
|
|
51
|
+
opts: Value,
|
|
52
|
+
) -> Result<Value, Error> {
|
|
53
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
54
|
+
let input = utf8_input(ruby, &data)?;
|
|
55
|
+
if data.as_value().is_frozen() {
|
|
56
|
+
// A frozen source cannot be mutated by the block, and `data`
|
|
57
|
+
// lives on this frame, so the borrow stays valid across yields.
|
|
58
|
+
walk_lines(ruby, input, &o)?;
|
|
59
|
+
} else {
|
|
60
|
+
// The block could mutate (or free the buffer of) an unfrozen
|
|
61
|
+
// source mid-iteration; walk a private copy. Same policy as
|
|
62
|
+
// NOSJ.lazy: pass a frozen string for zero-copy.
|
|
63
|
+
let owned = input.to_vec();
|
|
64
|
+
walk_lines(ruby, &owned, &o)?;
|
|
65
|
+
}
|
|
66
|
+
Ok(ruby.qnil().as_value())
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// `NOSJ.each_line_file(path, opts) { |value| }`: NDJSON over a
|
|
70
|
+
/// read-only memory map; the file never becomes a Ruby String.
|
|
71
|
+
pub fn each_line_file_native(
|
|
72
|
+
ruby: &Ruby,
|
|
73
|
+
_rb_self: Value,
|
|
74
|
+
path: RString,
|
|
75
|
+
opts: Value,
|
|
76
|
+
) -> Result<Value, Error> {
|
|
77
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
78
|
+
let p = path.to_string()?;
|
|
79
|
+
// An empty file is a valid, zero-event NDJSON stream, but mapping
|
|
80
|
+
// a zero-length file fails with EINVAL on Linux; answer before the
|
|
81
|
+
// mapper. Metadata failures (ENOENT, ...) fall through so the
|
|
82
|
+
// mapper raises the same Errno an open would.
|
|
83
|
+
if std::fs::metadata(&p).is_ok_and(|m| m.len() == 0) {
|
|
84
|
+
return Ok(ruby.qnil().as_value());
|
|
85
|
+
}
|
|
86
|
+
with_mapped_file(ruby, &p, |map| {
|
|
87
|
+
walk_lines(ruby, &map, &o)?;
|
|
88
|
+
Ok(ruby.qnil().as_value())
|
|
89
|
+
})
|
|
90
|
+
}
|
data/ext/nosj/src/parse.rs
CHANGED
|
@@ -6,12 +6,18 @@
|
|
|
6
6
|
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
7
7
|
use magnus::{Error, RString, Ruby, Value};
|
|
8
8
|
|
|
9
|
+
use crate::errors::{nesting_error, parser_error, parser_error_at};
|
|
9
10
|
use crate::sink::{NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
|
|
10
11
|
use crate::state::{ensure_marked_shadow, PullState, PULL_STATE};
|
|
11
12
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
pub(crate) use crate::errors::parser_error as err;
|
|
14
|
+
|
|
15
|
+
/// Byte range of `sub` within `source`. `sub` must be a subslice of
|
|
16
|
+
/// `source` (it always is here: pointer resolution and lazy spans hand
|
|
17
|
+
/// out slices borrowed from the document they resolved against).
|
|
18
|
+
pub(crate) fn span_of(source: &[u8], sub: &[u8]) -> (usize, usize) {
|
|
19
|
+
let start = sub.as_ptr() as usize - source.as_ptr() as usize;
|
|
20
|
+
(start, start + sub.len())
|
|
15
21
|
}
|
|
16
22
|
|
|
17
23
|
/// Validate that `data` is UTF-8 (or US-ASCII) with intact coderange and
|
|
@@ -107,12 +113,16 @@ pub(crate) fn parse_native_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeO
|
|
|
107
113
|
}
|
|
108
114
|
|
|
109
115
|
/// Pop the root value off the sink stack, or map a drive failure onto
|
|
110
|
-
/// the gem's exceptions. Shared by every driver.
|
|
116
|
+
/// the gem's exceptions. Shared by every driver. `source`/`base` locate
|
|
117
|
+
/// the driven bytes within the full document, so ParserError positions
|
|
118
|
+
/// stay absolute when a subtree slice was parsed.
|
|
111
119
|
fn finish_drive(
|
|
112
120
|
ruby: &Ruby,
|
|
113
121
|
result: Result<(), nosj::DriveError<SinkAbort>>,
|
|
114
122
|
stack: &mut Vec<rb_sys::VALUE>,
|
|
115
123
|
max_nesting: usize,
|
|
124
|
+
source: &[u8],
|
|
125
|
+
base: usize,
|
|
116
126
|
) -> Result<Value, Error> {
|
|
117
127
|
match result {
|
|
118
128
|
Ok(()) => {
|
|
@@ -122,23 +132,47 @@ fn finish_drive(
|
|
|
122
132
|
Ok(unsafe { Value::from_raw(raw) })
|
|
123
133
|
}
|
|
124
134
|
Err(nosj::DriveError::Sink(SinkAbort::Overflow)) => {
|
|
125
|
-
Err(
|
|
135
|
+
Err(parser_error(ruby, "document too large".into()))
|
|
126
136
|
}
|
|
127
137
|
Err(nosj::DriveError::Sink(SinkAbort::BadBigint)) => {
|
|
128
|
-
Err(
|
|
138
|
+
Err(parser_error(ruby, "invalid bignum".into()))
|
|
129
139
|
}
|
|
130
|
-
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(
|
|
140
|
+
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
|
|
131
141
|
ruby,
|
|
132
142
|
format!("nesting of {} is too deep", max_nesting.saturating_add(1)),
|
|
133
143
|
)),
|
|
134
|
-
|
|
144
|
+
// Raised only by the reformat pipe's sink, which never drives
|
|
145
|
+
// through here; the match must stay total.
|
|
146
|
+
Err(nosj::DriveError::Sink(SinkAbort::BrokenUtf8Output)) => Err(parser_error(
|
|
147
|
+
ruby,
|
|
148
|
+
"source sequence is illegal/malformed utf-8".into(),
|
|
149
|
+
)),
|
|
150
|
+
Err(nosj::DriveError::Parse(e)) => Err(parser_error_at(
|
|
151
|
+
ruby,
|
|
152
|
+
source,
|
|
153
|
+
base + e.offset,
|
|
154
|
+
e.to_string(),
|
|
155
|
+
)),
|
|
135
156
|
}
|
|
136
157
|
}
|
|
137
158
|
|
|
138
|
-
/// Drive the fused cursor over
|
|
139
|
-
///
|
|
140
|
-
|
|
141
|
-
|
|
159
|
+
/// Drive the fused cursor over the whole of `source`. See
|
|
160
|
+
/// [`materialize_at`].
|
|
161
|
+
pub(crate) fn materialize(ruby: &Ruby, source: &[u8], o: &ParseNativeOpts) -> Result<Value, Error> {
|
|
162
|
+
materialize_at(ruby, source, 0, source.len(), o)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// Drive the fused cursor over `source[start..end]`, building Ruby
|
|
166
|
+
/// values through the shared thread-local sink machinery. `source` must
|
|
167
|
+
/// be valid UTF-8 (see [`utf8_input`]); the full document is passed so
|
|
168
|
+
/// error positions come out absolute.
|
|
169
|
+
pub(crate) fn materialize_at(
|
|
170
|
+
ruby: &Ruby,
|
|
171
|
+
source: &[u8],
|
|
172
|
+
start: usize,
|
|
173
|
+
end: usize,
|
|
174
|
+
o: &ParseNativeOpts,
|
|
175
|
+
) -> Result<Value, Error> {
|
|
142
176
|
PULL_STATE.with(|cell| {
|
|
143
177
|
let mut state = cell.borrow_mut();
|
|
144
178
|
ensure_marked_shadow(&mut state.vstack);
|
|
@@ -166,8 +200,10 @@ pub(crate) fn materialize(ruby: &Ruby, input: &[u8], o: &ParseNativeOpts) -> Res
|
|
|
166
200
|
};
|
|
167
201
|
|
|
168
202
|
// Safety: callers verified UTF-8 (coderange or nosj slice).
|
|
169
|
-
let result = unsafe {
|
|
170
|
-
|
|
203
|
+
let result = unsafe {
|
|
204
|
+
nosj::parse_utf8_unchecked_with(&source[start..end], bufs, &mut sink, o.popts)
|
|
205
|
+
};
|
|
206
|
+
finish_drive(ruby, result, sink.stack, o.max_nesting, source, start)
|
|
171
207
|
})
|
|
172
208
|
}
|
|
173
209
|
|