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/parse.rs
CHANGED
|
@@ -6,8 +6,10 @@
|
|
|
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::
|
|
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};
|
|
11
13
|
use crate::state::{ensure_marked_shadow, with_pull_state, PullState};
|
|
12
14
|
|
|
13
15
|
pub(crate) use crate::errors::parser_error as err;
|
|
@@ -51,6 +53,8 @@ pub(crate) struct ParseNativeOpts {
|
|
|
51
53
|
pub(crate) symbolize: bool,
|
|
52
54
|
pub(crate) freeze: bool,
|
|
53
55
|
pub(crate) max_nesting: usize,
|
|
56
|
+
/// json 3 default: a repeated key raises.
|
|
57
|
+
pub(crate) allow_duplicate_key: bool,
|
|
54
58
|
pub(crate) popts: nosj::ParseOptions,
|
|
55
59
|
}
|
|
56
60
|
|
|
@@ -60,111 +64,171 @@ impl Default for ParseNativeOpts {
|
|
|
60
64
|
symbolize: false,
|
|
61
65
|
freeze: false,
|
|
62
66
|
max_nesting: MAX_NESTING,
|
|
67
|
+
allow_duplicate_key: false,
|
|
63
68
|
popts: nosj::ParseOptions::default(),
|
|
64
69
|
}
|
|
65
70
|
}
|
|
66
71
|
}
|
|
67
72
|
|
|
68
|
-
/// Decode a JSON.parse-compatible options hash
|
|
69
|
-
///
|
|
70
|
-
/// (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.
|
|
71
75
|
pub(crate) fn parse_native_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeOpts, Error> {
|
|
72
|
-
|
|
73
|
-
|
|
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
|
+
}
|
|
74
84
|
|
|
75
|
-
|
|
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;
|
|
76
90
|
if opts.is_nil() {
|
|
77
|
-
return Ok(
|
|
91
|
+
return Ok(None);
|
|
78
92
|
}
|
|
79
|
-
let h = RHash::from_value(opts)
|
|
93
|
+
let h = magnus::RHash::from_value(opts)
|
|
80
94
|
.ok_or_else(|| Error::new(ruby.exception_arg_error(), "options must be a Hash"))?;
|
|
95
|
+
Ok((!h.is_empty()).then_some(h))
|
|
96
|
+
}
|
|
81
97
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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()
|
|
85
109
|
};
|
|
110
|
+
out.popts.allow_nan = r.truthy(Opt::AllowNan);
|
|
111
|
+
out.popts.allow_trailing_comma = r.truthy(Opt::AllowTrailingComma);
|
|
86
112
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
out.popts.allow_trailing_comma = truthy("allow_trailing_comma");
|
|
113
|
+
if let Some(mn) = r.get(Opt::MaxNesting) {
|
|
114
|
+
out.max_nesting = max_nesting_of(mn);
|
|
115
|
+
}
|
|
91
116
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
+
]);
|
|
127
|
+
Ok(out)
|
|
128
|
+
}
|
|
129
|
+
|
|
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)
|
|
103
142
|
}
|
|
143
|
+
}
|
|
104
144
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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(
|
|
153
|
+
ruby: &Ruby,
|
|
154
|
+
failure: nosj::DriveError<SinkAbort>,
|
|
155
|
+
o: &ParseNativeOpts,
|
|
156
|
+
source: &[u8],
|
|
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(
|
|
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"),
|
|
192
|
+
format!("{spelling} not allowed in JSON"),
|
|
193
|
+
),
|
|
194
|
+
Parse(e) => parser_error_at(ruby, source, start + e.offset, e.to_string()),
|
|
117
195
|
}
|
|
118
|
-
Ok(out)
|
|
119
196
|
}
|
|
120
197
|
|
|
121
|
-
///
|
|
122
|
-
///
|
|
123
|
-
/// the
|
|
124
|
-
///
|
|
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`]).
|
|
125
219
|
fn finish_drive(
|
|
126
220
|
ruby: &Ruby,
|
|
127
|
-
result:
|
|
221
|
+
result: DriveResult,
|
|
128
222
|
stack: &mut Vec<rb_sys::VALUE>,
|
|
129
|
-
|
|
223
|
+
o: &ParseNativeOpts,
|
|
130
224
|
source: &[u8],
|
|
131
|
-
|
|
225
|
+
span: (usize, usize),
|
|
132
226
|
) -> Result<Value, Error> {
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
Ok(unsafe { Value::from_raw(raw) })
|
|
139
|
-
}
|
|
140
|
-
Err(nosj::DriveError::Sink(SinkAbort::Overflow)) => {
|
|
141
|
-
Err(parser_error(ruby, "document too large".into()))
|
|
142
|
-
}
|
|
143
|
-
Err(nosj::DriveError::Sink(SinkAbort::BadBigint)) => {
|
|
144
|
-
Err(parser_error(ruby, "invalid bignum".into()))
|
|
145
|
-
}
|
|
146
|
-
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
|
|
147
|
-
ruby,
|
|
148
|
-
format!("nesting of {} is too deep", max_nesting.saturating_add(1)),
|
|
149
|
-
)),
|
|
150
|
-
// Raised only by the reformat pipe's sink, which never drives
|
|
151
|
-
// through here; the match must stay total.
|
|
152
|
-
Err(nosj::DriveError::Sink(SinkAbort::BrokenUtf8Output)) => Err(parser_error(
|
|
153
|
-
ruby,
|
|
154
|
-
"source sequence is illegal/malformed utf-8".into(),
|
|
155
|
-
)),
|
|
156
|
-
// Also reformat-pipe-only, kept total for the same reason.
|
|
157
|
-
Err(nosj::DriveError::Sink(SinkAbort::NonFiniteFloat(spelling))) => Err(parser_error(
|
|
158
|
-
ruby,
|
|
159
|
-
format!("{spelling} not allowed in JSON"),
|
|
160
|
-
)),
|
|
161
|
-
Err(nosj::DriveError::Parse(e)) => Err(parser_error_at(
|
|
162
|
-
ruby,
|
|
163
|
-
source,
|
|
164
|
-
base + e.offset,
|
|
165
|
-
e.to_string(),
|
|
166
|
-
)),
|
|
167
|
-
}
|
|
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) })
|
|
168
232
|
}
|
|
169
233
|
|
|
170
234
|
/// Drive the fused cursor over the whole of `source`. See
|
|
@@ -207,13 +271,14 @@ pub(crate) fn materialize_at(
|
|
|
207
271
|
symbolize: o.symbolize,
|
|
208
272
|
freeze: o.freeze,
|
|
209
273
|
max_nesting: o.max_nesting,
|
|
274
|
+
allow_duplicate_key: o.allow_duplicate_key,
|
|
210
275
|
};
|
|
211
276
|
|
|
212
277
|
// Safety: callers verified UTF-8 (coderange or nosj slice).
|
|
213
278
|
let result = unsafe {
|
|
214
279
|
nosj::parse_utf8_unchecked_with(&source[start..end], bufs, &mut sink, o.popts)
|
|
215
280
|
};
|
|
216
|
-
finish_drive(ruby, result, sink.stack, o
|
|
281
|
+
finish_drive(ruby, result, sink.stack, o, source, (start, end))
|
|
217
282
|
})
|
|
218
283
|
}
|
|
219
284
|
|
|
@@ -243,14 +308,16 @@ pub fn valid_native(
|
|
|
243
308
|
let Ok(input) = utf8_input(ruby, &data) else {
|
|
244
309
|
return Ok(false);
|
|
245
310
|
};
|
|
246
|
-
let
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
.
|
|
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
|
+
})
|
|
254
321
|
});
|
|
255
|
-
Ok(
|
|
322
|
+
Ok(result.is_ok())
|
|
256
323
|
}
|
data/ext/nosj/src/reformat.rs
CHANGED
|
@@ -5,27 +5,26 @@
|
|
|
5
5
|
//! SIMD escape kernels on the way out there is nothing else.
|
|
6
6
|
//!
|
|
7
7
|
//! Output is exactly what `NOSJ.generate(NOSJ.parse(json), opts)`
|
|
8
|
-
//! would produce,
|
|
9
|
-
//! keys
|
|
10
|
-
//!
|
|
11
|
-
//!
|
|
12
|
-
//!
|
|
13
|
-
//!
|
|
14
|
-
//! normalized by the emission kernels.
|
|
8
|
+
//! would produce, and the pipe accepts exactly what parse accepts:
|
|
9
|
+
//! duplicate keys raise unless `allow_duplicate_key` (then they pass
|
|
10
|
+
//! through: a reformatter must not silently drop data the way parse's
|
|
11
|
+
//! last-key-wins materialization does), and lone surrogates raise.
|
|
12
|
+
//! Numbers come out in the gem's canonical spelling (`1.50` becomes
|
|
13
|
+
//! `1.5`), and string escapes are normalized by the emission kernels.
|
|
15
14
|
|
|
16
15
|
use std::cell::Cell;
|
|
17
16
|
|
|
18
|
-
use magnus::value::ReprValue;
|
|
19
17
|
use magnus::{Error, RString, Ruby, Value};
|
|
20
|
-
use nosj::emit::EscapeMode;
|
|
21
18
|
use nosj::{FloatFormat, WriteOptions, Writer};
|
|
22
19
|
|
|
23
|
-
use crate::errors::{nesting_error, nosj_exception, parser_error, parser_error_at};
|
|
24
20
|
use crate::files::with_mapped_file;
|
|
25
|
-
use crate::gen::opts::{
|
|
26
|
-
use crate::
|
|
21
|
+
use crate::gen::opts::{read_gen_opts, GenConfig, DEFAULT_CONFIG};
|
|
22
|
+
use crate::opt_reader::OptReader;
|
|
23
|
+
use crate::parse::{
|
|
24
|
+
drive_error, drive_hashless, options_hash, read_parse_opts, utf8_input, ParseNativeOpts,
|
|
25
|
+
};
|
|
27
26
|
use crate::patch::finish_string;
|
|
28
|
-
use crate::sink::SinkAbort;
|
|
27
|
+
use crate::sink::{DupKeys, SinkAbort};
|
|
29
28
|
use crate::state::{with_pull_state, with_taken};
|
|
30
29
|
|
|
31
30
|
thread_local! {
|
|
@@ -41,61 +40,10 @@ struct PipeSink<'a> {
|
|
|
41
40
|
w: Writer<'a>,
|
|
42
41
|
depth: usize,
|
|
43
42
|
max_nesting: usize,
|
|
44
|
-
/// For re-escaping WTF-8 string content (see [`quote_wtf8`]).
|
|
45
|
-
mode: EscapeMode,
|
|
46
43
|
/// Non-finite floats pass through as literals only when the
|
|
47
44
|
/// generate side allows them; see [`PipeSink::float`].
|
|
48
45
|
allow_nan: bool,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
|
52
|
-
|
|
53
|
-
/// WTF-8 lone surrogates arrive as one 3-byte sequence with this lead
|
|
54
|
-
/// byte (the only ill-formed runs the parser ever emits).
|
|
55
|
-
const WTF8_SURROGATE_LEAD: u8 = 0xED;
|
|
56
|
-
const WTF8_SURROGATE_LEN: usize = 3;
|
|
57
|
-
/// Payload bits of a UTF-8 lead / continuation byte.
|
|
58
|
-
const UTF8_LEAD3_BITS: u32 = 0x0F;
|
|
59
|
-
const UTF8_CONT_BITS: u32 = 0x3F;
|
|
60
|
-
|
|
61
|
-
/// Quote and escape WTF-8 content: valid UTF-8 runs go through the
|
|
62
|
-
/// configured escape kernel, and lone-surrogate sequences re-escape as
|
|
63
|
-
/// `\uXXXX`, so the output reparses to the identical string in every
|
|
64
|
-
/// mode (raw WTF-8 bytes would not: the parser requires UTF-8 input).
|
|
65
|
-
/// This deliberately diverges from `generate`, which refuses
|
|
66
|
-
/// broken-coderange strings: a reformatter must accept everything the
|
|
67
|
-
/// parser accepts.
|
|
68
|
-
fn quote_wtf8(out: &mut Vec<u8>, bytes: &[u8], mode: EscapeMode) {
|
|
69
|
-
out.push(b'"');
|
|
70
|
-
let mut rest = bytes;
|
|
71
|
-
loop {
|
|
72
|
-
match std::str::from_utf8(rest) {
|
|
73
|
-
Ok(s) => {
|
|
74
|
-
nosj::emit::escape_into(out, s.as_bytes(), mode);
|
|
75
|
-
break;
|
|
76
|
-
}
|
|
77
|
-
Err(e) => {
|
|
78
|
-
let valid = e.valid_up_to();
|
|
79
|
-
nosj::emit::escape_into(out, &rest[..valid], mode);
|
|
80
|
-
let sur = &rest[valid..];
|
|
81
|
-
debug_assert!(
|
|
82
|
-
sur.len() >= WTF8_SURROGATE_LEN && sur[0] == WTF8_SURROGATE_LEAD,
|
|
83
|
-
"parser only emits lone-surrogate WTF-8"
|
|
84
|
-
);
|
|
85
|
-
// Standard 3-byte UTF-8 decode of the surrogate
|
|
86
|
-
// codepoint (U+D800..U+DFFF), re-emitted as \uXXXX.
|
|
87
|
-
let cp = ((u32::from(sur[0]) & UTF8_LEAD3_BITS) << 12)
|
|
88
|
-
| ((u32::from(sur[1]) & UTF8_CONT_BITS) << 6)
|
|
89
|
-
| (u32::from(sur[2]) & UTF8_CONT_BITS);
|
|
90
|
-
out.extend_from_slice(b"\\u");
|
|
91
|
-
for shift in [12, 8, 4, 0] {
|
|
92
|
-
out.push(HEX_DIGITS[((cp >> shift) & 0xF) as usize]);
|
|
93
|
-
}
|
|
94
|
-
rest = &sur[WTF8_SURROGATE_LEN..];
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
out.push(b'"');
|
|
46
|
+
dup_keys: DupKeys<'a>,
|
|
99
47
|
}
|
|
100
48
|
|
|
101
49
|
impl PipeSink<'_> {
|
|
@@ -165,25 +113,18 @@ impl nosj::Sink for PipeSink<'_> {
|
|
|
165
113
|
Ok(())
|
|
166
114
|
}
|
|
167
115
|
|
|
168
|
-
fn str_bytes(&mut self,
|
|
169
|
-
|
|
170
|
-
let mut quoted = Vec::with_capacity(value.len() + 8);
|
|
171
|
-
quote_wtf8(&mut quoted, value, self.mode);
|
|
172
|
-
self.w.value_raw("ed);
|
|
173
|
-
Ok(())
|
|
116
|
+
fn str_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
117
|
+
Err(SinkAbort::LoneSurrogate)
|
|
174
118
|
}
|
|
175
119
|
|
|
176
120
|
fn key(&mut self, key: &str) -> Result<(), SinkAbort> {
|
|
121
|
+
self.dup_keys.key(key.as_bytes());
|
|
177
122
|
self.w.key(key);
|
|
178
123
|
Ok(())
|
|
179
124
|
}
|
|
180
125
|
|
|
181
|
-
fn key_bytes(&mut self,
|
|
182
|
-
|
|
183
|
-
// the Writer (values have value_raw; a key_raw is on the crate
|
|
184
|
-
// wishlist), so this pathological case keeps generate's
|
|
185
|
-
// refusal semantics.
|
|
186
|
-
Err(SinkAbort::BrokenUtf8Output)
|
|
126
|
+
fn key_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
127
|
+
Err(SinkAbort::LoneSurrogate)
|
|
187
128
|
}
|
|
188
129
|
|
|
189
130
|
fn begin_array(&mut self) -> Result<(), SinkAbort> {
|
|
@@ -199,7 +140,7 @@ impl nosj::Sink for PipeSink<'_> {
|
|
|
199
140
|
}
|
|
200
141
|
|
|
201
142
|
fn mark(&self) -> usize {
|
|
202
|
-
|
|
143
|
+
self.dup_keys.mark()
|
|
203
144
|
}
|
|
204
145
|
|
|
205
146
|
fn end_array(&mut self, _: usize, _: usize) -> Result<(), SinkAbort> {
|
|
@@ -208,8 +149,9 @@ impl nosj::Sink for PipeSink<'_> {
|
|
|
208
149
|
Ok(())
|
|
209
150
|
}
|
|
210
151
|
|
|
211
|
-
fn end_object(&mut self,
|
|
152
|
+
fn end_object(&mut self, mark: usize, _: usize) -> Result<(), SinkAbort> {
|
|
212
153
|
self.depth -= 1;
|
|
154
|
+
self.dup_keys.close(mark)?;
|
|
213
155
|
self.w.end_object();
|
|
214
156
|
Ok(())
|
|
215
157
|
}
|
|
@@ -238,14 +180,21 @@ struct ReformatOpts {
|
|
|
238
180
|
}
|
|
239
181
|
|
|
240
182
|
impl ReformatOpts {
|
|
183
|
+
/// One reader over both option sets, so a key either reads is known.
|
|
241
184
|
fn decode(ruby: &Ruby, opts: Value) -> Result<Self, Error> {
|
|
185
|
+
let Some(h) = options_hash(ruby, opts)? else {
|
|
186
|
+
return Ok(Self {
|
|
187
|
+
parse: ParseNativeOpts::default(),
|
|
188
|
+
generate: None,
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
let mut reader = OptReader::new(ruby, h);
|
|
192
|
+
let parse = read_parse_opts(&mut reader)?;
|
|
193
|
+
let (generate, _) = read_gen_opts(&mut reader)?;
|
|
194
|
+
reader.finish()?;
|
|
242
195
|
Ok(Self {
|
|
243
|
-
parse
|
|
244
|
-
generate:
|
|
245
|
-
None
|
|
246
|
-
} else {
|
|
247
|
-
Some(parse_gen_opts(ruby, opts)?.0)
|
|
248
|
-
},
|
|
196
|
+
parse,
|
|
197
|
+
generate: Some(generate),
|
|
249
198
|
})
|
|
250
199
|
}
|
|
251
200
|
}
|
|
@@ -257,46 +206,26 @@ fn reformat_over(ruby: &Ruby, input: &[u8], opts: &ReformatOpts) -> Result<RStri
|
|
|
257
206
|
let wopts = write_options(gcfg);
|
|
258
207
|
|
|
259
208
|
with_taken(&PIPE_BUF, |buf| {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
po.max_nesting.saturating_add(1)
|
|
281
|
-
),
|
|
282
|
-
)),
|
|
283
|
-
Err(nosj::DriveError::Sink(SinkAbort::BrokenUtf8Output)) => Err(Error::new(
|
|
284
|
-
// Gem parity: generate raises GeneratorError for a
|
|
285
|
-
// string ascii_only cannot represent.
|
|
286
|
-
nosj_exception(ruby, "GeneratorError"),
|
|
287
|
-
"source sequence is illegal/malformed utf-8",
|
|
288
|
-
)),
|
|
289
|
-
Err(nosj::DriveError::Sink(SinkAbort::NonFiniteFloat(spelling))) => Err(Error::new(
|
|
290
|
-
nosj_exception(ruby, "GeneratorError"),
|
|
291
|
-
format!("{spelling} not allowed in JSON"),
|
|
292
|
-
)),
|
|
293
|
-
Err(nosj::DriveError::Sink(_)) => {
|
|
294
|
-
Err(parser_error(ruby, "reformat pass aborted".into()))
|
|
295
|
-
}
|
|
296
|
-
Err(nosj::DriveError::Parse(e)) => {
|
|
297
|
-
Err(parser_error_at(ruby, input, e.offset, e.to_string()))
|
|
298
|
-
}
|
|
299
|
-
}
|
|
209
|
+
drive_hashless(input, po, |check_dups| {
|
|
210
|
+
buf.clear();
|
|
211
|
+
// The output is at least input-sized for minify-shaped runs.
|
|
212
|
+
buf.reserve(input.len());
|
|
213
|
+
with_pull_state(|state| {
|
|
214
|
+
let mut sink = PipeSink {
|
|
215
|
+
w: Writer::new(buf, &wopts),
|
|
216
|
+
depth: 0,
|
|
217
|
+
max_nesting: po.max_nesting,
|
|
218
|
+
allow_nan: gcfg.allow_nan,
|
|
219
|
+
dup_keys: DupKeys::new(&mut state.dup, check_dups),
|
|
220
|
+
};
|
|
221
|
+
// Safety: callers verified UTF-8 (coderange or full scan).
|
|
222
|
+
unsafe {
|
|
223
|
+
nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, po.popts)
|
|
224
|
+
}
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
.map_err(|failure| drive_error(ruby, failure, po, input, (0, input.len())))?;
|
|
228
|
+
finish_string(buf)
|
|
300
229
|
})
|
|
301
230
|
}
|
|
302
231
|
|