nosj 0.1.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.
@@ -0,0 +1,211 @@
1
+ //! Whole-document entry points: `NOSJ.parse` (fused cursor),
2
+ //! `NOSJ.valid?` (null sink), and the unregistered GVL-releasing
3
+ //! indexed parse, plus the shared option decoding, input gating, and
4
+ //! drive-result mapping they all use.
5
+
6
+ use magnus::rb_sys::{AsRawValue, FromRawValue};
7
+ use magnus::{Error, RString, Ruby, Value};
8
+
9
+ use crate::sink::{NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
10
+ use crate::state::{ensure_marked_shadow, PullState, PULL_STATE};
11
+
12
+ #[cold]
13
+ pub(crate) fn err(ruby: &Ruby, msg: String) -> Error {
14
+ Error::new(ruby.exception_runtime_error(), msg)
15
+ }
16
+
17
+ /// Validate that `data` is UTF-8 (or US-ASCII) with intact coderange and
18
+ /// hand out its byte slice.
19
+ pub(crate) fn utf8_input<'a>(ruby: &Ruby, data: &'a RString) -> Result<&'a [u8], Error> {
20
+ let raw = data.as_raw();
21
+ unsafe {
22
+ let enc = rb_sys::rb_enc_get_index(raw);
23
+ if enc != rb_sys::rb_utf8_encindex() && enc != rb_sys::rb_usascii_encindex() {
24
+ return Err(err(ruby, "input must be UTF-8 encoded".into()));
25
+ }
26
+ if rb_sys::rb_enc_str_coderange(raw)
27
+ == rb_sys::ruby_coderange_type::RUBY_ENC_CODERANGE_BROKEN as std::os::raw::c_int
28
+ {
29
+ return Err(err(ruby, "input is not valid UTF-8".into()));
30
+ }
31
+ Ok(data.as_slice())
32
+ }
33
+ }
34
+
35
+ /// JSON.parse-compatible options, decoded once from the Ruby hash and
36
+ /// shared by every entry point that materializes (or validates) values.
37
+ #[derive(Clone, Copy)]
38
+ pub(crate) struct ParseNativeOpts {
39
+ pub(crate) symbolize: bool,
40
+ pub(crate) freeze: bool,
41
+ pub(crate) max_nesting: usize,
42
+ pub(crate) popts: nosj::ParseOptions,
43
+ }
44
+
45
+ impl Default for ParseNativeOpts {
46
+ fn default() -> Self {
47
+ Self {
48
+ symbolize: false,
49
+ freeze: false,
50
+ max_nesting: MAX_NESTING,
51
+ popts: nosj::ParseOptions::default(),
52
+ }
53
+ }
54
+ }
55
+
56
+ /// Decode a JSON.parse-compatible options hash: symbolize_names, freeze,
57
+ /// max_nesting, allow_nan, allow_trailing_comma. Unsupported gem options
58
+ /// (object_class, array_class, decimal_class, create_additions) raise.
59
+ pub(crate) fn parse_native_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeOpts, Error> {
60
+ use magnus::value::ReprValue;
61
+ use magnus::RHash;
62
+
63
+ let mut out = ParseNativeOpts::default();
64
+ if opts.is_nil() {
65
+ return Ok(out);
66
+ }
67
+ let h = RHash::from_value(opts)
68
+ .ok_or_else(|| Error::new(ruby.exception_arg_error(), "options must be a Hash"))?;
69
+
70
+ let truthy = |name: &str| -> bool {
71
+ h.get(ruby.to_symbol(name))
72
+ .is_some_and(|v: Value| v.to_bool())
73
+ };
74
+
75
+ out.symbolize = truthy("symbolize_names");
76
+ out.freeze = truthy("freeze");
77
+ out.popts.allow_nan = truthy("allow_nan");
78
+ out.popts.allow_trailing_comma = truthy("allow_trailing_comma");
79
+
80
+ if let Some(mn) = h.get(ruby.to_symbol("max_nesting")) {
81
+ out.max_nesting =
82
+ if mn.is_nil() || mn.to_bool() && magnus::Integer::from_value(mn).is_none() {
83
+ MAX_NESTING // nil / true: gem default
84
+ } else if !mn.to_bool() {
85
+ usize::MAX // false: unlimited
86
+ } else {
87
+ magnus::Integer::from_value(mn)
88
+ .and_then(|i| i.to_u64().ok())
89
+ .map_or(MAX_NESTING, |n| n as usize)
90
+ };
91
+ }
92
+
93
+ for unsupported in [
94
+ "object_class",
95
+ "array_class",
96
+ "decimal_class",
97
+ "create_additions",
98
+ ] {
99
+ if truthy(unsupported) {
100
+ return Err(Error::new(
101
+ ruby.exception_arg_error(),
102
+ format!("NOSJ.parse does not support the {unsupported} option"),
103
+ ));
104
+ }
105
+ }
106
+ Ok(out)
107
+ }
108
+
109
+ /// Pop the root value off the sink stack, or map a drive failure onto
110
+ /// the gem's exceptions. Shared by every driver.
111
+ fn finish_drive(
112
+ ruby: &Ruby,
113
+ result: Result<(), nosj::DriveError<SinkAbort>>,
114
+ stack: &mut Vec<rb_sys::VALUE>,
115
+ max_nesting: usize,
116
+ ) -> Result<Value, Error> {
117
+ match result {
118
+ Ok(()) => {
119
+ let raw = stack
120
+ .pop()
121
+ .unwrap_or(rb_sys::special_consts::Qnil as rb_sys::VALUE);
122
+ Ok(unsafe { Value::from_raw(raw) })
123
+ }
124
+ Err(nosj::DriveError::Sink(SinkAbort::Overflow)) => {
125
+ Err(err(ruby, "document too large".into()))
126
+ }
127
+ Err(nosj::DriveError::Sink(SinkAbort::BadBigint)) => {
128
+ Err(err(ruby, "invalid bignum".into()))
129
+ }
130
+ Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(err(
131
+ ruby,
132
+ format!("nesting of {} is too deep", max_nesting.saturating_add(1)),
133
+ )),
134
+ Err(nosj::DriveError::Parse(e)) => Err(err(ruby, e.to_string())),
135
+ }
136
+ }
137
+
138
+ /// Drive the fused cursor over `input`, building Ruby values through the
139
+ /// shared thread-local sink machinery. `input` must be valid UTF-8 (see
140
+ /// [`utf8_input`]).
141
+ pub(crate) fn materialize(ruby: &Ruby, input: &[u8], o: &ParseNativeOpts) -> Result<Value, Error> {
142
+ PULL_STATE.with(|cell| {
143
+ let mut state = cell.borrow_mut();
144
+ ensure_marked_shadow(&mut state.vstack);
145
+ ensure_marked_shadow(&mut state.key_shadow);
146
+
147
+ let PullState {
148
+ bufs,
149
+ keys,
150
+ sym_keys,
151
+ vstack,
152
+ key_shadow,
153
+ ..
154
+ } = &mut *state;
155
+ let stack = &mut vstack.as_mut().unwrap().values;
156
+ stack.clear();
157
+
158
+ let mut sink = RubyValueSink {
159
+ stack,
160
+ keys: if o.symbolize { sym_keys } else { keys },
161
+ key_shadow: key_shadow.as_deref_mut().unwrap(),
162
+ depth: 0,
163
+ symbolize: o.symbolize,
164
+ freeze: o.freeze,
165
+ max_nesting: o.max_nesting,
166
+ };
167
+
168
+ // Safety: callers verified UTF-8 (coderange or nosj slice).
169
+ let result = unsafe { nosj::parse_utf8_unchecked_with(input, bufs, &mut sink, o.popts) };
170
+ finish_drive(ruby, result, sink.stack, o.max_nesting)
171
+ })
172
+ }
173
+
174
+ /// JSON.parse-compatible entry (see [`parse_native_opts`] for options).
175
+ pub fn parse_native(
176
+ ruby: &Ruby,
177
+ _rb_self: Value,
178
+ data: RString,
179
+ opts: Value,
180
+ ) -> Result<Value, Error> {
181
+ let o = parse_native_opts(ruby, opts)?;
182
+ let input = utf8_input(ruby, &data)?;
183
+ materialize(ruby, input, &o)
184
+ }
185
+
186
+ /// `NOSJ.valid?(source, opts)` returns true iff `NOSJ.parse` would
187
+ /// succeed under the same options. Parse refusals (malformed JSON, bad
188
+ /// encoding, too-deep nesting) return false; option and argument-type
189
+ /// errors still raise exactly like `parse`.
190
+ pub fn valid_native(
191
+ ruby: &Ruby,
192
+ _rb_self: Value,
193
+ data: RString,
194
+ opts: Value,
195
+ ) -> Result<bool, Error> {
196
+ let o = parse_native_opts(ruby, opts)?;
197
+ let Ok(input) = utf8_input(ruby, &data) else {
198
+ return Ok(false);
199
+ };
200
+ let ok = PULL_STATE.with(|cell| {
201
+ let mut state = cell.borrow_mut();
202
+ let mut sink = NullSink {
203
+ depth: 0,
204
+ max_nesting: o.max_nesting,
205
+ };
206
+ // Safety: coderange verified by utf8_input.
207
+ unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
208
+ .is_ok()
209
+ });
210
+ Ok(ok)
211
+ }
@@ -0,0 +1,190 @@
1
+ //! Partial parsing: `NOSJ.dig`, `NOSJ.at_pointer`, and their batch
2
+ //! forms. Pointers resolve against the raw document through the nosj
3
+ //! crate's cursor-mode skipper; only the matched subtree materializes,
4
+ //! through the same sink machinery as a full parse.
5
+
6
+ use magnus::{Error, RString, Ruby, Value};
7
+
8
+ use crate::parse::{err, materialize, parse_native_opts, utf8_input, ParseNativeOpts};
9
+ use crate::state::PULL_STATE;
10
+
11
+ /// Resolve one JSON Pointer against `data`, materializing the matched
12
+ /// subtree; `nil` when the pointer misses.
13
+ fn at_pointer_impl(
14
+ ruby: &Ruby,
15
+ data: RString,
16
+ pointer: &str,
17
+ o: &ParseNativeOpts,
18
+ ) -> Result<Value, Error> {
19
+ use magnus::value::ReprValue;
20
+ let input = utf8_input(ruby, &data)?;
21
+ // Resolve first (one PULL_STATE borrow), then materialize (a fresh
22
+ // borrow); the resolved slice borrows `input`, not the buffers.
23
+ let resolved = PULL_STATE.with(|cell| {
24
+ let mut state = cell.borrow_mut();
25
+ // Safety: coderange verified above.
26
+ unsafe { nosj::pointer_utf8_unchecked(input, pointer, &mut state.bufs) }
27
+ });
28
+ match resolved {
29
+ Ok(None) => Ok(ruby.qnil().as_value()),
30
+ Ok(Some(slice)) => materialize(ruby, slice.as_bytes(), o),
31
+ Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
32
+ Err(Error::new(ruby.exception_arg_error(), e.to_string()))
33
+ }
34
+ Err(e) => Err(err(ruby, e.to_string())),
35
+ }
36
+ }
37
+
38
+ /// `NOSJ.at_pointer(source, "/a/b/0", opts)`: pointer lookup; the
39
+ /// matched subtree materializes under the same options as `parse`.
40
+ pub fn at_pointer_native(
41
+ ruby: &Ruby,
42
+ _rb_self: Value,
43
+ data: RString,
44
+ pointer: RString,
45
+ opts: Value,
46
+ ) -> Result<Value, Error> {
47
+ let o = parse_native_opts(ruby, opts)?;
48
+ let ptr = pointer.to_string()?;
49
+ at_pointer_impl(ruby, data, &ptr, &o)
50
+ }
51
+
52
+ /// Convert one dig path (String/Symbol keys, Integer indices) into a
53
+ /// JSON Pointer, `~`/`/` escaped. `None` = a negative index: no pointer
54
+ /// equivalent, the path resolves to `nil` by definition.
55
+ fn path_to_pointer(ruby: &Ruby, path: magnus::RArray) -> Result<Option<String>, Error> {
56
+ fn push_escaped_token(ptr: &mut String, key: &str) {
57
+ ptr.push('/');
58
+ for c in key.chars() {
59
+ match c {
60
+ '~' => ptr.push_str("~0"),
61
+ '/' => ptr.push_str("~1"),
62
+ c => ptr.push(c),
63
+ }
64
+ }
65
+ }
66
+
67
+ let mut ptr = String::new();
68
+ for i in 0..path.len() {
69
+ let item: Value = path.entry(i as isize)?;
70
+ if let Some(int) = magnus::Integer::from_value(item) {
71
+ let idx = int.to_i64()?;
72
+ if idx < 0 {
73
+ return Ok(None);
74
+ }
75
+ ptr.push('/');
76
+ ptr.push_str(&idx.to_string());
77
+ } else if let Some(s) = RString::from_value(item) {
78
+ push_escaped_token(&mut ptr, &s.to_string()?);
79
+ } else if let Some(sym) = magnus::Symbol::from_value(item) {
80
+ push_escaped_token(&mut ptr, &sym.name()?);
81
+ } else {
82
+ return Err(Error::new(
83
+ ruby.exception_arg_error(),
84
+ "path elements must be Strings, Symbols, or Integers",
85
+ ));
86
+ }
87
+ }
88
+ Ok(Some(ptr))
89
+ }
90
+
91
+ /// `NOSJ.dig(source, *path)`: Hash#dig-shaped partial parsing.
92
+ /// Negative indices resolve to `nil` (JSON Pointer has no equivalent).
93
+ pub fn dig_native(
94
+ ruby: &Ruby,
95
+ _rb_self: Value,
96
+ data: RString,
97
+ path: magnus::RArray,
98
+ ) -> Result<Value, Error> {
99
+ use magnus::value::ReprValue;
100
+ match path_to_pointer(ruby, path)? {
101
+ Some(ptr) => at_pointer_impl(ruby, data, &ptr, &ParseNativeOpts::default()),
102
+ None => Ok(ruby.qnil().as_value()),
103
+ }
104
+ }
105
+
106
+ /// Resolve many pointers in ONE forward pass (`nosj::pointers`: the
107
+ /// walk descends only into subtrees some pointer still needs, so a batch
108
+ /// costs about its single deepest query), then materialize each hit.
109
+ /// `None` entries (negative dig indices) come back as `nil` without ever
110
+ /// reaching the resolver.
111
+ fn at_pointers_impl(
112
+ ruby: &Ruby,
113
+ data: RString,
114
+ pointers: &[Option<String>],
115
+ o: &ParseNativeOpts,
116
+ ) -> Result<Value, Error> {
117
+ use magnus::value::ReprValue;
118
+
119
+ let input = utf8_input(ruby, &data)?;
120
+ let live: Vec<&str> = pointers.iter().flatten().map(String::as_str).collect();
121
+
122
+ // Resolve first (one PULL_STATE borrow); the resolved slices borrow
123
+ // `input`, not the buffers, so materializing can re-borrow freely.
124
+ let resolved = PULL_STATE.with(|cell| {
125
+ let mut state = cell.borrow_mut();
126
+ // Safety: coderange verified by utf8_input.
127
+ unsafe { nosj::pointers_utf8_unchecked(input, &live, &mut state.bufs) }
128
+ });
129
+ let mut hits = match resolved {
130
+ Ok(hits) => hits.into_iter(),
131
+ Err(e) if matches!(e.kind, nosj::ErrorKind::InvalidPointer) => {
132
+ return Err(Error::new(ruby.exception_arg_error(), e.to_string()));
133
+ }
134
+ Err(e) => return Err(err(ruby, e.to_string())),
135
+ };
136
+
137
+ let out = ruby.ary_new_capa(pointers.len());
138
+ for ptr in pointers {
139
+ let hit = if ptr.is_some() {
140
+ hits.next().flatten()
141
+ } else {
142
+ None
143
+ };
144
+ match hit {
145
+ Some(slice) => out.push(materialize(ruby, slice.as_bytes(), o)?)?,
146
+ None => out.push(ruby.qnil())?,
147
+ }
148
+ }
149
+ Ok(out.as_value())
150
+ }
151
+
152
+ /// `NOSJ.at_pointers(source, pointers, opts)`: batch pointer lookup,
153
+ /// positionally aligned results.
154
+ pub fn at_pointers_native(
155
+ ruby: &Ruby,
156
+ _rb_self: Value,
157
+ data: RString,
158
+ pointers: magnus::RArray,
159
+ opts: Value,
160
+ ) -> Result<Value, Error> {
161
+ let o = parse_native_opts(ruby, opts)?;
162
+ let mut ptrs: Vec<Option<String>> = Vec::with_capacity(pointers.len());
163
+ for i in 0..pointers.len() {
164
+ let item: Value = pointers.entry(i as isize)?;
165
+ let s = RString::from_value(item)
166
+ .ok_or_else(|| Error::new(ruby.exception_arg_error(), "pointers must be Strings"))?;
167
+ ptrs.push(Some(s.to_string()?));
168
+ }
169
+ at_pointers_impl(ruby, data, &ptrs, &o)
170
+ }
171
+
172
+ /// `NOSJ.dig_many(source, paths, opts)`: batch dig, one resolver
173
+ /// pass for all paths.
174
+ pub fn dig_many_native(
175
+ ruby: &Ruby,
176
+ _rb_self: Value,
177
+ data: RString,
178
+ paths: magnus::RArray,
179
+ opts: Value,
180
+ ) -> Result<Value, Error> {
181
+ let o = parse_native_opts(ruby, opts)?;
182
+ let mut ptrs: Vec<Option<String>> = Vec::with_capacity(paths.len());
183
+ for i in 0..paths.len() {
184
+ let item: Value = paths.entry(i as isize)?;
185
+ let path = magnus::RArray::from_value(item)
186
+ .ok_or_else(|| Error::new(ruby.exception_arg_error(), "each path must be an Array"))?;
187
+ ptrs.push(path_to_pointer(ruby, path)?);
188
+ }
189
+ at_pointers_impl(ruby, data, &ptrs, &o)
190
+ }