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/reformat.rs
CHANGED
|
@@ -5,34 +5,32 @@
|
|
|
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
|
-
use std::cell::
|
|
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;
|
|
29
|
-
use crate::state::
|
|
27
|
+
use crate::sink::{DupKeys, SinkAbort};
|
|
28
|
+
use crate::state::{with_pull_state, with_taken};
|
|
30
29
|
|
|
31
30
|
thread_local! {
|
|
32
|
-
/// Pooled output buffer: capacity survives across calls
|
|
33
|
-
///
|
|
34
|
-
|
|
35
|
-
static PIPE_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
|
|
31
|
+
/// Pooled output buffer: capacity survives across calls (see
|
|
32
|
+
/// `state::with_taken`).
|
|
33
|
+
static PIPE_BUF: Cell<Vec<u8>> = const { Cell::new(Vec::new()) };
|
|
36
34
|
}
|
|
37
35
|
|
|
38
36
|
/// The event-to-Writer pipe. Structure events forward to the Writer's
|
|
@@ -42,61 +40,10 @@ struct PipeSink<'a> {
|
|
|
42
40
|
w: Writer<'a>,
|
|
43
41
|
depth: usize,
|
|
44
42
|
max_nesting: usize,
|
|
45
|
-
/// For re-escaping WTF-8 string content (see [`quote_wtf8`]).
|
|
46
|
-
mode: EscapeMode,
|
|
47
43
|
/// Non-finite floats pass through as literals only when the
|
|
48
44
|
/// generate side allows them; see [`PipeSink::float`].
|
|
49
45
|
allow_nan: bool,
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
const HEX_DIGITS: &[u8; 16] = b"0123456789abcdef";
|
|
53
|
-
|
|
54
|
-
/// WTF-8 lone surrogates arrive as one 3-byte sequence with this lead
|
|
55
|
-
/// byte (the only ill-formed runs the parser ever emits).
|
|
56
|
-
const WTF8_SURROGATE_LEAD: u8 = 0xED;
|
|
57
|
-
const WTF8_SURROGATE_LEN: usize = 3;
|
|
58
|
-
/// Payload bits of a UTF-8 lead / continuation byte.
|
|
59
|
-
const UTF8_LEAD3_BITS: u32 = 0x0F;
|
|
60
|
-
const UTF8_CONT_BITS: u32 = 0x3F;
|
|
61
|
-
|
|
62
|
-
/// Quote and escape WTF-8 content: valid UTF-8 runs go through the
|
|
63
|
-
/// configured escape kernel, and lone-surrogate sequences re-escape as
|
|
64
|
-
/// `\uXXXX`, so the output reparses to the identical string in every
|
|
65
|
-
/// mode (raw WTF-8 bytes would not: the parser requires UTF-8 input).
|
|
66
|
-
/// This deliberately diverges from `generate`, which refuses
|
|
67
|
-
/// broken-coderange strings: a reformatter must accept everything the
|
|
68
|
-
/// parser accepts.
|
|
69
|
-
fn quote_wtf8(out: &mut Vec<u8>, bytes: &[u8], mode: EscapeMode) {
|
|
70
|
-
out.push(b'"');
|
|
71
|
-
let mut rest = bytes;
|
|
72
|
-
loop {
|
|
73
|
-
match std::str::from_utf8(rest) {
|
|
74
|
-
Ok(s) => {
|
|
75
|
-
nosj::emit::escape_into(out, s.as_bytes(), mode);
|
|
76
|
-
break;
|
|
77
|
-
}
|
|
78
|
-
Err(e) => {
|
|
79
|
-
let valid = e.valid_up_to();
|
|
80
|
-
nosj::emit::escape_into(out, &rest[..valid], mode);
|
|
81
|
-
let sur = &rest[valid..];
|
|
82
|
-
debug_assert!(
|
|
83
|
-
sur.len() >= WTF8_SURROGATE_LEN && sur[0] == WTF8_SURROGATE_LEAD,
|
|
84
|
-
"parser only emits lone-surrogate WTF-8"
|
|
85
|
-
);
|
|
86
|
-
// Standard 3-byte UTF-8 decode of the surrogate
|
|
87
|
-
// codepoint (U+D800..U+DFFF), re-emitted as \uXXXX.
|
|
88
|
-
let cp = ((u32::from(sur[0]) & UTF8_LEAD3_BITS) << 12)
|
|
89
|
-
| ((u32::from(sur[1]) & UTF8_CONT_BITS) << 6)
|
|
90
|
-
| (u32::from(sur[2]) & UTF8_CONT_BITS);
|
|
91
|
-
out.extend_from_slice(b"\\u");
|
|
92
|
-
for shift in [12, 8, 4, 0] {
|
|
93
|
-
out.push(HEX_DIGITS[((cp >> shift) & 0xF) as usize]);
|
|
94
|
-
}
|
|
95
|
-
rest = &sur[WTF8_SURROGATE_LEN..];
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
out.push(b'"');
|
|
46
|
+
dup_keys: DupKeys<'a>,
|
|
100
47
|
}
|
|
101
48
|
|
|
102
49
|
impl PipeSink<'_> {
|
|
@@ -166,25 +113,18 @@ impl nosj::Sink for PipeSink<'_> {
|
|
|
166
113
|
Ok(())
|
|
167
114
|
}
|
|
168
115
|
|
|
169
|
-
fn str_bytes(&mut self,
|
|
170
|
-
|
|
171
|
-
let mut quoted = Vec::with_capacity(value.len() + 8);
|
|
172
|
-
quote_wtf8(&mut quoted, value, self.mode);
|
|
173
|
-
self.w.value_raw("ed);
|
|
174
|
-
Ok(())
|
|
116
|
+
fn str_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
117
|
+
Err(SinkAbort::LoneSurrogate)
|
|
175
118
|
}
|
|
176
119
|
|
|
177
120
|
fn key(&mut self, key: &str) -> Result<(), SinkAbort> {
|
|
121
|
+
self.dup_keys.key(key.as_bytes());
|
|
178
122
|
self.w.key(key);
|
|
179
123
|
Ok(())
|
|
180
124
|
}
|
|
181
125
|
|
|
182
|
-
fn key_bytes(&mut self,
|
|
183
|
-
|
|
184
|
-
// the Writer (values have value_raw; a key_raw is on the crate
|
|
185
|
-
// wishlist), so this pathological case keeps generate's
|
|
186
|
-
// refusal semantics.
|
|
187
|
-
Err(SinkAbort::BrokenUtf8Output)
|
|
126
|
+
fn key_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
127
|
+
Err(SinkAbort::LoneSurrogate)
|
|
188
128
|
}
|
|
189
129
|
|
|
190
130
|
fn begin_array(&mut self) -> Result<(), SinkAbort> {
|
|
@@ -200,7 +140,7 @@ impl nosj::Sink for PipeSink<'_> {
|
|
|
200
140
|
}
|
|
201
141
|
|
|
202
142
|
fn mark(&self) -> usize {
|
|
203
|
-
|
|
143
|
+
self.dup_keys.mark()
|
|
204
144
|
}
|
|
205
145
|
|
|
206
146
|
fn end_array(&mut self, _: usize, _: usize) -> Result<(), SinkAbort> {
|
|
@@ -209,8 +149,9 @@ impl nosj::Sink for PipeSink<'_> {
|
|
|
209
149
|
Ok(())
|
|
210
150
|
}
|
|
211
151
|
|
|
212
|
-
fn end_object(&mut self,
|
|
152
|
+
fn end_object(&mut self, mark: usize, _: usize) -> Result<(), SinkAbort> {
|
|
213
153
|
self.depth -= 1;
|
|
154
|
+
self.dup_keys.close(mark)?;
|
|
214
155
|
self.w.end_object();
|
|
215
156
|
Ok(())
|
|
216
157
|
}
|
|
@@ -230,61 +171,61 @@ fn write_options(cfg: &GenConfig) -> WriteOptions {
|
|
|
230
171
|
w
|
|
231
172
|
}
|
|
232
173
|
|
|
174
|
+
/// Decoded reformat options: parse acceptance plus generate formatting.
|
|
175
|
+
/// Decoded before the source is borrowed, since decoding can run Ruby
|
|
176
|
+
/// (an option value's `to_int`; see `parse::utf8_input`).
|
|
177
|
+
struct ReformatOpts {
|
|
178
|
+
parse: ParseNativeOpts,
|
|
179
|
+
generate: Option<GenConfig>,
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
impl ReformatOpts {
|
|
183
|
+
/// One reader over both option sets, so a key either reads is known.
|
|
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()?;
|
|
195
|
+
Ok(Self {
|
|
196
|
+
parse,
|
|
197
|
+
generate: Some(generate),
|
|
198
|
+
})
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
233
202
|
/// Run the pipe over already-UTF-8-vouched bytes.
|
|
234
|
-
fn reformat_over(ruby: &Ruby, input: &[u8], opts:
|
|
235
|
-
let po =
|
|
236
|
-
let
|
|
237
|
-
let gcfg: &GenConfig = if opts.is_nil() {
|
|
238
|
-
&DEFAULT_CONFIG
|
|
239
|
-
} else {
|
|
240
|
-
built = parse_gen_opts(ruby, opts)?.0;
|
|
241
|
-
&built
|
|
242
|
-
};
|
|
203
|
+
fn reformat_over(ruby: &Ruby, input: &[u8], opts: &ReformatOpts) -> Result<RString, Error> {
|
|
204
|
+
let po = &opts.parse;
|
|
205
|
+
let gcfg = opts.generate.as_ref().unwrap_or(&DEFAULT_CONFIG);
|
|
243
206
|
let wopts = write_options(gcfg);
|
|
244
207
|
|
|
245
|
-
PIPE_BUF
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
format!(
|
|
267
|
-
"nesting of {} is too deep",
|
|
268
|
-
po.max_nesting.saturating_add(1)
|
|
269
|
-
),
|
|
270
|
-
)),
|
|
271
|
-
Err(nosj::DriveError::Sink(SinkAbort::BrokenUtf8Output)) => Err(Error::new(
|
|
272
|
-
// Gem parity: generate raises GeneratorError for a
|
|
273
|
-
// string ascii_only cannot represent.
|
|
274
|
-
nosj_exception(ruby, "GeneratorError"),
|
|
275
|
-
"source sequence is illegal/malformed utf-8",
|
|
276
|
-
)),
|
|
277
|
-
Err(nosj::DriveError::Sink(SinkAbort::NonFiniteFloat(spelling))) => Err(Error::new(
|
|
278
|
-
nosj_exception(ruby, "GeneratorError"),
|
|
279
|
-
format!("{spelling} not allowed in JSON"),
|
|
280
|
-
)),
|
|
281
|
-
Err(nosj::DriveError::Sink(_)) => {
|
|
282
|
-
Err(parser_error(ruby, "reformat pass aborted".into()))
|
|
283
|
-
}
|
|
284
|
-
Err(nosj::DriveError::Parse(e)) => {
|
|
285
|
-
Err(parser_error_at(ruby, input, e.offset, e.to_string()))
|
|
286
|
-
}
|
|
287
|
-
}
|
|
208
|
+
with_taken(&PIPE_BUF, |buf| {
|
|
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)
|
|
288
229
|
})
|
|
289
230
|
}
|
|
290
231
|
|
|
@@ -296,8 +237,9 @@ pub fn reformat_native(
|
|
|
296
237
|
data: RString,
|
|
297
238
|
opts: Value,
|
|
298
239
|
) -> Result<RString, Error> {
|
|
240
|
+
let opts = ReformatOpts::decode(ruby, opts)?;
|
|
299
241
|
let input = utf8_input(ruby, &data)?;
|
|
300
|
-
reformat_over(ruby, input, opts)
|
|
242
|
+
reformat_over(ruby, input, &opts)
|
|
301
243
|
}
|
|
302
244
|
|
|
303
245
|
/// `NOSJ.reformat_file_native(path, opts)`: the pipe over a read-only
|
|
@@ -309,12 +251,13 @@ pub fn reformat_file_native(
|
|
|
309
251
|
opts: Value,
|
|
310
252
|
) -> Result<RString, Error> {
|
|
311
253
|
let p = path.to_string()?;
|
|
254
|
+
let opts = ReformatOpts::decode(ruby, opts)?;
|
|
312
255
|
// Mapping a zero-length file fails with EINVAL on Linux; route an
|
|
313
256
|
// empty file to the parser's own "unexpected end of input" so the
|
|
314
257
|
// error class is deterministic across platforms. Metadata failures
|
|
315
258
|
// fall through for the mapper's Errno.
|
|
316
259
|
if std::fs::metadata(&p).is_ok_and(|m| m.len() == 0) {
|
|
317
|
-
return reformat_over(ruby, &[], opts);
|
|
260
|
+
return reformat_over(ruby, &[], &opts);
|
|
318
261
|
}
|
|
319
|
-
with_mapped_file(ruby, &p, |map| reformat_over(ruby, &map, opts))
|
|
262
|
+
with_mapped_file(ruby, &p, |map| reformat_over(ruby, &map, &opts))
|
|
320
263
|
}
|
data/ext/nosj/src/sink.rs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
//! The nosj sinks: `RubyValueSink` builds Ruby VALUEs directly during
|
|
2
2
|
//! the parse (with interned-key caches and gem-compatible option
|
|
3
3
|
//! handling); `NullSink` powers `NOSJ.valid?` by discarding every
|
|
4
|
-
//! event
|
|
4
|
+
//! event; `DupKeys` gives hash-less sinks duplicate-key detection. Raw
|
|
5
|
+
//! VALUE construction helpers live here too.
|
|
5
6
|
|
|
6
7
|
use ahash::AHashMap;
|
|
7
8
|
|
|
@@ -17,17 +18,20 @@ pub(crate) const MAX_NESTING: usize = 100;
|
|
|
17
18
|
const KEY_CACHE_CAP: usize = 2048;
|
|
18
19
|
|
|
19
20
|
/// Why a sink stopped the drive; mapped onto the gem's exceptions in
|
|
20
|
-
/// [`crate::parse::
|
|
21
|
+
/// [`crate::parse::drive_error`]. Sinks see no offsets, so the two
|
|
22
|
+
/// document refusals get their position from a cold-path re-walk
|
|
23
|
+
/// (`crate::locate`).
|
|
21
24
|
pub(crate) enum SinkAbort {
|
|
22
25
|
Overflow,
|
|
23
26
|
BadBigint,
|
|
24
27
|
TooDeep,
|
|
25
|
-
///
|
|
26
|
-
///
|
|
27
|
-
///
|
|
28
|
-
|
|
29
|
-
///
|
|
30
|
-
|
|
28
|
+
/// An object repeats a key and `allow_duplicate_key` is off (json
|
|
29
|
+
/// 3 semantics). From `DupKeys` it may be a fingerprint collision,
|
|
30
|
+
/// which the exact cold-path check rules out.
|
|
31
|
+
DuplicateKey,
|
|
32
|
+
/// A string or key decodes to a lone UTF-16 surrogate (json 3
|
|
33
|
+
/// rejects trailing ones too, not only leading ones).
|
|
34
|
+
LoneSurrogate,
|
|
31
35
|
/// The reformat pipe met a non-finite float without `allow_nan`.
|
|
32
36
|
/// Parsing accepts huge-exponent literals like 1e999 as Infinity
|
|
33
37
|
/// even in strict mode (gem parity), but generation refuses them;
|
|
@@ -35,24 +39,160 @@ pub(crate) enum SinkAbort {
|
|
|
35
39
|
NonFiniteFloat(&'static str),
|
|
36
40
|
}
|
|
37
41
|
|
|
38
|
-
///
|
|
39
|
-
///
|
|
40
|
-
///
|
|
41
|
-
///
|
|
42
|
-
///
|
|
43
|
-
///
|
|
44
|
-
///
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
42
|
+
/// Duplicate-key detection for sinks that build no Hash (validation and
|
|
43
|
+
/// the reformat pipe): each key is remembered as a 64-bit fingerprint,
|
|
44
|
+
/// `mark()` at a container's start is the fingerprint count, and an
|
|
45
|
+
/// object's close checks only its own keys. Integers, because comparing
|
|
46
|
+
/// them is what keeps this cheap: comparing the key bytes themselves
|
|
47
|
+
/// measured up to 93% slower (twitter's 40-key objects). A fingerprint
|
|
48
|
+
/// collision reads as a duplicate, so callers confirm a hit exactly on
|
|
49
|
+
/// the cold path; a collision can only cost time.
|
|
50
|
+
///
|
|
51
|
+
/// Measured split (twitter `valid?`): hashing and pushing each key is
|
|
52
|
+
/// ~0-2%; the close-time check is the cost, hence [`SeenTable`].
|
|
53
|
+
pub(crate) struct DupKeys<'a> {
|
|
54
|
+
scratch: &'a mut DupScratch,
|
|
55
|
+
enabled: bool,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// [`DupKeys`]' pooled buffers (per thread, in `PullState`): the key
|
|
59
|
+
/// fingerprints of every open object, and the close-time set.
|
|
60
|
+
#[derive(Default)]
|
|
61
|
+
pub(crate) struct DupScratch {
|
|
62
|
+
fingerprints: Vec<u64>,
|
|
63
|
+
table: SeenTable,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/// Fixed seeds: fingerprints only need to spread keys, and a collision
|
|
67
|
+
/// merely sends the document to the exact cold-path check. (A leaner
|
|
68
|
+
/// hand-rolled hash measured no faster: hashing is not the cost.)
|
|
69
|
+
const FINGERPRINT: ahash::RandomState = ahash::RandomState::with_seeds(
|
|
70
|
+
0x243f_6a88_85a3_08d3,
|
|
71
|
+
0x1319_8a2e_0370_7344,
|
|
72
|
+
0xa409_3822_299f_31d0,
|
|
73
|
+
0x082e_fa98_ec4e_6c89,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
/// Objects up to this many keys compare pairwise (a few compares beat
|
|
77
|
+
/// any table traffic).
|
|
78
|
+
const PAIRWISE_MAX: usize = 8;
|
|
79
|
+
/// Table slots; a power of two, so a fingerprint's low bits index it.
|
|
80
|
+
const TABLE_SLOTS: usize = 256;
|
|
81
|
+
/// Objects up to this many keys use the table, which then stays at
|
|
82
|
+
/// least half empty; larger ones sort.
|
|
83
|
+
const TABLE_MAX_KEYS: usize = TABLE_SLOTS / 2;
|
|
84
|
+
|
|
85
|
+
/// Open-addressing set for one object's close: fingerprints are already
|
|
86
|
+
/// uniformly mixed, so their low bits index it directly, and one probe
|
|
87
|
+
/// per key usually decides. Each close claims a fresh epoch, and a slot
|
|
88
|
+
/// is empty unless it carries the current one, so nothing is cleared
|
|
89
|
+
/// between objects.
|
|
90
|
+
struct SeenTable {
|
|
91
|
+
slots: Box<[(u64, u32)]>,
|
|
92
|
+
epoch: u32,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
impl Default for SeenTable {
|
|
96
|
+
fn default() -> Self {
|
|
97
|
+
SeenTable {
|
|
98
|
+
slots: vec![(0, 0); TABLE_SLOTS].into_boxed_slice(),
|
|
99
|
+
epoch: 0,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
impl SeenTable {
|
|
105
|
+
/// Whether `keys` repeats a fingerprint. Callers keep
|
|
106
|
+
/// `keys.len() <= TABLE_MAX_KEYS`, so a free slot always exists.
|
|
107
|
+
#[inline(always)]
|
|
108
|
+
fn any_repeat(&mut self, keys: &[u64]) -> bool {
|
|
109
|
+
const MASK: usize = TABLE_SLOTS - 1;
|
|
110
|
+
self.epoch = self.epoch.wrapping_add(1);
|
|
111
|
+
if self.epoch == 0 {
|
|
112
|
+
// Epoch 0 marks an empty slot; after a full wrap, really clear.
|
|
113
|
+
self.slots.fill((0, 0));
|
|
114
|
+
self.epoch = 1;
|
|
115
|
+
}
|
|
116
|
+
let epoch = self.epoch;
|
|
117
|
+
for &fp in keys {
|
|
118
|
+
let mut at = fp as usize & MASK;
|
|
119
|
+
loop {
|
|
120
|
+
let slot = &mut self.slots[at];
|
|
121
|
+
if slot.1 != epoch {
|
|
122
|
+
*slot = (fp, epoch);
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
if slot.0 == fp {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
at = (at + 1) & MASK;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
false
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
impl<'a> DupKeys<'a> {
|
|
136
|
+
pub(crate) fn new(scratch: &'a mut DupScratch, enabled: bool) -> Self {
|
|
137
|
+
scratch.fingerprints.clear();
|
|
138
|
+
DupKeys { scratch, enabled }
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
#[inline(always)]
|
|
142
|
+
pub(crate) fn mark(&self) -> usize {
|
|
143
|
+
self.scratch.fingerprints.len()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
#[inline(always)]
|
|
147
|
+
pub(crate) fn key(&mut self, key: &[u8]) {
|
|
148
|
+
if self.enabled {
|
|
149
|
+
self.scratch.fingerprints.push(FINGERPRINT.hash_one(key));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/// Close the object whose keys start at `mark`.
|
|
154
|
+
#[inline(always)]
|
|
155
|
+
pub(crate) fn close(&mut self, mark: usize) -> Result<(), SinkAbort> {
|
|
156
|
+
if !self.enabled {
|
|
157
|
+
return Ok(());
|
|
158
|
+
}
|
|
159
|
+
let DupScratch {
|
|
160
|
+
fingerprints,
|
|
161
|
+
table,
|
|
162
|
+
} = &mut *self.scratch;
|
|
163
|
+
let keys = &mut fingerprints[mark..];
|
|
164
|
+
let repeated = match keys.len() {
|
|
165
|
+
n if n <= PAIRWISE_MAX => (1..n).any(|i| keys[..i].contains(&keys[i])),
|
|
166
|
+
n if n <= TABLE_MAX_KEYS => table.any_repeat(keys),
|
|
167
|
+
_ => {
|
|
168
|
+
keys.sort_unstable();
|
|
169
|
+
keys.windows(2).any(|w| w[0] == w[1])
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
fingerprints.truncate(mark);
|
|
173
|
+
if repeated {
|
|
174
|
+
Err(SinkAbort::DuplicateKey)
|
|
175
|
+
} else {
|
|
176
|
+
Ok(())
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/// Integer VALUE for `i` via rb-sys's inline `LONG2NUM` (the header
|
|
182
|
+
/// macro: a Fixnum tagged inline when it fits, else a Bignum), which
|
|
183
|
+
/// saves the FFI call per integer that `rb_ll2inum` costs. The fixable
|
|
184
|
+
/// range is defined by the C `long`, which is 32-bit on Windows (LLP64):
|
|
185
|
+
/// fixnums there hold only 31 bits, and tagging anything wider crashes
|
|
186
|
+
/// Ruby with "Unnormalized Fixnum value"; values beyond `long` take
|
|
187
|
+
/// `rb_ll2inum`.
|
|
188
|
+
// The conversion is an identity on LP64 hosts (clippy flags it there)
|
|
189
|
+
// but narrows on Windows, where c_long is 32-bit.
|
|
190
|
+
#[allow(clippy::useless_conversion, clippy::unnecessary_fallible_conversions)]
|
|
48
191
|
#[inline(always)]
|
|
49
192
|
fn int_to_raw(i: i64) -> rb_sys::VALUE {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
((i as u64) << 1).wrapping_add(1) as rb_sys::VALUE
|
|
54
|
-
} else {
|
|
55
|
-
unsafe { rb_sys::rb_ll2inum(i) }
|
|
193
|
+
match std::os::raw::c_long::try_from(i) {
|
|
194
|
+
Ok(l) => rb_sys::macros::LONG2NUM(l),
|
|
195
|
+
Err(_) => unsafe { rb_sys::rb_ll2inum(i) },
|
|
56
196
|
}
|
|
57
197
|
}
|
|
58
198
|
|
|
@@ -146,6 +286,7 @@ pub(crate) struct RubyValueSink<'a> {
|
|
|
146
286
|
pub(crate) symbolize: bool,
|
|
147
287
|
pub(crate) freeze: bool,
|
|
148
288
|
pub(crate) max_nesting: usize,
|
|
289
|
+
pub(crate) allow_duplicate_key: bool,
|
|
149
290
|
}
|
|
150
291
|
|
|
151
292
|
// Tried and rejected (2026-07-10): a jiter-style cache of repeated VALUE
|
|
@@ -228,35 +369,14 @@ impl nosj::Sink for RubyValueSink<'_> {
|
|
|
228
369
|
self.push_raw(raw)
|
|
229
370
|
}
|
|
230
371
|
|
|
231
|
-
/// Lone-
|
|
232
|
-
///
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
let raw = unsafe {
|
|
236
|
-
let s = rb_sys::rb_utf8_str_new(
|
|
237
|
-
value.as_ptr() as *const std::os::raw::c_char,
|
|
238
|
-
value.len() as std::os::raw::c_long,
|
|
239
|
-
);
|
|
240
|
-
if self.freeze {
|
|
241
|
-
rb_sys::rb_str_freeze(s)
|
|
242
|
-
} else {
|
|
243
|
-
s
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
self.push_raw(raw)
|
|
372
|
+
/// Lone-surrogate content (the crate hands it over as WTF-8): json 3
|
|
373
|
+
/// rejects it, trailing surrogates included.
|
|
374
|
+
fn str_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
375
|
+
Err(SinkAbort::LoneSurrogate)
|
|
247
376
|
}
|
|
248
377
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
// Interning is skipped: these keys are pathological, not hot.
|
|
252
|
-
let raw = unsafe {
|
|
253
|
-
rb_sys::rb_utf8_str_new(
|
|
254
|
-
key.as_ptr() as *const std::os::raw::c_char,
|
|
255
|
-
key.len() as std::os::raw::c_long,
|
|
256
|
-
)
|
|
257
|
-
};
|
|
258
|
-
let frozen = unsafe { rb_sys::rb_str_freeze(raw) };
|
|
259
|
-
self.push_raw(frozen)
|
|
378
|
+
fn key_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
379
|
+
Err(SinkAbort::LoneSurrogate)
|
|
260
380
|
}
|
|
261
381
|
|
|
262
382
|
#[inline(always)]
|
|
@@ -314,19 +434,28 @@ impl nosj::Sink for RubyValueSink<'_> {
|
|
|
314
434
|
}
|
|
315
435
|
}
|
|
316
436
|
self.stack.truncate(mark);
|
|
437
|
+
// A repeated key collapses into one entry: the hash comes out
|
|
438
|
+
// smaller than the pair count (one size read per object).
|
|
439
|
+
if !self.allow_duplicate_key
|
|
440
|
+
&& (unsafe { rb_sys::macros::RHASH_SIZE(hash_raw) } as usize) < pairs
|
|
441
|
+
{
|
|
442
|
+
return Err(SinkAbort::DuplicateKey);
|
|
443
|
+
}
|
|
317
444
|
self.push_raw(hash_raw)
|
|
318
445
|
}
|
|
319
446
|
}
|
|
320
447
|
|
|
321
448
|
/// Validation-only sink: every event is a no-op except nesting-depth
|
|
322
|
-
/// tracking, so `NOSJ.valid?` runs the
|
|
323
|
-
/// string decode, number validation) without
|
|
324
|
-
|
|
449
|
+
/// tracking and duplicate-key fingerprints, so `NOSJ.valid?` runs the
|
|
450
|
+
/// full parser (tokenizers, string decode, number validation) without
|
|
451
|
+
/// allocating a single VALUE.
|
|
452
|
+
pub(crate) struct NullSink<'a> {
|
|
325
453
|
pub(crate) depth: usize,
|
|
326
454
|
pub(crate) max_nesting: usize,
|
|
455
|
+
pub(crate) dup_keys: DupKeys<'a>,
|
|
327
456
|
}
|
|
328
457
|
|
|
329
|
-
impl nosj::Sink for NullSink {
|
|
458
|
+
impl nosj::Sink for NullSink<'_> {
|
|
330
459
|
type Error = SinkAbort;
|
|
331
460
|
|
|
332
461
|
fn null(&mut self) -> Result<(), SinkAbort> {
|
|
@@ -347,14 +476,18 @@ impl nosj::Sink for NullSink {
|
|
|
347
476
|
fn str(&mut self, _: &str) -> Result<(), SinkAbort> {
|
|
348
477
|
Ok(())
|
|
349
478
|
}
|
|
350
|
-
fn key(&mut self,
|
|
479
|
+
fn key(&mut self, key: &str) -> Result<(), SinkAbort> {
|
|
480
|
+
self.dup_keys.key(key.as_bytes());
|
|
351
481
|
Ok(())
|
|
352
482
|
}
|
|
353
483
|
fn str_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
354
|
-
|
|
484
|
+
Err(SinkAbort::LoneSurrogate)
|
|
485
|
+
}
|
|
486
|
+
fn key_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
|
|
487
|
+
Err(SinkAbort::LoneSurrogate)
|
|
355
488
|
}
|
|
356
489
|
fn mark(&self) -> usize {
|
|
357
|
-
|
|
490
|
+
self.dup_keys.mark()
|
|
358
491
|
}
|
|
359
492
|
fn begin_array(&mut self) -> Result<(), SinkAbort> {
|
|
360
493
|
self.depth += 1;
|
|
@@ -374,8 +507,8 @@ impl nosj::Sink for NullSink {
|
|
|
374
507
|
self.depth -= 1;
|
|
375
508
|
Ok(())
|
|
376
509
|
}
|
|
377
|
-
fn end_object(&mut self,
|
|
510
|
+
fn end_object(&mut self, mark: usize, _: usize) -> Result<(), SinkAbort> {
|
|
378
511
|
self.depth -= 1;
|
|
379
|
-
|
|
512
|
+
self.dup_keys.close(mark)
|
|
380
513
|
}
|
|
381
514
|
}
|