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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +105 -0
- data/Cargo.lock +17 -17
- data/README.md +32 -15
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- data/ext/nosj/fuzz/src/prelude.rb +20 -44
- data/ext/nosj/src/files.rs +21 -20
- data/ext/nosj/src/gen/errors.rs +12 -10
- data/ext/nosj/src/gen/mod.rs +8 -10
- data/ext/nosj/src/gen/opts.rs +76 -141
- data/ext/nosj/src/gen/ruby.rs +50 -22
- data/ext/nosj/src/gen/walker.rs +219 -102
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lib.rs +4 -0
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/locate.rs +162 -0
- data/ext/nosj/src/opt_reader.rs +170 -0
- data/ext/nosj/src/parse.rs +170 -99
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +83 -140
- data/ext/nosj/src/sink.rs +193 -60
- data/ext/nosj/src/state.rs +65 -14
- data/ext/nosj/src/stats.rs +35 -33
- data/lib/nosj/json.rb +105 -39
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +36 -22
- data/sig/nosj.rbs +4 -3
- metadata +4 -2
data/ext/nosj/src/parse.rs
CHANGED
|
@@ -6,9 +6,11 @@
|
|
|
6
6
|
use magnus::rb_sys::{AsRawValue, FromRawValue};
|
|
7
7
|
use magnus::{Error, RString, Ruby, Value};
|
|
8
8
|
|
|
9
|
-
use crate::errors::{nesting_error, parser_error, parser_error_at};
|
|
10
|
-
use crate::
|
|
11
|
-
use crate::
|
|
9
|
+
use crate::errors::{nesting_error, nosj_exception, parser_error, parser_error_at};
|
|
10
|
+
use crate::locate::Repeat;
|
|
11
|
+
use crate::opt_reader::{Opt, OptReader};
|
|
12
|
+
use crate::sink::{DupKeys, NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
|
|
13
|
+
use crate::state::{ensure_marked_shadow, with_pull_state, PullState};
|
|
12
14
|
|
|
13
15
|
pub(crate) use crate::errors::parser_error as err;
|
|
14
16
|
|
|
@@ -22,6 +24,12 @@ pub(crate) fn span_of(source: &[u8], sub: &[u8]) -> (usize, usize) {
|
|
|
22
24
|
|
|
23
25
|
/// Validate that `data` is UTF-8 (or US-ASCII) with intact coderange and
|
|
24
26
|
/// hand out its byte slice.
|
|
27
|
+
///
|
|
28
|
+
/// The slice borrows the string's buffer, which Ruby may reallocate or
|
|
29
|
+
/// swap (even for a frozen string: see `lazy::DocBytes`), so it must
|
|
30
|
+
/// not be held across anything that can run Ruby code: user callbacks,
|
|
31
|
+
/// yields, or option decoding (`to_int` and friends). Decode options
|
|
32
|
+
/// first; re-borrow after callbacks.
|
|
25
33
|
pub(crate) fn utf8_input<'a>(ruby: &Ruby, data: &'a RString) -> Result<&'a [u8], Error> {
|
|
26
34
|
let raw = data.as_raw();
|
|
27
35
|
unsafe {
|
|
@@ -45,6 +53,8 @@ pub(crate) struct ParseNativeOpts {
|
|
|
45
53
|
pub(crate) symbolize: bool,
|
|
46
54
|
pub(crate) freeze: bool,
|
|
47
55
|
pub(crate) max_nesting: usize,
|
|
56
|
+
/// json 3 default: a repeated key raises.
|
|
57
|
+
pub(crate) allow_duplicate_key: bool,
|
|
48
58
|
pub(crate) popts: nosj::ParseOptions,
|
|
49
59
|
}
|
|
50
60
|
|
|
@@ -54,113 +64,173 @@ impl Default for ParseNativeOpts {
|
|
|
54
64
|
symbolize: false,
|
|
55
65
|
freeze: false,
|
|
56
66
|
max_nesting: MAX_NESTING,
|
|
67
|
+
allow_duplicate_key: false,
|
|
57
68
|
popts: nosj::ParseOptions::default(),
|
|
58
69
|
}
|
|
59
70
|
}
|
|
60
71
|
}
|
|
61
72
|
|
|
62
|
-
/// Decode a JSON.parse-compatible options hash
|
|
63
|
-
///
|
|
64
|
-
/// (object_class, array_class, decimal_class, create_additions) raise.
|
|
73
|
+
/// Decode a JSON.parse-compatible options hash (see [`read_parse_opts`]);
|
|
74
|
+
/// keys it does not read raise ArgumentError, like json 3.
|
|
65
75
|
pub(crate) fn parse_native_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeOpts, Error> {
|
|
66
|
-
|
|
67
|
-
|
|
76
|
+
let Some(h) = options_hash(ruby, opts)? else {
|
|
77
|
+
return Ok(ParseNativeOpts::default());
|
|
78
|
+
};
|
|
79
|
+
let mut reader = OptReader::new(ruby, h);
|
|
80
|
+
let out = read_parse_opts(&mut reader)?;
|
|
81
|
+
reader.finish()?;
|
|
82
|
+
Ok(out)
|
|
83
|
+
}
|
|
68
84
|
|
|
69
|
-
|
|
85
|
+
/// The non-empty options Hash in `opts`, or None for nil and `{}`
|
|
86
|
+
/// (json 3's keyword-only parse hands the drop-in an empty hash per
|
|
87
|
+
/// call).
|
|
88
|
+
pub(crate) fn options_hash(ruby: &Ruby, opts: Value) -> Result<Option<magnus::RHash>, Error> {
|
|
89
|
+
use magnus::value::ReprValue;
|
|
70
90
|
if opts.is_nil() {
|
|
71
|
-
return Ok(
|
|
91
|
+
return Ok(None);
|
|
72
92
|
}
|
|
73
|
-
let h = RHash::from_value(opts)
|
|
93
|
+
let h = magnus::RHash::from_value(opts)
|
|
74
94
|
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "options must be a Hash"))?;
|
|
95
|
+
Ok((!h.is_empty()).then_some(h))
|
|
96
|
+
}
|
|
75
97
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
98
|
+
/// Read symbolize_names, freeze, max_nesting, allow_nan,
|
|
99
|
+
/// allow_trailing_comma and allow_duplicate_key. The json options NOSJ
|
|
100
|
+
/// does not implement (object_class, array_class, decimal_class,
|
|
101
|
+
/// on_load, create_additions, allow_comments, allow_control_characters,
|
|
102
|
+
/// allow_invalid_escape) raise unless falsy.
|
|
103
|
+
pub(crate) fn read_parse_opts(r: &mut OptReader) -> Result<ParseNativeOpts, Error> {
|
|
104
|
+
let mut out = ParseNativeOpts {
|
|
105
|
+
symbolize: r.truthy(Opt::SymbolizeNames),
|
|
106
|
+
freeze: r.truthy(Opt::Freeze),
|
|
107
|
+
allow_duplicate_key: r.truthy(Opt::AllowDuplicateKey),
|
|
108
|
+
..ParseNativeOpts::default()
|
|
79
109
|
};
|
|
110
|
+
out.popts.allow_nan = r.truthy(Opt::AllowNan);
|
|
111
|
+
out.popts.allow_trailing_comma = r.truthy(Opt::AllowTrailingComma);
|
|
80
112
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
out.popts.allow_nan = truthy("allow_nan");
|
|
84
|
-
out.popts.allow_trailing_comma = truthy("allow_trailing_comma");
|
|
85
|
-
|
|
86
|
-
if let Some(mn) = h.get(ruby.to_symbol("max_nesting")) {
|
|
87
|
-
out.max_nesting =
|
|
88
|
-
if mn.is_nil() || mn.to_bool() && magnus::Integer::from_value(mn).is_none() {
|
|
89
|
-
MAX_NESTING // nil / true: gem default
|
|
90
|
-
} else if !mn.to_bool() {
|
|
91
|
-
usize::MAX // false: unlimited
|
|
92
|
-
} else {
|
|
93
|
-
magnus::Integer::from_value(mn)
|
|
94
|
-
.and_then(|i| i.to_u64().ok())
|
|
95
|
-
.map_or(MAX_NESTING, |n| n as usize)
|
|
96
|
-
};
|
|
113
|
+
if let Some(mn) = r.get(Opt::MaxNesting) {
|
|
114
|
+
out.max_nesting = max_nesting_of(mn);
|
|
97
115
|
}
|
|
98
116
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
));
|
|
110
|
-
}
|
|
111
|
-
}
|
|
117
|
+
r.tolerate(&[
|
|
118
|
+
Opt::ObjectClass,
|
|
119
|
+
Opt::ArrayClass,
|
|
120
|
+
Opt::DecimalClass,
|
|
121
|
+
Opt::OnLoad,
|
|
122
|
+
Opt::CreateAdditions,
|
|
123
|
+
Opt::AllowComments,
|
|
124
|
+
Opt::AllowControlCharacters,
|
|
125
|
+
Opt::AllowInvalidEscape,
|
|
126
|
+
]);
|
|
112
127
|
Ok(out)
|
|
113
128
|
}
|
|
114
129
|
|
|
115
|
-
///
|
|
116
|
-
///
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
130
|
+
/// A given `max_nesting` value as a limit: nil or true keep the gem's
|
|
131
|
+
/// default, false is unlimited, an Integer is the limit.
|
|
132
|
+
pub(crate) fn max_nesting_of(value: Value) -> usize {
|
|
133
|
+
use magnus::value::ReprValue;
|
|
134
|
+
if value.is_nil() || value.to_bool() && magnus::Integer::from_value(value).is_none() {
|
|
135
|
+
MAX_NESTING
|
|
136
|
+
} else if !value.to_bool() {
|
|
137
|
+
usize::MAX
|
|
138
|
+
} else {
|
|
139
|
+
magnus::Integer::from_value(value)
|
|
140
|
+
.and_then(|i| i.to_u64().ok())
|
|
141
|
+
.map_or(MAX_NESTING, |n| n as usize)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
pub(crate) type DriveResult = Result<(), nosj::DriveError<SinkAbort>>;
|
|
146
|
+
|
|
147
|
+
/// A drive's failure as the gem's exception; shared by every driver.
|
|
148
|
+
/// `source[start..end]` is what was driven, within the full document,
|
|
149
|
+
/// so ParserError positions stay absolute when a subtree slice was
|
|
150
|
+
/// parsed. Sinks see no offsets, so their two document refusals are
|
|
151
|
+
/// positioned by a cold-path re-walk (`crate::locate`).
|
|
152
|
+
pub(crate) fn drive_error(
|
|
120
153
|
ruby: &Ruby,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
max_nesting: usize,
|
|
154
|
+
failure: nosj::DriveError<SinkAbort>,
|
|
155
|
+
o: &ParseNativeOpts,
|
|
124
156
|
source: &[u8],
|
|
125
|
-
|
|
126
|
-
) ->
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
Err(parser_error(ruby, "document too large".into()))
|
|
136
|
-
}
|
|
137
|
-
Err(nosj::DriveError::Sink(SinkAbort::BadBigint)) => {
|
|
138
|
-
Err(parser_error(ruby, "invalid bignum".into()))
|
|
139
|
-
}
|
|
140
|
-
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
|
|
141
|
-
ruby,
|
|
142
|
-
format!("nesting of {} is too deep", max_nesting.saturating_add(1)),
|
|
143
|
-
)),
|
|
144
|
-
// Raised only by the reformat pipe's sink, which never drives
|
|
145
|
-
// through here; the match must stay total.
|
|
146
|
-
Err(nosj::DriveError::Sink(SinkAbort::BrokenUtf8Output)) => Err(parser_error(
|
|
147
|
-
ruby,
|
|
148
|
-
"source sequence is illegal/malformed utf-8".into(),
|
|
149
|
-
)),
|
|
150
|
-
// Also reformat-pipe-only, kept total for the same reason.
|
|
151
|
-
Err(nosj::DriveError::Sink(SinkAbort::NonFiniteFloat(spelling))) => Err(parser_error(
|
|
157
|
+
(start, end): (usize, usize),
|
|
158
|
+
) -> Error {
|
|
159
|
+
use magnus::value::ReprValue;
|
|
160
|
+
use nosj::DriveError::{Parse, Sink};
|
|
161
|
+
|
|
162
|
+
let driven = &source[start..end];
|
|
163
|
+
match failure {
|
|
164
|
+
Sink(SinkAbort::Overflow) => parser_error(ruby, "document too large".into()),
|
|
165
|
+
Sink(SinkAbort::BadBigint) => parser_error(ruby, "invalid bignum".into()),
|
|
166
|
+
Sink(SinkAbort::TooDeep) => nesting_error(
|
|
152
167
|
ruby,
|
|
168
|
+
format!("nesting of {} is too deep", o.max_nesting.saturating_add(1)),
|
|
169
|
+
),
|
|
170
|
+
// Positioned like json 3's: at the `{` of the object repeating
|
|
171
|
+
// the key.
|
|
172
|
+
Sink(SinkAbort::DuplicateKey) => match crate::locate::duplicate_key(driven, o.popts) {
|
|
173
|
+
Repeat::Found { at, key } => parser_error_at(
|
|
174
|
+
ruby,
|
|
175
|
+
source,
|
|
176
|
+
start + at,
|
|
177
|
+
format!(
|
|
178
|
+
"duplicate key {} at byte {at}",
|
|
179
|
+
ruby.str_new(&key).inspect()
|
|
180
|
+
),
|
|
181
|
+
),
|
|
182
|
+
Repeat::Absent | Repeat::Undecided => parser_error(ruby, "duplicate key".into()),
|
|
183
|
+
},
|
|
184
|
+
Sink(SinkAbort::LoneSurrogate) => match crate::locate::first_walk_error(driven, o.popts) {
|
|
185
|
+
Some(e) => parser_error_at(ruby, source, start + e.offset, e.to_string()),
|
|
186
|
+
None => parser_error(ruby, "lone UTF-16 surrogate".into()),
|
|
187
|
+
},
|
|
188
|
+
// Only the reformat pipe raises it: generation refuses a
|
|
189
|
+
// non-finite float, with the gem's GeneratorError.
|
|
190
|
+
Sink(SinkAbort::NonFiniteFloat(spelling)) => Error::new(
|
|
191
|
+
nosj_exception(ruby, "GeneratorError"),
|
|
153
192
|
format!("{spelling} not allowed in JSON"),
|
|
154
|
-
)
|
|
155
|
-
|
|
156
|
-
ruby,
|
|
157
|
-
source,
|
|
158
|
-
base + e.offset,
|
|
159
|
-
e.to_string(),
|
|
160
|
-
)),
|
|
193
|
+
),
|
|
194
|
+
Parse(e) => parser_error_at(ruby, source, start + e.offset, e.to_string()),
|
|
161
195
|
}
|
|
162
196
|
}
|
|
163
197
|
|
|
198
|
+
/// Drive a sink that builds no Hash (`valid?`, the reformat pipe) under
|
|
199
|
+
/// json 3's duplicate-key rule. `drive(check_dups)` runs the parse. With
|
|
200
|
+
/// the check on, `DupKeys` compares key fingerprints, so a refusal is
|
|
201
|
+
/// confirmed exactly, and a collision (no object really repeats a key)
|
|
202
|
+
/// drives again with the check off.
|
|
203
|
+
pub(crate) fn drive_hashless(
|
|
204
|
+
input: &[u8],
|
|
205
|
+
o: &ParseNativeOpts,
|
|
206
|
+
mut drive: impl FnMut(bool) -> DriveResult,
|
|
207
|
+
) -> DriveResult {
|
|
208
|
+
let result = drive(!o.allow_duplicate_key);
|
|
209
|
+
if matches!(result, Err(nosj::DriveError::Sink(SinkAbort::DuplicateKey)))
|
|
210
|
+
&& matches!(crate::locate::duplicate_key(input, o.popts), Repeat::Absent)
|
|
211
|
+
{
|
|
212
|
+
return drive(false);
|
|
213
|
+
}
|
|
214
|
+
result
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Pop the root value off the sink stack, or raise the drive's failure
|
|
218
|
+
/// (see [`drive_error`]).
|
|
219
|
+
fn finish_drive(
|
|
220
|
+
ruby: &Ruby,
|
|
221
|
+
result: DriveResult,
|
|
222
|
+
stack: &mut Vec<rb_sys::VALUE>,
|
|
223
|
+
o: &ParseNativeOpts,
|
|
224
|
+
source: &[u8],
|
|
225
|
+
span: (usize, usize),
|
|
226
|
+
) -> Result<Value, Error> {
|
|
227
|
+
result.map_err(|failure| drive_error(ruby, failure, o, source, span))?;
|
|
228
|
+
let raw = stack
|
|
229
|
+
.pop()
|
|
230
|
+
.unwrap_or(rb_sys::special_consts::Qnil as rb_sys::VALUE);
|
|
231
|
+
Ok(unsafe { Value::from_raw(raw) })
|
|
232
|
+
}
|
|
233
|
+
|
|
164
234
|
/// Drive the fused cursor over the whole of `source`. See
|
|
165
235
|
/// [`materialize_at`].
|
|
166
236
|
pub(crate) fn materialize(ruby: &Ruby, source: &[u8], o: &ParseNativeOpts) -> Result<Value, Error> {
|
|
@@ -178,8 +248,7 @@ pub(crate) fn materialize_at(
|
|
|
178
248
|
end: usize,
|
|
179
249
|
o: &ParseNativeOpts,
|
|
180
250
|
) -> Result<Value, Error> {
|
|
181
|
-
|
|
182
|
-
let mut state = cell.borrow_mut();
|
|
251
|
+
with_pull_state(|state| {
|
|
183
252
|
ensure_marked_shadow(&mut state.vstack);
|
|
184
253
|
ensure_marked_shadow(&mut state.key_shadow);
|
|
185
254
|
|
|
@@ -190,7 +259,7 @@ pub(crate) fn materialize_at(
|
|
|
190
259
|
vstack,
|
|
191
260
|
key_shadow,
|
|
192
261
|
..
|
|
193
|
-
} =
|
|
262
|
+
} = state;
|
|
194
263
|
let stack = &mut vstack.as_mut().unwrap().values;
|
|
195
264
|
stack.clear();
|
|
196
265
|
|
|
@@ -202,13 +271,14 @@ pub(crate) fn materialize_at(
|
|
|
202
271
|
symbolize: o.symbolize,
|
|
203
272
|
freeze: o.freeze,
|
|
204
273
|
max_nesting: o.max_nesting,
|
|
274
|
+
allow_duplicate_key: o.allow_duplicate_key,
|
|
205
275
|
};
|
|
206
276
|
|
|
207
277
|
// Safety: callers verified UTF-8 (coderange or nosj slice).
|
|
208
278
|
let result = unsafe {
|
|
209
279
|
nosj::parse_utf8_unchecked_with(&source[start..end], bufs, &mut sink, o.popts)
|
|
210
280
|
};
|
|
211
|
-
finish_drive(ruby, result, sink.stack, o
|
|
281
|
+
finish_drive(ruby, result, sink.stack, o, source, (start, end))
|
|
212
282
|
})
|
|
213
283
|
}
|
|
214
284
|
|
|
@@ -238,15 +308,16 @@ pub fn valid_native(
|
|
|
238
308
|
let Ok(input) = utf8_input(ruby, &data) else {
|
|
239
309
|
return Ok(false);
|
|
240
310
|
};
|
|
241
|
-
let
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
.
|
|
311
|
+
let result = drive_hashless(input, &o, |check_dups| {
|
|
312
|
+
with_pull_state(|state| {
|
|
313
|
+
let mut sink = NullSink {
|
|
314
|
+
depth: 0,
|
|
315
|
+
max_nesting: o.max_nesting,
|
|
316
|
+
dup_keys: DupKeys::new(&mut state.dup, check_dups),
|
|
317
|
+
};
|
|
318
|
+
// Safety: coderange verified by utf8_input.
|
|
319
|
+
unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
|
|
320
|
+
})
|
|
250
321
|
});
|
|
251
|
-
Ok(
|
|
322
|
+
Ok(result.is_ok())
|
|
252
323
|
}
|
data/ext/nosj/src/patch.rs
CHANGED
|
@@ -16,7 +16,7 @@ use magnus::{Error, ExceptionClass, RArray, RHash, RString, Ruby, Value};
|
|
|
16
16
|
use crate::errors::{nosj_exception, parser_error_at};
|
|
17
17
|
use crate::gen::{self, opts::GenConfig};
|
|
18
18
|
use crate::parse::{materialize_at, span_of, utf8_input, ParseNativeOpts};
|
|
19
|
-
use crate::state::
|
|
19
|
+
use crate::state::with_pull_state;
|
|
20
20
|
|
|
21
21
|
const WS: [u8; 4] = *b" \t\n\r";
|
|
22
22
|
|
|
@@ -46,8 +46,7 @@ fn gen_config<'a>(
|
|
|
46
46
|
/// Parse failures raise the rich ParserError (absolute positions);
|
|
47
47
|
/// pointer syntax errors raise ArgumentError, like `at_pointer`.
|
|
48
48
|
fn span_at(ruby: &Ruby, doc: &[u8], pointer: &str) -> Result<Option<(usize, usize)>, Error> {
|
|
49
|
-
let resolved =
|
|
50
|
-
let mut state = cell.borrow_mut();
|
|
49
|
+
let resolved = with_pull_state(|state| {
|
|
51
50
|
// SAFETY: every entry validated the document bytes as UTF-8.
|
|
52
51
|
unsafe { nosj::pointer_utf8_unchecked(doc, pointer, &mut state.bufs) }
|
|
53
52
|
});
|
|
@@ -82,8 +81,7 @@ fn container_children(
|
|
|
82
81
|
) -> Result<(u8, Vec<ChildSpan>), Error> {
|
|
83
82
|
let span = &doc[start..end];
|
|
84
83
|
let kind = span.first().copied().unwrap_or(0);
|
|
85
|
-
let walk: Result<Vec<ChildSpan>, nosj::ParseError> =
|
|
86
|
-
let mut state = cell.borrow_mut();
|
|
84
|
+
let walk: Result<Vec<ChildSpan>, nosj::ParseError> = with_pull_state(|state| {
|
|
87
85
|
// SAFETY: doc validated UTF-8 by the entry; spans lie on token
|
|
88
86
|
// edges.
|
|
89
87
|
let mut r = unsafe { nosj::Reader::from_utf8_unchecked(span, &mut state.bufs) };
|
|
@@ -360,6 +358,12 @@ fn op_test(ruby: &Ruby, doc: &[u8], path: &str, expected: Value) -> Result<(), E
|
|
|
360
358
|
/// `NOSJ.splice(json, edits, opts)`: batch pointer replacement. All
|
|
361
359
|
/// targets resolve in ONE forward pass; the output is built in one
|
|
362
360
|
/// sweep copying every byte outside the target spans untouched.
|
|
361
|
+
///
|
|
362
|
+
/// Every value is generated BEFORE the source bytes are borrowed:
|
|
363
|
+
/// generation runs user code (`to_json`, `to_s`) that may mutate or
|
|
364
|
+
/// reallocate the source, or drop the only Ruby reference to a later
|
|
365
|
+
/// value. Rendering inside the edits iteration keeps each value a live
|
|
366
|
+
/// argument while it runs, and nothing after the borrow calls Ruby.
|
|
363
367
|
pub fn splice_native(
|
|
364
368
|
ruby: &Ruby,
|
|
365
369
|
_rb_self: Value,
|
|
@@ -369,21 +373,26 @@ pub fn splice_native(
|
|
|
369
373
|
) -> Result<RString, Error> {
|
|
370
374
|
let mut slot = None;
|
|
371
375
|
let cfg = gen_config(ruby, opts, &mut slot)?;
|
|
372
|
-
|
|
376
|
+
// Validate the encoding up front (error precedence); the bytes are
|
|
377
|
+
// borrowed again only after every callback has run.
|
|
378
|
+
utf8_input(ruby, &data)?;
|
|
373
379
|
|
|
374
380
|
let mut pointers: Vec<String> = Vec::with_capacity(edits.len());
|
|
375
|
-
let mut
|
|
381
|
+
let mut rendered: Vec<u8> = Vec::new();
|
|
382
|
+
let mut ranges: Vec<(usize, usize)> = Vec::with_capacity(edits.len());
|
|
376
383
|
edits.foreach(|k: Value, v: Value| {
|
|
377
384
|
let ptr = RString::from_value(k)
|
|
378
385
|
.ok_or_else(|| arg_error(ruby, "splice pointers must be Strings".into()))?;
|
|
379
386
|
pointers.push(ptr.to_string()?);
|
|
380
|
-
|
|
387
|
+
let start = rendered.len();
|
|
388
|
+
gen::emit_into(ruby, v, cfg, &mut rendered)?;
|
|
389
|
+
ranges.push((start, rendered.len()));
|
|
381
390
|
Ok(magnus::r_hash::ForEach::Continue)
|
|
382
391
|
})?;
|
|
383
392
|
|
|
393
|
+
let input = utf8_input(ruby, &data)?;
|
|
384
394
|
let refs: Vec<&str> = pointers.iter().map(String::as_str).collect();
|
|
385
|
-
let resolved =
|
|
386
|
-
let mut state = cell.borrow_mut();
|
|
395
|
+
let resolved = with_pull_state(|state| {
|
|
387
396
|
// SAFETY: coderange verified by utf8_input.
|
|
388
397
|
unsafe { nosj::pointers_utf8_unchecked(input, &refs, &mut state.bufs) }
|
|
389
398
|
});
|
|
@@ -395,7 +404,7 @@ pub fn splice_native(
|
|
|
395
404
|
Err(e) => return Err(parser_error_at(ruby, input, e.offset, e.to_string())),
|
|
396
405
|
};
|
|
397
406
|
|
|
398
|
-
let mut spans: Vec<(usize, usize,
|
|
407
|
+
let mut spans: Vec<(usize, usize, usize)> = Vec::with_capacity(pointers.len());
|
|
399
408
|
for (i, hit) in hits.into_iter().enumerate() {
|
|
400
409
|
let Some(slice) = hit else {
|
|
401
410
|
let exc: ExceptionClass = ruby.exception_key_error();
|
|
@@ -405,7 +414,7 @@ pub fn splice_native(
|
|
|
405
414
|
));
|
|
406
415
|
};
|
|
407
416
|
let (s, e) = span_of(input, slice.as_bytes());
|
|
408
|
-
spans.push((s, e,
|
|
417
|
+
spans.push((s, e, i));
|
|
409
418
|
}
|
|
410
419
|
spans.sort_unstable_by_key(|&(s, _, _)| s);
|
|
411
420
|
for pair in spans.windows(2) {
|
|
@@ -417,11 +426,12 @@ pub fn splice_native(
|
|
|
417
426
|
}
|
|
418
427
|
}
|
|
419
428
|
|
|
420
|
-
let mut out = Vec::with_capacity(input.len() +
|
|
429
|
+
let mut out = Vec::with_capacity(input.len() + rendered.len());
|
|
421
430
|
let mut pos = 0;
|
|
422
|
-
for &(s, e,
|
|
431
|
+
for &(s, e, edit) in &spans {
|
|
432
|
+
let (rs, re) = ranges[edit];
|
|
423
433
|
out.extend_from_slice(&input[pos..s]);
|
|
424
|
-
|
|
434
|
+
out.extend_from_slice(&rendered[rs..re]);
|
|
425
435
|
pos = e;
|
|
426
436
|
}
|
|
427
437
|
out.extend_from_slice(&input[pos..]);
|
data/ext/nosj/src/pointer.rs
CHANGED
|
@@ -7,7 +7,7 @@ use magnus::{Error, RString, Ruby, Value};
|
|
|
7
7
|
|
|
8
8
|
use crate::errors::parser_error_at;
|
|
9
9
|
use crate::parse::{materialize_at, parse_native_opts, span_of, utf8_input, ParseNativeOpts};
|
|
10
|
-
use crate::state::
|
|
10
|
+
use crate::state::with_pull_state;
|
|
11
11
|
|
|
12
12
|
/// Resolve one JSON Pointer against `data`, materializing the matched
|
|
13
13
|
/// subtree; `nil` when the pointer misses.
|
|
@@ -19,12 +19,11 @@ fn at_pointer_impl(
|
|
|
19
19
|
) -> Result<Value, Error> {
|
|
20
20
|
use magnus::value::ReprValue;
|
|
21
21
|
let input = utf8_input(ruby, &data)?;
|
|
22
|
-
// Resolve
|
|
23
|
-
//
|
|
24
|
-
let resolved =
|
|
25
|
-
let mut state = cell.borrow_mut();
|
|
22
|
+
// Resolve, then materialize, as two separate uses of the parse
|
|
23
|
+
// state; the resolved slice borrows `input`, not the state.
|
|
24
|
+
let resolved = with_pull_state(|state| {
|
|
26
25
|
// Safety: coderange verified above.
|
|
27
|
-
unsafe { nosj::
|
|
26
|
+
unsafe { nosj::pointer_utf8_unchecked_with(input, pointer, &mut state.bufs, o.popts) }
|
|
28
27
|
});
|
|
29
28
|
match resolved {
|
|
30
29
|
Ok(None) => Ok(ruby.qnil().as_value()),
|
|
@@ -125,12 +124,11 @@ fn at_pointers_impl(
|
|
|
125
124
|
let input = utf8_input(ruby, &data)?;
|
|
126
125
|
let live: Vec<&str> = pointers.iter().flatten().map(String::as_str).collect();
|
|
127
126
|
|
|
128
|
-
// Resolve first (one
|
|
129
|
-
// `input`, not the
|
|
130
|
-
let resolved =
|
|
131
|
-
let mut state = cell.borrow_mut();
|
|
127
|
+
// Resolve first (one use of the parse state); the resolved slices
|
|
128
|
+
// borrow `input`, not the state, so each materializes separately.
|
|
129
|
+
let resolved = with_pull_state(|state| {
|
|
132
130
|
// Safety: coderange verified by utf8_input.
|
|
133
|
-
unsafe { nosj::
|
|
131
|
+
unsafe { nosj::pointers_utf8_unchecked_with(input, &live, &mut state.bufs, o.popts) }
|
|
134
132
|
});
|
|
135
133
|
let mut hits = match resolved {
|
|
136
134
|
Ok(hits) => hits.into_iter(),
|