nosj 0.4.0 → 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.
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;
@@ -24,22 +24,31 @@ fn blank(line: &[u8]) -> bool {
24
24
  /// Yield one parsed value per non-blank line. Each line parses through
25
25
  /// the shared sink machinery against the FULL source, so a malformed
26
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> {
27
+ /// NDJSON line number. No slice is held across a yield: the block runs
28
+ /// arbitrary Ruby (even deduplicating a frozen source swaps its buffer,
29
+ /// see `lazy::DocBytes`), so `source` hands out the bytes afresh for
30
+ /// every line, with identical content and so identical offsets.
31
+ fn walk_lines<'s>(
32
+ ruby: &Ruby,
33
+ source: impl Fn() -> &'s [u8],
34
+ o: &ParseNativeOpts,
35
+ ) -> Result<(), Error> {
30
36
  let mut pos = 0;
31
- while pos < source.len() {
32
- let line_end = source[pos..]
37
+ loop {
38
+ let bytes = source();
39
+ if pos >= bytes.len() {
40
+ return Ok(());
41
+ }
42
+ let line_end = bytes[pos..]
33
43
  .iter()
34
44
  .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)?;
45
+ .map_or(bytes.len(), |p| pos + p);
46
+ if !blank(&bytes[pos..line_end]) {
47
+ let value = materialize_at(ruby, bytes, pos, line_end, o)?;
38
48
  let _: Value = ruby.yield_value(value)?;
39
49
  }
40
50
  pos = line_end + 1;
41
51
  }
42
- Ok(())
43
52
  }
44
53
 
45
54
  /// `NOSJ.each_line(source, opts) { |value| }`: the Ruby wrapper
@@ -53,15 +62,16 @@ pub fn each_line_native(
53
62
  let o = parse_native_opts(ruby, opts)?;
54
63
  let input = utf8_input(ruby, &data)?;
55
64
  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)?;
65
+ // A frozen source keeps its content (never its buffer, see
66
+ // walk_lines), so it is re-read per line, zero-copy.
67
+ // SAFETY: validated UTF-8 above; `data` lives on this frame.
68
+ walk_lines(ruby, || unsafe { data.as_slice() }, &o)?;
59
69
  } 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.
70
+ // The block could rewrite an unfrozen source mid-iteration;
71
+ // walk a private copy. Same policy as NOSJ.lazy: pass a frozen
72
+ // string for zero-copy.
63
73
  let owned = input.to_vec();
64
- walk_lines(ruby, &owned, &o)?;
74
+ walk_lines(ruby, || &owned, &o)?;
65
75
  }
66
76
  Ok(ruby.qnil().as_value())
67
77
  }
@@ -84,7 +94,7 @@ pub fn each_line_file_native(
84
94
  return Ok(ruby.qnil().as_value());
85
95
  }
86
96
  with_mapped_file(ruby, &p, |map| {
87
- walk_lines(ruby, &map, &o)?;
97
+ walk_lines(ruby, || &map, &o)?;
88
98
  Ok(ruby.qnil().as_value())
89
99
  })
90
100
  }
@@ -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
+ }