nosj 0.1.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 +110 -1
- data/Cargo.lock +13 -3
- data/README.md +272 -24
- data/ext/nosj/Cargo.toml +5 -2
- data/ext/nosj/src/errors.rs +171 -0
- data/ext/nosj/src/files.rs +240 -0
- data/ext/nosj/src/gen/errors.rs +2 -9
- data/ext/nosj/src/gen/mod.rs +198 -20
- 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 +422 -0
- data/ext/nosj/src/lib.rs +60 -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 +26 -17
- 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/lazy.rb +165 -0
- 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 +434 -5
- data/sig/nosj.rbs +120 -0
- metadata +16 -3
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
|
}
|
|
@@ -0,0 +1,422 @@
|
|
|
1
|
+
//! Lazy documents: `NOSJ.lazy` wraps a JSON document and resolves
|
|
2
|
+
//! access on demand through the crate's pointer skipper. Every node is
|
|
3
|
+
//! a byte span into a shared, immutable copy of the document; container
|
|
4
|
+
//! children come back as further lazy nodes, scalars materialize
|
|
5
|
+
//! immediately through the same sink machinery as a full parse. Nothing
|
|
6
|
+
//! outside the touched path is ever parsed, so pulling a few fields out
|
|
7
|
+
//! of a large document costs microseconds, not a full parse.
|
|
8
|
+
//!
|
|
9
|
+
//! Validation is as-you-go (the crate's skipper checks bracket balance
|
|
10
|
+
//! over skipped content and fully validates resolved targets), so a
|
|
11
|
+
//! malformed region raises when an access first walks it, not at
|
|
12
|
+
//! `NOSJ.lazy` time.
|
|
13
|
+
|
|
14
|
+
use std::sync::Arc;
|
|
15
|
+
|
|
16
|
+
use magnus::typed_data::Obj;
|
|
17
|
+
use magnus::value::ReprValue;
|
|
18
|
+
use magnus::{DataTypeFunctions, Error, RArray, RString, Ruby, TypedData, Value};
|
|
19
|
+
|
|
20
|
+
use crate::errors::parser_error_at;
|
|
21
|
+
use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
|
|
22
|
+
use crate::pointer::{path_to_pointer, push_escaped_token};
|
|
23
|
+
use crate::state::PULL_STATE;
|
|
24
|
+
|
|
25
|
+
/// The document bytes behind a node tree. A frozen Ruby source is
|
|
26
|
+
/// borrowed zero-copy: freezing rules out mutation, and every node
|
|
27
|
+
/// GC-marks the string with `rb_gc_mark` semantics, which both keeps it
|
|
28
|
+
/// alive and pins it against compaction, so the captured pointer stays
|
|
29
|
+
/// valid for as long as any node exists. Anything else is copied once.
|
|
30
|
+
pub(crate) enum DocBytes {
|
|
31
|
+
Owned(Vec<u8>),
|
|
32
|
+
Frozen {
|
|
33
|
+
source: rb_sys::VALUE,
|
|
34
|
+
ptr: *const u8,
|
|
35
|
+
len: usize,
|
|
36
|
+
},
|
|
37
|
+
/// A read-only file mapping (`NOSJ.load_lazy_file`): pages never
|
|
38
|
+
/// touched are never read off disk. Concurrent modification of the
|
|
39
|
+
/// mapped file by another process is documented as unsupported
|
|
40
|
+
/// (the standard mmap caveat).
|
|
41
|
+
Mmap(memmap2::Mmap),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// The shared document: stable bytes plus the parse options every
|
|
45
|
+
/// materialization from this document uses.
|
|
46
|
+
struct DocInner {
|
|
47
|
+
bytes: DocBytes,
|
|
48
|
+
opts: ParseNativeOpts,
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// SAFETY: the bytes are immutable for the document's whole life (an
|
|
52
|
+
// owned Vec, or a frozen Ruby string pinned and kept alive by every
|
|
53
|
+
// node's GC mark), so cross-thread reads are plain shared reads; the
|
|
54
|
+
// raw VALUE is only dereferenced by the GC mark, which runs at
|
|
55
|
+
// safepoints (the ShadowHandle contract in state.rs). There is no
|
|
56
|
+
// interior mutability anywhere in the type.
|
|
57
|
+
unsafe impl Send for DocInner {}
|
|
58
|
+
unsafe impl Sync for DocInner {}
|
|
59
|
+
|
|
60
|
+
impl DocInner {
|
|
61
|
+
fn bytes(&self) -> &[u8] {
|
|
62
|
+
match &self.bytes {
|
|
63
|
+
DocBytes::Owned(v) => v,
|
|
64
|
+
// SAFETY: the source string is frozen (no mutation, no
|
|
65
|
+
// buffer reallocation) and pinned+kept alive by every
|
|
66
|
+
// node's GC mark; see DocBytes.
|
|
67
|
+
DocBytes::Frozen { ptr, len, .. } => unsafe { std::slice::from_raw_parts(*ptr, *len) },
|
|
68
|
+
DocBytes::Mmap(m) => m,
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const KIND_OBJECT: u8 = b'{';
|
|
74
|
+
const KIND_ARRAY: u8 = b'[';
|
|
75
|
+
|
|
76
|
+
/// One lazy container node: a byte span into its document. Spans always
|
|
77
|
+
/// come from the crate's resolver (token edges within the doc bytes),
|
|
78
|
+
/// and never cross the Ruby boundary, so they cannot be forged from
|
|
79
|
+
/// Ruby.
|
|
80
|
+
#[derive(TypedData)]
|
|
81
|
+
#[magnus(class = "NOSJ::Lazy", mark)]
|
|
82
|
+
pub struct LazyNode {
|
|
83
|
+
doc: Arc<DocInner>,
|
|
84
|
+
start: usize,
|
|
85
|
+
end: usize,
|
|
86
|
+
kind: u8,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
impl DataTypeFunctions for LazyNode {
|
|
90
|
+
fn mark(&self, marker: &magnus::gc::Marker) {
|
|
91
|
+
if let DocBytes::Frozen { source, .. } = self.doc.bytes {
|
|
92
|
+
use magnus::rb_sys::FromRawValue;
|
|
93
|
+
// SAFETY: the VALUE was a live, frozen string at node
|
|
94
|
+
// creation and this mark is what keeps it that way.
|
|
95
|
+
marker.mark(unsafe { Value::from_raw(source) });
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl LazyNode {
|
|
101
|
+
fn span(&self) -> &[u8] {
|
|
102
|
+
&self.doc.bytes()[self.start..self.end]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/// Wrap a resolved raw-value slice: containers become new lazy nodes,
|
|
107
|
+
/// scalars materialize now. `sub` must be a subslice of `doc.bytes`.
|
|
108
|
+
fn resolved_to_value(ruby: &Ruby, doc: &Arc<DocInner>, sub: &[u8]) -> Result<Value, Error> {
|
|
109
|
+
let (start, end) = span_of(doc.bytes(), sub);
|
|
110
|
+
match sub.first().copied() {
|
|
111
|
+
Some(k) if k == KIND_OBJECT || k == KIND_ARRAY => {
|
|
112
|
+
let node = LazyNode {
|
|
113
|
+
doc: Arc::clone(doc),
|
|
114
|
+
start,
|
|
115
|
+
end,
|
|
116
|
+
kind: k,
|
|
117
|
+
};
|
|
118
|
+
let obj: Obj<LazyNode> = ruby.obj_wrap(node);
|
|
119
|
+
Ok(obj.as_value())
|
|
120
|
+
}
|
|
121
|
+
_ => materialize_at(ruby, doc.bytes(), start, end, &doc.opts),
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Resolve `pointer` within `node`'s span. Shared by `__get` and
|
|
126
|
+
/// `__at_pointer`; both misses and negative-index paths return nil.
|
|
127
|
+
fn resolve_in_span(ruby: &Ruby, node: &LazyNode, pointer: &str) -> Result<Value, Error> {
|
|
128
|
+
// Resolve first (one PULL_STATE borrow, slice borrows the doc, not
|
|
129
|
+
// the buffers), then materialize (which re-borrows internally).
|
|
130
|
+
let resolved = PULL_STATE.with(|cell| {
|
|
131
|
+
let mut state = cell.borrow_mut();
|
|
132
|
+
// SAFETY: doc bytes were coderange-gated at NOSJ.lazy creation,
|
|
133
|
+
// and spans lie on token edges, so the span is valid UTF-8.
|
|
134
|
+
unsafe { nosj::pointer_utf8_unchecked(node.span(), pointer, &mut state.bufs) }
|
|
135
|
+
});
|
|
136
|
+
match resolved {
|
|
137
|
+
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
138
|
+
Ok(Some(sub)) => resolved_to_value(ruby, &node.doc, sub.as_bytes()),
|
|
139
|
+
Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
|
|
140
|
+
Err(Error::new(ruby.exception_arg_error(), e.to_string()))
|
|
141
|
+
}
|
|
142
|
+
Err(e) => Err(reader_err(ruby, node, e)),
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/// `NOSJ.lazy(source, opts)`: gate the encoding, copy the bytes, locate
|
|
147
|
+
/// the root value. Container roots wrap as lazy nodes; a scalar root has
|
|
148
|
+
/// nothing to defer and materializes immediately.
|
|
149
|
+
///
|
|
150
|
+
/// The root span is the input trimmed of surrounding whitespace; no
|
|
151
|
+
/// byte of content is walked here (resolving via the "" pointer would
|
|
152
|
+
/// bracket-skip the whole document just to find the same end), so
|
|
153
|
+
/// malformation anywhere, including root-level trailing garbage,
|
|
154
|
+
/// surfaces on first access, per the module's lazy-validation contract.
|
|
155
|
+
pub fn lazy_native(
|
|
156
|
+
ruby: &Ruby,
|
|
157
|
+
_rb_self: Value,
|
|
158
|
+
data: RString,
|
|
159
|
+
opts: Value,
|
|
160
|
+
) -> Result<Value, Error> {
|
|
161
|
+
let o = parse_native_opts(ruby, opts)?;
|
|
162
|
+
let input = utf8_input(ruby, &data)?;
|
|
163
|
+
|
|
164
|
+
// Frozen sources are borrowed zero-copy (see DocBytes); `data` is
|
|
165
|
+
// on the caller's machine stack, so it stays pinned through this
|
|
166
|
+
// call, and the node's mark takes over from the first GC on.
|
|
167
|
+
let bytes = if data.as_value().is_frozen() {
|
|
168
|
+
use magnus::rb_sys::AsRawValue;
|
|
169
|
+
DocBytes::Frozen {
|
|
170
|
+
source: data.as_raw(),
|
|
171
|
+
ptr: input.as_ptr(),
|
|
172
|
+
len: input.len(),
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
DocBytes::Owned(input.to_vec())
|
|
176
|
+
};
|
|
177
|
+
wrap_root(ruby, bytes, o)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// Wrap document bytes as the root lazy value. Shared by `NOSJ.lazy`
|
|
181
|
+
/// and `NOSJ.load_lazy_file`; the bytes must already be known UTF-8.
|
|
182
|
+
pub(crate) fn wrap_root(
|
|
183
|
+
ruby: &Ruby,
|
|
184
|
+
bytes: DocBytes,
|
|
185
|
+
opts: ParseNativeOpts,
|
|
186
|
+
) -> Result<Value, Error> {
|
|
187
|
+
const WS: [u8; 4] = *b" \t\n\r";
|
|
188
|
+
let doc = Arc::new(DocInner { bytes, opts });
|
|
189
|
+
let input = doc.bytes();
|
|
190
|
+
let Some(start) = input.iter().position(|b| !WS.contains(b)) else {
|
|
191
|
+
return Err(parser_error_at(
|
|
192
|
+
ruby,
|
|
193
|
+
input,
|
|
194
|
+
input.len(),
|
|
195
|
+
"unexpected end of input".into(),
|
|
196
|
+
));
|
|
197
|
+
};
|
|
198
|
+
let end = input.iter().rposition(|b| !WS.contains(b)).unwrap() + 1;
|
|
199
|
+
let sub = &doc.bytes()[start..end];
|
|
200
|
+
resolved_to_value(ruby, &doc, sub)
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/// `__get(token)`: one path step. Integer tokens are JSON Pointer
|
|
204
|
+
/// indices (negative ones resolve to nil, as in NOSJ.dig); String and
|
|
205
|
+
/// Symbol tokens are keys, `~`/`/`-escaped.
|
|
206
|
+
pub fn lazy_get(ruby: &Ruby, rb_self: Obj<LazyNode>, token: Value) -> Result<Value, Error> {
|
|
207
|
+
let mut ptr = String::new();
|
|
208
|
+
if let Some(int) = magnus::Integer::from_value(token) {
|
|
209
|
+
let idx = int.to_i64()?;
|
|
210
|
+
if idx < 0 {
|
|
211
|
+
return Ok(ruby.qnil().as_value());
|
|
212
|
+
}
|
|
213
|
+
ptr.push('/');
|
|
214
|
+
ptr.push_str(&idx.to_string());
|
|
215
|
+
} else if let Some(s) = RString::from_value(token) {
|
|
216
|
+
push_escaped_token(&mut ptr, &s.to_string()?);
|
|
217
|
+
} else if let Some(sym) = magnus::Symbol::from_value(token) {
|
|
218
|
+
push_escaped_token(&mut ptr, &sym.name()?);
|
|
219
|
+
} else {
|
|
220
|
+
return Err(Error::new(
|
|
221
|
+
ruby.exception_arg_error(),
|
|
222
|
+
"keys must be Strings, Symbols, or Integers",
|
|
223
|
+
));
|
|
224
|
+
}
|
|
225
|
+
resolve_in_span(ruby, &rb_self, &ptr)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/// `__dig(path)`: the whole dig path fused into ONE pointer resolution
|
|
229
|
+
/// within this node's span, instead of one resolve (and one cached
|
|
230
|
+
/// node) per step. Semantics match `NOSJ.dig`: negative indices and
|
|
231
|
+
/// steps into scalars resolve to nil.
|
|
232
|
+
pub fn lazy_dig(ruby: &Ruby, rb_self: Obj<LazyNode>, path: RArray) -> Result<Value, Error> {
|
|
233
|
+
match path_to_pointer(ruby, path)? {
|
|
234
|
+
Some(ptr) => resolve_in_span(ruby, &rb_self, &ptr),
|
|
235
|
+
None => Ok(ruby.qnil().as_value()),
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// `__at_pointer(pointer)`: a full RFC 6901 pointer, resolved within
|
|
240
|
+
/// this node's subtree.
|
|
241
|
+
pub fn lazy_at_pointer(
|
|
242
|
+
ruby: &Ruby,
|
|
243
|
+
rb_self: Obj<LazyNode>,
|
|
244
|
+
pointer: RString,
|
|
245
|
+
) -> Result<Value, Error> {
|
|
246
|
+
let ptr = pointer.to_string()?;
|
|
247
|
+
resolve_in_span(ruby, &rb_self, &ptr)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/// `__materialize`: the whole span as plain Ruby values, under the
|
|
251
|
+
/// document's parse options.
|
|
252
|
+
pub fn lazy_materialize(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<Value, Error> {
|
|
253
|
+
materialize_at(
|
|
254
|
+
ruby,
|
|
255
|
+
rb_self.doc.bytes(),
|
|
256
|
+
rb_self.start,
|
|
257
|
+
rb_self.end,
|
|
258
|
+
&rb_self.doc.opts,
|
|
259
|
+
)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/// `__kind`: `:object` or `:array`.
|
|
263
|
+
pub fn lazy_kind(ruby: &Ruby, rb_self: Obj<LazyNode>) -> magnus::Symbol {
|
|
264
|
+
match rb_self.kind {
|
|
265
|
+
KIND_OBJECT => ruby.to_symbol("object"),
|
|
266
|
+
_ => ruby.to_symbol("array"),
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/// `__byte_size`: span length in bytes (cheap; used by #inspect).
|
|
271
|
+
pub fn lazy_byte_size(_ruby: &Ruby, rb_self: Obj<LazyNode>) -> usize {
|
|
272
|
+
rb_self.end - rb_self.start
|
|
273
|
+
}
|
|
274
|
+
|
|
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())
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// `__keys`: the object's decoded keys, one Reader walk, values skipped.
|
|
283
|
+
pub fn lazy_keys(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
284
|
+
if rb_self.kind != KIND_OBJECT {
|
|
285
|
+
return Err(Error::new(
|
|
286
|
+
ruby.exception_type_error(),
|
|
287
|
+
"keys on a JSON array",
|
|
288
|
+
));
|
|
289
|
+
}
|
|
290
|
+
let out = ruby.ary_new();
|
|
291
|
+
PULL_STATE.with(|cell| -> Result<(), Error> {
|
|
292
|
+
let mut state = cell.borrow_mut();
|
|
293
|
+
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
294
|
+
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
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
|
+
{
|
|
300
|
+
Some(k) => {
|
|
301
|
+
out.push(ruby.str_new(k))?;
|
|
302
|
+
true
|
|
303
|
+
}
|
|
304
|
+
None => false,
|
|
305
|
+
};
|
|
306
|
+
while has {
|
|
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
|
+
{
|
|
312
|
+
Some(k) => {
|
|
313
|
+
out.push(ruby.str_new(k))?;
|
|
314
|
+
true
|
|
315
|
+
}
|
|
316
|
+
None => false,
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
Ok(())
|
|
320
|
+
})?;
|
|
321
|
+
Ok(out)
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/// `__size`: entry count (object pairs or array elements), one walk,
|
|
325
|
+
/// nothing materialized.
|
|
326
|
+
pub fn lazy_size(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<usize, Error> {
|
|
327
|
+
PULL_STATE.with(|cell| {
|
|
328
|
+
let mut state = cell.borrow_mut();
|
|
329
|
+
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
330
|
+
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
331
|
+
r.next_node().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
332
|
+
let mut n = 0usize;
|
|
333
|
+
if rb_self.kind == KIND_OBJECT {
|
|
334
|
+
let mut has = r
|
|
335
|
+
.object_first_key()
|
|
336
|
+
.map_err(|e| reader_err(ruby, &rb_self, e))?
|
|
337
|
+
.is_some();
|
|
338
|
+
while has {
|
|
339
|
+
n += 1;
|
|
340
|
+
r.skip_value().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
341
|
+
has = r
|
|
342
|
+
.object_next_key()
|
|
343
|
+
.map_err(|e| reader_err(ruby, &rb_self, e))?
|
|
344
|
+
.is_some();
|
|
345
|
+
}
|
|
346
|
+
} else {
|
|
347
|
+
let mut has = r.array_first().map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
348
|
+
while has {
|
|
349
|
+
n += 1;
|
|
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))?;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
Ok(n)
|
|
355
|
+
})
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/// A child discovered during a container walk: a doc-relative span,
|
|
359
|
+
/// plus the owned key for object entries (decoded keys borrow the
|
|
360
|
+
/// reader's scratch, so they are copied out before phase two).
|
|
361
|
+
struct ChildDesc {
|
|
362
|
+
key: Option<String>,
|
|
363
|
+
start: usize,
|
|
364
|
+
end: usize,
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/// `__children`: every direct child in ONE walk. Objects yield
|
|
368
|
+
/// `[key, child]` pairs, arrays yield children; containers wrap lazily,
|
|
369
|
+
/// scalars materialize. Two phases so the Reader's buffer borrow ends
|
|
370
|
+
/// before materialization re-borrows the thread state.
|
|
371
|
+
pub fn lazy_children(ruby: &Ruby, rb_self: Obj<LazyNode>) -> Result<RArray, Error> {
|
|
372
|
+
let base = rb_self.doc.bytes().as_ptr() as usize;
|
|
373
|
+
let descs: Result<Vec<ChildDesc>, nosj::ParseError> = PULL_STATE.with(|cell| {
|
|
374
|
+
let mut state = cell.borrow_mut();
|
|
375
|
+
// SAFETY: spans are valid UTF-8 (see resolve_in_span).
|
|
376
|
+
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(rb_self.span(), &mut state.bufs) };
|
|
377
|
+
r.next_node()?;
|
|
378
|
+
let mut out = Vec::new();
|
|
379
|
+
if rb_self.kind == KIND_OBJECT {
|
|
380
|
+
let mut key = r.object_first_key()?.map(String::from);
|
|
381
|
+
while let Some(k) = key {
|
|
382
|
+
let sub = r.skip_value()?;
|
|
383
|
+
let start = sub.as_ptr() as usize - base;
|
|
384
|
+
out.push(ChildDesc {
|
|
385
|
+
key: Some(k),
|
|
386
|
+
start,
|
|
387
|
+
end: start + sub.len(),
|
|
388
|
+
});
|
|
389
|
+
key = r.object_next_key()?.map(String::from);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
let mut has = r.array_first()?;
|
|
393
|
+
while has {
|
|
394
|
+
let sub = r.skip_value()?;
|
|
395
|
+
let start = sub.as_ptr() as usize - base;
|
|
396
|
+
out.push(ChildDesc {
|
|
397
|
+
key: None,
|
|
398
|
+
start,
|
|
399
|
+
end: start + sub.len(),
|
|
400
|
+
});
|
|
401
|
+
has = r.array_next()?;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
Ok(out)
|
|
405
|
+
});
|
|
406
|
+
let descs = descs.map_err(|e| reader_err(ruby, &rb_self, e))?;
|
|
407
|
+
|
|
408
|
+
let out = ruby.ary_new_capa(descs.len());
|
|
409
|
+
for d in descs {
|
|
410
|
+
let child = resolved_to_value(ruby, &rb_self.doc, &rb_self.doc.bytes()[d.start..d.end])?;
|
|
411
|
+
match d.key {
|
|
412
|
+
Some(k) => {
|
|
413
|
+
let pair = ruby.ary_new_capa(2);
|
|
414
|
+
pair.push(ruby.str_new(&k))?;
|
|
415
|
+
pair.push(child)?;
|
|
416
|
+
out.push(pair)?;
|
|
417
|
+
}
|
|
418
|
+
None => out.push(child)?,
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
Ok(out)
|
|
422
|
+
}
|