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.
@@ -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::sink::{NullSink, RubyValueSink, SinkAbort, MAX_NESTING};
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: symbolize_names, freeze,
69
- /// max_nesting, allow_nan, allow_trailing_comma. Unsupported gem options
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
- use magnus::value::ReprValue;
73
- use magnus::RHash;
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
- let mut out = ParseNativeOpts::default();
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(out);
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
- let truthy = |name: &str| -> bool {
83
- h.get(ruby.to_symbol(name))
84
- .is_some_and(|v: Value| v.to_bool())
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
- out.symbolize = truthy("symbolize_names");
88
- out.freeze = truthy("freeze");
89
- out.popts.allow_nan = truthy("allow_nan");
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
- if let Some(mn) = h.get(ruby.to_symbol("max_nesting")) {
93
- out.max_nesting =
94
- if mn.is_nil() || mn.to_bool() && magnus::Integer::from_value(mn).is_none() {
95
- MAX_NESTING // nil / true: gem default
96
- } else if !mn.to_bool() {
97
- usize::MAX // false: unlimited
98
- } else {
99
- magnus::Integer::from_value(mn)
100
- .and_then(|i| i.to_u64().ok())
101
- .map_or(MAX_NESTING, |n| n as usize)
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
- for unsupported in [
106
- "object_class",
107
- "array_class",
108
- "decimal_class",
109
- "create_additions",
110
- ] {
111
- if truthy(unsupported) {
112
- return Err(Error::new(
113
- ruby.exception_arg_error(),
114
- format!("NOSJ.parse does not support the {unsupported} option"),
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
- /// Pop the root value off the sink stack, or map a drive failure onto
122
- /// the gem's exceptions. Shared by every driver. `source`/`base` locate
123
- /// the driven bytes within the full document, so ParserError positions
124
- /// stay absolute when a subtree slice was parsed.
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: Result<(), nosj::DriveError<SinkAbort>>,
221
+ result: DriveResult,
128
222
  stack: &mut Vec<rb_sys::VALUE>,
129
- max_nesting: usize,
223
+ o: &ParseNativeOpts,
130
224
  source: &[u8],
131
- base: usize,
225
+ span: (usize, usize),
132
226
  ) -> Result<Value, Error> {
133
- match result {
134
- Ok(()) => {
135
- let raw = stack
136
- .pop()
137
- .unwrap_or(rb_sys::special_consts::Qnil as rb_sys::VALUE);
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.max_nesting, source, start)
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 ok = with_pull_state(|state| {
247
- let mut sink = NullSink {
248
- depth: 0,
249
- max_nesting: o.max_nesting,
250
- };
251
- // Safety: coderange verified by utf8_input.
252
- unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
253
- .is_ok()
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(ok)
322
+ Ok(result.is_ok())
256
323
  }
@@ -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, with two deliberate differences: duplicate object
9
- //! keys pass through (a reformatter must not silently drop data the
10
- //! way parse's last-key-wins materialization does), and lone-surrogate
11
- //! string values re-escape as `\uXXXX` instead of raising (the output
12
- //! must reparse; raw WTF-8 would not). Numbers come out in the gem's
13
- //! canonical spelling (`1.50` becomes `1.5`), and string escapes are
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::{parse_gen_opts, GenConfig, DEFAULT_CONFIG};
26
- use crate::parse::{parse_native_opts, utf8_input, ParseNativeOpts};
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, value: &[u8]) -> Result<(), SinkAbort> {
169
- // Rare path (lone-surrogate content); a per-call buffer is fine.
170
- let mut quoted = Vec::with_capacity(value.len() + 8);
171
- quote_wtf8(&mut quoted, value, self.mode);
172
- self.w.value_raw(&quoted);
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, _key: &[u8]) -> Result<(), SinkAbort> {
182
- // A lone-surrogate KEY has no pre-serialized escape hatch in
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
- 0
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, _: usize, _: usize) -> Result<(), SinkAbort> {
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: parse_native_opts(ruby, opts)?,
244
- generate: if opts.is_nil() {
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
- buf.clear();
261
- // The output is at least input-sized for minify-shaped runs.
262
- buf.reserve(input.len());
263
- let mut sink = PipeSink {
264
- w: Writer::new(buf, &wopts),
265
- depth: 0,
266
- max_nesting: po.max_nesting,
267
- mode: gcfg.mode,
268
- allow_nan: gcfg.allow_nan,
269
- };
270
- let result = with_pull_state(|state| {
271
- // Safety: callers verified UTF-8 (coderange or full scan).
272
- unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, po.popts) }
273
- });
274
- match result {
275
- Ok(()) => finish_string(buf),
276
- Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
277
- ruby,
278
- format!(
279
- "nesting of {} is too deep",
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