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.
@@ -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, rstring_bytes, str_coderange,
20
- str_enc_index, utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
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>(cfg: &GenConfig, keys: &'k GenKeyCache, k: VALUE) -> Option<&'k [u8]> {
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 mode_cacheable(cfg.mode)
172
- && !is_special_const(k)
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(&mut self, k: VALUE, comma: bool, value: i64) -> Result<(), ()> {
225
- if let Some(bytes) = Self::cached_key_bytes(self.cfg, self.keys, k) {
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: String and Symbol direct, anything else via to_s
254
- /// (the gem's key coercion).
255
- fn emit_key(&mut self, k: VALUE) -> Result<(), ()> {
256
- if !is_special_const(k) {
257
- match unsafe { RB_BUILTIN_TYPE(k) } {
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(protected_to_s(k))?;
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
+ }