nosj 0.4.1 → 0.5.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 +61 -0
- data/Cargo.lock +1 -1
- data/README.md +30 -14
- data/ext/nosj/Cargo.toml +1 -1
- data/ext/nosj/fuzz/src/prelude.rb +20 -44
- data/ext/nosj/src/gen/opts.rs +76 -141
- data/ext/nosj/src/gen/ruby.rs +5 -0
- data/ext/nosj/src/gen/walker.rs +166 -32
- data/ext/nosj/src/lib.rs +4 -0
- data/ext/nosj/src/locate.rs +162 -0
- data/ext/nosj/src/opt_reader.rs +170 -0
- data/ext/nosj/src/parse.rs +160 -93
- data/ext/nosj/src/reformat.rs +54 -125
- data/ext/nosj/src/sink.rs +180 -44
- data/ext/nosj/src/state.rs +5 -0
- data/ext/nosj/src/stats.rs +33 -30
- data/lib/nosj/json.rb +105 -39
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +34 -21
- data/sig/nosj.rbs +4 -3
- metadata +3 -1
data/ext/nosj/src/gen/walker.rs
CHANGED
|
@@ -16,8 +16,9 @@ use super::keys::GenKeyCache;
|
|
|
16
16
|
use super::opts::GenConfig;
|
|
17
17
|
use super::ruby::{
|
|
18
18
|
is_json_fragment, is_special_const, protected_as_json, protected_encode_utf8,
|
|
19
|
-
protected_to_json, protected_to_json_if_responds, protected_to_s,
|
|
20
|
-
str_enc_index, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL,
|
|
19
|
+
protected_inspect, protected_to_json, protected_to_json_if_responds, protected_to_s,
|
|
20
|
+
rstring_bytes, str_coderange, str_enc_index, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL,
|
|
21
|
+
QTRUE,
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
/// Whether keys escaped under `mode` may be cached: the cached bytes
|
|
@@ -27,6 +28,84 @@ pub(super) fn mode_cacheable(mode: EscapeMode) -> bool {
|
|
|
27
28
|
matches!(mode, EscapeMode::Standard | EscapeMode::HtmlSafe)
|
|
28
29
|
}
|
|
29
30
|
|
|
31
|
+
/// A hash key's kind: how it renders (see [`key_string`]) and json 3's
|
|
32
|
+
/// duplicate-key rule.
|
|
33
|
+
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
34
|
+
enum KeyKind {
|
|
35
|
+
String,
|
|
36
|
+
Symbol,
|
|
37
|
+
Other,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
#[inline(always)]
|
|
41
|
+
fn key_kind(k: VALUE) -> KeyKind {
|
|
42
|
+
if !is_special_const(k) {
|
|
43
|
+
match unsafe { RB_BUILTIN_TYPE(k) } {
|
|
44
|
+
ruby_value_type::RUBY_T_STRING => KeyKind::String,
|
|
45
|
+
ruby_value_type::RUBY_T_SYMBOL => KeyKind::Symbol,
|
|
46
|
+
_ => KeyKind::Other,
|
|
47
|
+
}
|
|
48
|
+
} else if STATIC_SYM_P(k) {
|
|
49
|
+
KeyKind::Symbol
|
|
50
|
+
} else {
|
|
51
|
+
KeyKind::Other
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/// The String a key renders as, the gem's key coercion: Strings as they
|
|
56
|
+
/// are, Symbols by name, anything else through `to_s`.
|
|
57
|
+
fn key_string(k: VALUE, kind: KeyKind) -> Result<VALUE, Error> {
|
|
58
|
+
match kind {
|
|
59
|
+
KeyKind::String => Ok(k),
|
|
60
|
+
KeyKind::Symbol => Ok(unsafe { rb_sys::rb_sym2str(k) }),
|
|
61
|
+
KeyKind::Other => protected_to_s(k),
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// json 3's cheap trigger for its duplicate-key check, per hash: keys of
|
|
66
|
+
/// one kind cannot render alike, so only a String or Symbol key in a
|
|
67
|
+
/// hash whose first key was of another kind runs the full check (once).
|
|
68
|
+
/// A key of the first key's kind costs one byte compare (the Option
|
|
69
|
+
/// packs into one byte), a hash's first key one more; only actual
|
|
70
|
+
/// mixing goes out of line.
|
|
71
|
+
struct MixedKeys {
|
|
72
|
+
first: Option<KeyKind>,
|
|
73
|
+
/// Checked already, or `allow_duplicate_key`: nothing left to do.
|
|
74
|
+
done: bool,
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl MixedKeys {
|
|
78
|
+
#[inline(always)]
|
|
79
|
+
fn new(allow_duplicate_key: bool) -> Self {
|
|
80
|
+
MixedKeys {
|
|
81
|
+
first: None,
|
|
82
|
+
done: allow_duplicate_key,
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
#[inline(always)]
|
|
87
|
+
fn needs_check(&mut self, kind: KeyKind) -> bool {
|
|
88
|
+
match self.first {
|
|
89
|
+
Some(first) if first == kind => false,
|
|
90
|
+
None => {
|
|
91
|
+
self.first = Some(kind);
|
|
92
|
+
false
|
|
93
|
+
}
|
|
94
|
+
Some(_) => self.mixed(kind),
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
#[cold]
|
|
99
|
+
#[inline(never)]
|
|
100
|
+
fn mixed(&mut self, kind: KeyKind) -> bool {
|
|
101
|
+
if self.done || kind == KeyKind::Other {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
self.done = true;
|
|
105
|
+
true
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
30
109
|
pub(super) struct Gen<'a> {
|
|
31
110
|
/// The pooled per-thread output buffer, borrowed for the call.
|
|
32
111
|
pub(super) out: &'a mut Vec<u8>,
|
|
@@ -164,13 +243,19 @@ impl Gen<'_> {
|
|
|
164
243
|
/// The pre-escaped bytes for `k` when the cache may serve it:
|
|
165
244
|
/// frozen string key, cacheable escape mode (see
|
|
166
245
|
/// [`Gen::emit_key_cached`]). An associated fn over the split-out
|
|
167
|
-
/// fields so callers keep `self.out` free.
|
|
246
|
+
/// fields so callers keep `self.out` free. `kind` is the caller's
|
|
247
|
+
/// [`key_kind`] of `k`, computed once per pair for both this and
|
|
248
|
+
/// the duplicate-key tracking.
|
|
168
249
|
#[inline(always)]
|
|
169
|
-
fn cached_key_bytes<'k>(
|
|
250
|
+
fn cached_key_bytes<'k>(
|
|
251
|
+
cfg: &GenConfig,
|
|
252
|
+
keys: &'k GenKeyCache,
|
|
253
|
+
k: VALUE,
|
|
254
|
+
kind: KeyKind,
|
|
255
|
+
) -> Option<&'k [u8]> {
|
|
170
256
|
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
171
|
-
if
|
|
172
|
-
&&
|
|
173
|
-
&& unsafe { RB_BUILTIN_TYPE(k) } == ruby_value_type::RUBY_T_STRING
|
|
257
|
+
if kind == KeyKind::String
|
|
258
|
+
&& mode_cacheable(cfg.mode)
|
|
174
259
|
&& unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0
|
|
175
260
|
{
|
|
176
261
|
keys.get(k)
|
|
@@ -187,8 +272,8 @@ impl Gen<'_> {
|
|
|
187
272
|
/// all through the cache-hit path here. Misses take the plain path
|
|
188
273
|
/// below, which also populates the cache.
|
|
189
274
|
#[inline(always)]
|
|
190
|
-
fn emit_pair_prefix_compact(&mut self, k: VALUE, comma: bool) -> Result<(), ()> {
|
|
191
|
-
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k) {
|
|
275
|
+
fn emit_pair_prefix_compact(&mut self, k: VALUE, kind: KeyKind, comma: bool) -> Result<(), ()> {
|
|
276
|
+
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k, kind) {
|
|
192
277
|
let n = bytes.len();
|
|
193
278
|
self.out.reserve(n + 2);
|
|
194
279
|
// SAFETY: `n + 2` bytes reserved above; `bytes` borrows
|
|
@@ -211,7 +296,7 @@ impl Gen<'_> {
|
|
|
211
296
|
if comma {
|
|
212
297
|
self.out.push(b',');
|
|
213
298
|
}
|
|
214
|
-
self.emit_key(k)?;
|
|
299
|
+
self.emit_key(k, kind)?;
|
|
215
300
|
self.out.push(b':');
|
|
216
301
|
Ok(())
|
|
217
302
|
}
|
|
@@ -221,8 +306,14 @@ impl Gen<'_> {
|
|
|
221
306
|
/// separator, cached key, colon, and digits in one reservation and
|
|
222
307
|
/// one raw cursor.
|
|
223
308
|
#[inline(always)]
|
|
224
|
-
fn emit_pair_int_compact(
|
|
225
|
-
|
|
309
|
+
fn emit_pair_int_compact(
|
|
310
|
+
&mut self,
|
|
311
|
+
k: VALUE,
|
|
312
|
+
kind: KeyKind,
|
|
313
|
+
comma: bool,
|
|
314
|
+
value: i64,
|
|
315
|
+
) -> Result<(), ()> {
|
|
316
|
+
if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k, kind) {
|
|
226
317
|
let n = bytes.len();
|
|
227
318
|
self.out.reserve(n + 2 + emit::I64_MAX_LEN);
|
|
228
319
|
// SAFETY: the reservation covers separator + key + colon +
|
|
@@ -245,28 +336,18 @@ impl Gen<'_> {
|
|
|
245
336
|
}
|
|
246
337
|
return Ok(());
|
|
247
338
|
}
|
|
248
|
-
self.emit_pair_prefix_compact(k, comma)?;
|
|
339
|
+
self.emit_pair_prefix_compact(k, kind, comma)?;
|
|
249
340
|
emit::write_i64(&mut *self.out, value);
|
|
250
341
|
Ok(())
|
|
251
342
|
}
|
|
252
343
|
|
|
253
|
-
/// Object/hash key
|
|
254
|
-
///
|
|
255
|
-
fn emit_key(&mut self, k: VALUE) -> Result<(), ()> {
|
|
256
|
-
if
|
|
257
|
-
|
|
258
|
-
ruby_value_type::RUBY_T_STRING => return self.emit_key_cached(k),
|
|
259
|
-
ruby_value_type::RUBY_T_SYMBOL => {
|
|
260
|
-
let s = unsafe { rb_sys::rb_sym2str(k) };
|
|
261
|
-
return self.emit_rstring_quoted(s);
|
|
262
|
-
}
|
|
263
|
-
_ => {}
|
|
264
|
-
}
|
|
265
|
-
} else if STATIC_SYM_P(k) {
|
|
266
|
-
let s = unsafe { rb_sys::rb_sym2str(k) };
|
|
267
|
-
return self.emit_rstring_quoted(s);
|
|
344
|
+
/// Object/hash key of `kind` (the caller's [`key_kind`]), rendered
|
|
345
|
+
/// by [`key_string`]; String keys go through the pre-escaped cache.
|
|
346
|
+
fn emit_key(&mut self, k: VALUE, kind: KeyKind) -> Result<(), ()> {
|
|
347
|
+
if kind == KeyKind::String {
|
|
348
|
+
return self.emit_key_cached(k);
|
|
268
349
|
}
|
|
269
|
-
let s = self.reraise(
|
|
350
|
+
let s = self.reraise(key_string(k, kind))?;
|
|
270
351
|
self.emit_rstring_quoted(s)
|
|
271
352
|
}
|
|
272
353
|
|
|
@@ -311,6 +392,50 @@ impl Gen<'_> {
|
|
|
311
392
|
self.emit_rstring_quoted(s)
|
|
312
393
|
}
|
|
313
394
|
|
|
395
|
+
/// json 3's full duplicate check, run once for a hash whose keys
|
|
396
|
+
/// mixed kinds: every key through `to_s`, first repeat raises the
|
|
397
|
+
/// gem's exact message. Cold: only mixed-kind hashes reach it.
|
|
398
|
+
#[cold]
|
|
399
|
+
fn check_duplicate_keys(&mut self, hash: VALUE) -> Result<(), ()> {
|
|
400
|
+
use super::hash_iter::{foreach_raw, Step};
|
|
401
|
+
let mut seen = std::collections::HashSet::new();
|
|
402
|
+
let mut outcome: Result<Option<VALUE>, Error> = Ok(None);
|
|
403
|
+
// SAFETY: only reached for T_HASH values.
|
|
404
|
+
unsafe {
|
|
405
|
+
foreach_raw(hash, |k, _| {
|
|
406
|
+
let key_str = match key_string(k, key_kind(k)) {
|
|
407
|
+
Ok(s) => s,
|
|
408
|
+
Err(exc) => {
|
|
409
|
+
outcome = Err(exc);
|
|
410
|
+
return Step::Stop;
|
|
411
|
+
}
|
|
412
|
+
};
|
|
413
|
+
if seen.insert(rstring_bytes(key_str).to_vec()) {
|
|
414
|
+
Step::Continue
|
|
415
|
+
} else {
|
|
416
|
+
outcome = Ok(Some(key_str));
|
|
417
|
+
Step::Stop
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
let Some(key_str) = self.reraise(outcome)? else {
|
|
422
|
+
return Ok(());
|
|
423
|
+
};
|
|
424
|
+
let key_inspect = self.reraise(protected_inspect(key_str))?;
|
|
425
|
+
let hash_inspect = self.reraise(protected_inspect(hash))?;
|
|
426
|
+
// SAFETY: rb_inspect returns Strings; both are copied out here.
|
|
427
|
+
let (key_inspect, hash_inspect) = unsafe {
|
|
428
|
+
(
|
|
429
|
+
String::from_utf8_lossy(rstring_bytes(key_inspect)).into_owned(),
|
|
430
|
+
String::from_utf8_lossy(rstring_bytes(hash_inspect)).into_owned(),
|
|
431
|
+
)
|
|
432
|
+
};
|
|
433
|
+
self.fail = Some(GenFail::Generator(format!(
|
|
434
|
+
"detected duplicate key {key_inspect} in {hash_inspect}"
|
|
435
|
+
)));
|
|
436
|
+
Err(())
|
|
437
|
+
}
|
|
438
|
+
|
|
314
439
|
/// Splice `raw`'s `to_json` result verbatim: the JSON::Fragment
|
|
315
440
|
/// path (pre-rendered JSON, trusted like the gem trusts it).
|
|
316
441
|
fn splice_to_json(&mut self, raw: VALUE) -> Result<(), ()> {
|
|
@@ -506,18 +631,23 @@ impl Gen<'_> {
|
|
|
506
631
|
self.out.push(b'}');
|
|
507
632
|
return Ok(());
|
|
508
633
|
}
|
|
634
|
+
let mut mixed = MixedKeys::new(self.cfg.allow_duplicate_key);
|
|
509
635
|
// SAFETY: emit_object is only reached for T_HASH values.
|
|
510
636
|
if PRETTY {
|
|
511
637
|
let mut first = true;
|
|
512
638
|
unsafe {
|
|
513
639
|
foreach_raw(hash, |k, v| {
|
|
640
|
+
let kind = key_kind(k);
|
|
641
|
+
if mixed.needs_check(kind) && self.check_duplicate_keys(hash).is_err() {
|
|
642
|
+
return Step::Stop;
|
|
643
|
+
}
|
|
514
644
|
if !first {
|
|
515
645
|
self.out.push(b',');
|
|
516
646
|
}
|
|
517
647
|
first = false;
|
|
518
648
|
self.out.extend_from_slice(&self.cfg.object_nl);
|
|
519
649
|
self.push_indent(inner);
|
|
520
|
-
if self.emit_key(k).is_err() {
|
|
650
|
+
if self.emit_key(k, kind).is_err() {
|
|
521
651
|
return Step::Stop;
|
|
522
652
|
}
|
|
523
653
|
self.out.extend_from_slice(&self.cfg.space_before);
|
|
@@ -532,20 +662,24 @@ impl Gen<'_> {
|
|
|
532
662
|
} else {
|
|
533
663
|
unsafe {
|
|
534
664
|
foreach_raw(hash, |k, v| {
|
|
665
|
+
let kind = key_kind(k);
|
|
666
|
+
if mixed.needs_check(kind) && self.check_duplicate_keys(hash).is_err() {
|
|
667
|
+
return Step::Stop;
|
|
668
|
+
}
|
|
535
669
|
// `out` always ends with '{' (just pushed) or the
|
|
536
670
|
// previous pair.
|
|
537
671
|
let comma = *self.out.last().unwrap_unchecked() != b'{';
|
|
538
672
|
// Int values fuse with their key into one write.
|
|
539
673
|
if FIXNUM_P(v) {
|
|
540
674
|
if self
|
|
541
|
-
.emit_pair_int_compact(k, comma, FIX2LONG(v) as i64)
|
|
675
|
+
.emit_pair_int_compact(k, kind, comma, FIX2LONG(v) as i64)
|
|
542
676
|
.is_err()
|
|
543
677
|
{
|
|
544
678
|
return Step::Stop;
|
|
545
679
|
}
|
|
546
680
|
return Step::Continue;
|
|
547
681
|
}
|
|
548
|
-
if self.emit_pair_prefix_compact(k, comma).is_err() {
|
|
682
|
+
if self.emit_pair_prefix_compact(k, kind, comma).is_err() {
|
|
549
683
|
return Step::Stop;
|
|
550
684
|
}
|
|
551
685
|
// Value fast arms, mirroring emit_value's: one
|
data/ext/nosj/src/lib.rs
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
//! - `parse.rs`: whole-document entry points (parse, valid?, the
|
|
5
5
|
//! GVL-releasing indexed parse) plus shared option decoding and
|
|
6
6
|
//! input gating.
|
|
7
|
+
//! - `opt_reader.rs`: options-hash reading with json 3's unknown-key
|
|
8
|
+
//! rule, shared by the parse and generate decoders.
|
|
7
9
|
//! - `pointer.rs`: partial parsing (dig, at_pointer, batch forms).
|
|
8
10
|
//! - `lazy.rs`: lazy documents (NOSJ.lazy nodes resolving access on
|
|
9
11
|
//! demand over shared document bytes).
|
|
@@ -23,6 +25,8 @@ pub mod files;
|
|
|
23
25
|
pub mod gen;
|
|
24
26
|
pub mod lazy;
|
|
25
27
|
pub mod lines;
|
|
28
|
+
pub mod locate;
|
|
29
|
+
pub mod opt_reader;
|
|
26
30
|
pub mod parse;
|
|
27
31
|
pub mod patch;
|
|
28
32
|
pub mod pointer;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
//! Cold-path positions for the refusals a sink detects without offsets.
|
|
2
|
+
//!
|
|
3
|
+
//! Sinks see events, not byte positions, so a duplicate key (found by
|
|
4
|
+
//! hash size or key fingerprints) or a lone surrogate (delivered as
|
|
5
|
+
//! WTF-8) aborts the drive with no location. These walks re-read the
|
|
6
|
+
//! same bytes with the crate's pull `Reader`, under the same grammar
|
|
7
|
+
//! options, to find where. Each is linear and iterative: one `Reader`
|
|
8
|
+
//! over the whole document, an explicit stack instead of recursion, so
|
|
9
|
+
//! no nesting depth is out of reach.
|
|
10
|
+
|
|
11
|
+
use std::collections::HashSet;
|
|
12
|
+
|
|
13
|
+
use nosj::{Buffers, Node, ParseError, ParseOptions, Reader};
|
|
14
|
+
|
|
15
|
+
use crate::parse::span_of;
|
|
16
|
+
|
|
17
|
+
/// What the exact check found behind a sink's duplicate-key refusal.
|
|
18
|
+
pub(crate) enum Repeat {
|
|
19
|
+
/// The first object, in the order objects close (as the drive meets
|
|
20
|
+
/// them), that repeats a key: the offset of its `{` and the key.
|
|
21
|
+
Found { at: usize, key: String },
|
|
22
|
+
/// No object repeats a key: the refusal was a fingerprint collision.
|
|
23
|
+
Absent,
|
|
24
|
+
/// The walk stopped on a Reader error before deciding. Callers
|
|
25
|
+
/// treat it as a refusal.
|
|
26
|
+
Undecided,
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
pub(crate) fn duplicate_key(doc: &[u8], popts: ParseOptions) -> Repeat {
|
|
30
|
+
match scan(doc, popts, true) {
|
|
31
|
+
Ok(None) => Repeat::Absent,
|
|
32
|
+
Ok(Some((path, key))) => match container_offset(doc, &path, popts) {
|
|
33
|
+
Ok(at) => Repeat::Found { at, key },
|
|
34
|
+
Err(_) => Repeat::Undecided,
|
|
35
|
+
},
|
|
36
|
+
Err(_) => Repeat::Undecided,
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// The first error a full walk of `doc` meets, decoding every string
|
|
41
|
+
/// and key: where a lone surrogate the sink refused sits (the Reader
|
|
42
|
+
/// rejects them with a position).
|
|
43
|
+
pub(crate) fn first_walk_error(doc: &[u8], popts: ParseOptions) -> Option<ParseError> {
|
|
44
|
+
scan(doc, popts, false).err()
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// One open container of [`scan`]'s walk.
|
|
48
|
+
struct Frame {
|
|
49
|
+
object: bool,
|
|
50
|
+
/// The member or element being walked.
|
|
51
|
+
index: usize,
|
|
52
|
+
seen: HashSet<String>,
|
|
53
|
+
repeated: Option<String>,
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
impl Frame {
|
|
57
|
+
fn open(object: bool) -> Self {
|
|
58
|
+
Frame {
|
|
59
|
+
object,
|
|
60
|
+
index: 0,
|
|
61
|
+
seen: HashSet::new(),
|
|
62
|
+
repeated: None,
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
fn note_key(&mut self, key: &str) {
|
|
67
|
+
if self.repeated.is_none() && !self.seen.insert(key.to_owned()) {
|
|
68
|
+
self.repeated = Some(key.to_owned());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/// Walk all of `doc`, depth first. With `find_repeats`, stop at the
|
|
74
|
+
/// first object to close with a repeated key: the member/element
|
|
75
|
+
/// indices from the root down to it, and the key.
|
|
76
|
+
fn scan(
|
|
77
|
+
doc: &[u8],
|
|
78
|
+
popts: ParseOptions,
|
|
79
|
+
find_repeats: bool,
|
|
80
|
+
) -> Result<Option<(Vec<usize>, String)>, ParseError> {
|
|
81
|
+
let mut bufs = Buffers::new();
|
|
82
|
+
// SAFETY: `doc` is validated UTF-8.
|
|
83
|
+
let mut r = unsafe { Reader::from_utf8_unchecked_with(doc, &mut bufs, popts) };
|
|
84
|
+
let mut stack: Vec<Frame> = Vec::new();
|
|
85
|
+
loop {
|
|
86
|
+
// The cursor is at a value: open it, or step past it.
|
|
87
|
+
let opened = match r.next_node()? {
|
|
88
|
+
Node::ObjectStart => match r.object_first_key()? {
|
|
89
|
+
Some(key) => {
|
|
90
|
+
let mut frame = Frame::open(true);
|
|
91
|
+
if find_repeats {
|
|
92
|
+
frame.note_key(key);
|
|
93
|
+
}
|
|
94
|
+
Some(frame)
|
|
95
|
+
}
|
|
96
|
+
None => None,
|
|
97
|
+
},
|
|
98
|
+
Node::ArrayStart => r.array_first()?.then(|| Frame::open(false)),
|
|
99
|
+
_ => None,
|
|
100
|
+
};
|
|
101
|
+
if let Some(frame) = opened {
|
|
102
|
+
stack.push(frame);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// The value is complete: advance its container, closing every
|
|
106
|
+
// container it completes on the way up.
|
|
107
|
+
loop {
|
|
108
|
+
let Some(top) = stack.last_mut() else {
|
|
109
|
+
return Ok(None);
|
|
110
|
+
};
|
|
111
|
+
let more = if top.object {
|
|
112
|
+
match r.object_next_key()? {
|
|
113
|
+
Some(key) => {
|
|
114
|
+
if find_repeats {
|
|
115
|
+
top.note_key(key);
|
|
116
|
+
}
|
|
117
|
+
true
|
|
118
|
+
}
|
|
119
|
+
None => false,
|
|
120
|
+
}
|
|
121
|
+
} else {
|
|
122
|
+
r.array_next()?
|
|
123
|
+
};
|
|
124
|
+
if more {
|
|
125
|
+
top.index += 1;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
if let Some(key) = stack.pop().and_then(|closed| closed.repeated) {
|
|
129
|
+
return Ok(Some((stack.iter().map(|f| f.index).collect(), key)));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/// The offset of the container at `path`, member/element indices from
|
|
136
|
+
/// the root: walk down skipping earlier siblings, then skip the
|
|
137
|
+
/// container itself, whose slice starts at its bracket.
|
|
138
|
+
fn container_offset(doc: &[u8], path: &[usize], popts: ParseOptions) -> Result<usize, ParseError> {
|
|
139
|
+
let mut bufs = Buffers::new();
|
|
140
|
+
// SAFETY: `doc` is validated UTF-8.
|
|
141
|
+
let mut r = unsafe { Reader::from_utf8_unchecked_with(doc, &mut bufs, popts) };
|
|
142
|
+
// Every index on the path was walked by `scan`, so each step lands
|
|
143
|
+
// on an existing member.
|
|
144
|
+
for &index in path {
|
|
145
|
+
let object = matches!(r.next_node()?, Node::ObjectStart);
|
|
146
|
+
if object {
|
|
147
|
+
r.object_first_key()?;
|
|
148
|
+
} else {
|
|
149
|
+
r.array_first()?;
|
|
150
|
+
}
|
|
151
|
+
for _ in 0..index {
|
|
152
|
+
r.skip_value()?;
|
|
153
|
+
if object {
|
|
154
|
+
r.object_next_key()?;
|
|
155
|
+
} else {
|
|
156
|
+
r.array_next()?;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
let container = r.skip_value()?;
|
|
161
|
+
Ok(span_of(doc, container.as_bytes()).0)
|
|
162
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//! Options-hash reading under json 3's unknown-key rule: every key of an
|
|
2
|
+
//! options hash must be one the entry point reads, else ArgumentError
|
|
3
|
+
//! in json 3's wording ("unknown keyword: foo"). A reader counts the
|
|
4
|
+
//! keys it finds, so a clean hash costs one length compare; only a
|
|
5
|
+
//! shortfall walks the hash to name the keys nobody read.
|
|
6
|
+
|
|
7
|
+
use magnus::r_hash::ForEach;
|
|
8
|
+
use magnus::value::ReprValue;
|
|
9
|
+
use magnus::{Error, RHash, Ruby, Symbol, Value};
|
|
10
|
+
|
|
11
|
+
macro_rules! options {
|
|
12
|
+
($($variant:ident => $name:literal,)*) => {
|
|
13
|
+
/// Every option any entry point reads. The discriminant is the
|
|
14
|
+
/// option's bit in [`OptReader`]'s masks, so a key two decoders
|
|
15
|
+
/// read from one hash (reformat takes parse and generate
|
|
16
|
+
/// options together) counts once.
|
|
17
|
+
#[derive(Clone, Copy)]
|
|
18
|
+
pub(crate) enum Opt {
|
|
19
|
+
$($variant,)*
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/// Ruby option names, indexed by [`Opt`] discriminant.
|
|
23
|
+
const NAMES: &[&str] = &[$($name,)*];
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
options! {
|
|
28
|
+
SymbolizeNames => "symbolize_names",
|
|
29
|
+
Freeze => "freeze",
|
|
30
|
+
MaxNesting => "max_nesting",
|
|
31
|
+
AllowNan => "allow_nan",
|
|
32
|
+
AllowTrailingComma => "allow_trailing_comma",
|
|
33
|
+
AllowDuplicateKey => "allow_duplicate_key",
|
|
34
|
+
ObjectClass => "object_class",
|
|
35
|
+
ArrayClass => "array_class",
|
|
36
|
+
DecimalClass => "decimal_class",
|
|
37
|
+
OnLoad => "on_load",
|
|
38
|
+
CreateAdditions => "create_additions",
|
|
39
|
+
AllowComments => "allow_comments",
|
|
40
|
+
AllowControlCharacters => "allow_control_characters",
|
|
41
|
+
AllowInvalidEscape => "allow_invalid_escape",
|
|
42
|
+
Indent => "indent",
|
|
43
|
+
Space => "space",
|
|
44
|
+
SpaceBefore => "space_before",
|
|
45
|
+
ObjectNl => "object_nl",
|
|
46
|
+
ArrayNl => "array_nl",
|
|
47
|
+
AsciiOnly => "ascii_only",
|
|
48
|
+
ScriptSafe => "script_safe",
|
|
49
|
+
Strict => "strict",
|
|
50
|
+
Depth => "depth",
|
|
51
|
+
BufferInitialLength => "buffer_initial_length",
|
|
52
|
+
SortKeys => "sort_keys",
|
|
53
|
+
AsJson => "as_json",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const _: () = assert!(NAMES.len() <= u64::BITS as usize);
|
|
57
|
+
|
|
58
|
+
impl Opt {
|
|
59
|
+
fn bit(self) -> u64 {
|
|
60
|
+
1 << self as u32
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
pub(crate) fn name(self) -> &'static str {
|
|
64
|
+
NAMES[self as usize]
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pub(crate) struct OptReader<'a> {
|
|
69
|
+
ruby: &'a Ruby,
|
|
70
|
+
hash: RHash,
|
|
71
|
+
/// The hash's key count, read once.
|
|
72
|
+
len: usize,
|
|
73
|
+
/// Options read and present in the hash.
|
|
74
|
+
found: u64,
|
|
75
|
+
/// Options accepted only while falsy (see [`OptReader::tolerate`]).
|
|
76
|
+
tolerated: u64,
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
impl<'a> OptReader<'a> {
|
|
80
|
+
pub(crate) fn new(ruby: &'a Ruby, hash: RHash) -> Self {
|
|
81
|
+
Self {
|
|
82
|
+
ruby,
|
|
83
|
+
hash,
|
|
84
|
+
len: hash.len(),
|
|
85
|
+
found: 0,
|
|
86
|
+
tolerated: 0,
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
pub(crate) fn ruby(&self) -> &'a Ruby {
|
|
91
|
+
self.ruby
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
fn all_found(&self) -> bool {
|
|
95
|
+
self.found.count_ones() as usize == self.len
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/// The value under `opt`'s Symbol key; an explicit nil is present.
|
|
99
|
+
/// Once every key is found, the rest are absent without a lookup
|
|
100
|
+
/// (`{symbolize_names: true}` costs one lookup, not six). An option
|
|
101
|
+
/// read twice (reformat reads a few for both parsing and
|
|
102
|
+
/// generating) is looked up again.
|
|
103
|
+
pub(crate) fn get(&mut self, opt: Opt) -> Option<Value> {
|
|
104
|
+
if self.all_found() && self.found & opt.bit() == 0 {
|
|
105
|
+
return None;
|
|
106
|
+
}
|
|
107
|
+
// An interned StaticSymbol: no String allocation per lookup.
|
|
108
|
+
let value = self.hash.get(self.ruby.sym_new(opt.name()));
|
|
109
|
+
if value.is_some() {
|
|
110
|
+
self.found |= opt.bit();
|
|
111
|
+
}
|
|
112
|
+
value
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
pub(crate) fn truthy(&mut self, opt: Opt) -> bool {
|
|
116
|
+
self.get(opt).is_some_and(|v| v.to_bool())
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/// Accept these json options, which NOSJ does not implement, only
|
|
120
|
+
/// while falsy: their default behavior is NOSJ's, anything else
|
|
121
|
+
/// would be silently ignored. No lookup happens here: such a key can
|
|
122
|
+
/// only be present when the reads leave keys unfound, so
|
|
123
|
+
/// [`OptReader::finish`] checks them on its cold path.
|
|
124
|
+
pub(crate) fn tolerate(&mut self, opts: &[Opt]) {
|
|
125
|
+
for opt in opts {
|
|
126
|
+
self.tolerated |= opt.bit();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Raise for keys no read asked for, and for tolerated options set
|
|
131
|
+
/// to something truthy.
|
|
132
|
+
pub(crate) fn finish(self) -> Result<(), Error> {
|
|
133
|
+
if self.all_found() {
|
|
134
|
+
return Ok(());
|
|
135
|
+
}
|
|
136
|
+
self.leftover_keys()
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
#[cold]
|
|
140
|
+
#[inline(never)]
|
|
141
|
+
fn leftover_keys(self) -> Result<(), Error> {
|
|
142
|
+
let (found, tolerated) = (self.found, self.tolerated);
|
|
143
|
+
let option_of = |key: Value| {
|
|
144
|
+
let name = Symbol::from_value(key)?.name().ok()?;
|
|
145
|
+
NAMES.iter().position(|known| *known == name)
|
|
146
|
+
};
|
|
147
|
+
let mut unknown = Vec::new();
|
|
148
|
+
let mut unsupported = None;
|
|
149
|
+
self.hash.foreach(|key: Value, value: Value| {
|
|
150
|
+
match option_of(key).map(|index| 1u64 << index) {
|
|
151
|
+
Some(bit) if found & bit != 0 => {}
|
|
152
|
+
Some(bit) if tolerated & bit != 0 => {
|
|
153
|
+
if value.to_bool() {
|
|
154
|
+
unsupported = Some(key.to_string());
|
|
155
|
+
return Ok(ForEach::Stop);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
_ => unknown.push(key.to_string()),
|
|
159
|
+
}
|
|
160
|
+
Ok(ForEach::Continue)
|
|
161
|
+
})?;
|
|
162
|
+
let message = match (unsupported, unknown.as_slice()) {
|
|
163
|
+
(Some(name), _) => format!("NOSJ does not support the {name} option"),
|
|
164
|
+
(None, []) => return Ok(()),
|
|
165
|
+
(None, [key]) => format!("unknown keyword: {key}"),
|
|
166
|
+
(None, keys) => format!("unknown keywords: {}", keys.join(", ")),
|
|
167
|
+
};
|
|
168
|
+
Err(Error::new(self.ruby.exception_arg_error(), message))
|
|
169
|
+
}
|
|
170
|
+
}
|