nosj 0.2.0 → 0.3.1
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 +112 -0
- data/Cargo.lock +3 -3
- data/README.md +220 -19
- data/ext/nosj/Cargo.toml +2 -2
- data/ext/nosj/fuzz/Cargo.toml +41 -0
- data/ext/nosj/fuzz/fuzz_targets/lines.rs +9 -0
- data/ext/nosj/fuzz/fuzz_targets/patch.rs +10 -0
- data/ext/nosj/fuzz/fuzz_targets/reformat.rs +10 -0
- data/ext/nosj/fuzz/src/lib.rs +48 -0
- data/ext/nosj/fuzz/src/prelude.rb +400 -0
- data/ext/nosj/src/errors.rs +171 -0
- data/ext/nosj/src/files.rs +29 -7
- data/ext/nosj/src/gen/errors.rs +2 -9
- data/ext/nosj/src/gen/mod.rs +156 -12
- data/ext/nosj/src/gen/opts.rs +88 -10
- data/ext/nosj/src/gen/ruby.rs +51 -0
- data/ext/nosj/src/gen/walker.rs +97 -59
- data/ext/nosj/src/lazy.rs +42 -22
- data/ext/nosj/src/lib.rs +30 -0
- data/ext/nosj/src/lines.rs +90 -0
- data/ext/nosj/src/parse.rs +55 -14
- data/ext/nosj/src/patch.rs +547 -0
- data/ext/nosj/src/pointer.rs +12 -5
- data/ext/nosj/src/reformat.rs +320 -0
- data/ext/nosj/src/sink.rs +11 -0
- data/ext/nosj/src/stats.rs +301 -0
- data/lib/nosj/json.rb +30 -6
- data/lib/nosj/multi_json.rb +1 -1
- data/lib/nosj/rails.rb +76 -0
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +351 -5
- data/sig/nosj.rbs +66 -0
- metadata +16 -2
data/ext/nosj/src/gen/mod.rs
CHANGED
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
mod errors;
|
|
24
24
|
mod hash_iter;
|
|
25
25
|
mod keys;
|
|
26
|
-
mod opts;
|
|
26
|
+
pub(crate) mod opts;
|
|
27
27
|
mod ruby;
|
|
28
28
|
mod walker;
|
|
29
29
|
|
|
@@ -46,12 +46,17 @@ use walker::Gen;
|
|
|
46
46
|
struct GenScratch {
|
|
47
47
|
buf: Vec<u8>,
|
|
48
48
|
keys: GenKeyCache,
|
|
49
|
+
/// Keys pre-escaped under HtmlSafe (the Rails encoder): cached
|
|
50
|
+
/// bytes bake in the escape mode, so each cacheable mode owns a
|
|
51
|
+
/// cache (see walker::mode_cacheable).
|
|
52
|
+
html_keys: GenKeyCache,
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
thread_local! {
|
|
52
56
|
static GEN_SCRATCH: RefCell<GenScratch> = RefCell::new(GenScratch {
|
|
53
57
|
buf: Vec::new(),
|
|
54
58
|
keys: GenKeyCache::with_capacity(256),
|
|
59
|
+
html_keys: GenKeyCache::with_capacity(64),
|
|
55
60
|
});
|
|
56
61
|
}
|
|
57
62
|
|
|
@@ -86,6 +91,29 @@ pub fn generate_entry(ruby: &Ruby, _rb_self: Value, args: &[Value]) -> Result<RS
|
|
|
86
91
|
generate_scratched(ruby, obj, cfg, cap_hint)
|
|
87
92
|
}
|
|
88
93
|
|
|
94
|
+
/// `NOSJ.generate_rails_native(obj, escape_html, escape_js)`: compact
|
|
95
|
+
/// generation under ActiveSupport walk semantics (non-native values
|
|
96
|
+
/// recurse through as_json, non-finite floats emit null). Every escape
|
|
97
|
+
/// flag combination maps to a crate escape mode, so HTML/JS-safety
|
|
98
|
+
/// escaping is always fused into the emission kernels: one pass, no
|
|
99
|
+
/// post-processing. The Rails encoder installed by
|
|
100
|
+
/// `require "nosj/rails"` is the only caller.
|
|
101
|
+
pub fn generate_rails_native(
|
|
102
|
+
ruby: &Ruby,
|
|
103
|
+
_rb_self: Value,
|
|
104
|
+
obj: Value,
|
|
105
|
+
escape_html: bool,
|
|
106
|
+
escape_js: bool,
|
|
107
|
+
) -> Result<RString, Error> {
|
|
108
|
+
let cfg = match (escape_html, escape_js) {
|
|
109
|
+
(true, true) => &opts::RAILS_HTML_SAFE_CONFIG,
|
|
110
|
+
(true, false) => &opts::RAILS_HTML_ENTITIES_CONFIG,
|
|
111
|
+
(false, true) => &opts::RAILS_JS_SEPARATORS_CONFIG,
|
|
112
|
+
(false, false) => &opts::RAILS_CONFIG,
|
|
113
|
+
};
|
|
114
|
+
generate_scratched(ruby, obj, cfg, 0)
|
|
115
|
+
}
|
|
116
|
+
|
|
89
117
|
/// `NOSJ.generate_native(obj, opts)`: the fixed-arity entry kept for
|
|
90
118
|
/// the Ruby-level wrappers (pretty_generate merges options first).
|
|
91
119
|
pub fn generate_native(
|
|
@@ -149,15 +177,18 @@ fn generate_scratched_into<R>(
|
|
|
149
177
|
GEN_SCRATCH.with(|cell| match cell.try_borrow_mut() {
|
|
150
178
|
Ok(mut scratch) => {
|
|
151
179
|
let scratch = &mut *scratch;
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
180
|
+
let GenScratch {
|
|
181
|
+
buf,
|
|
182
|
+
keys,
|
|
183
|
+
html_keys,
|
|
184
|
+
..
|
|
185
|
+
} = scratch;
|
|
186
|
+
let keys = if cfg.mode == nosj::emit::EscapeMode::HtmlSafe {
|
|
187
|
+
html_keys
|
|
188
|
+
} else {
|
|
189
|
+
keys
|
|
190
|
+
};
|
|
191
|
+
generate_with(ruby, obj, cfg, cap_hint, buf, keys, finish)
|
|
161
192
|
}
|
|
162
193
|
Err(_) => {
|
|
163
194
|
let mut buf = Vec::new();
|
|
@@ -180,7 +211,21 @@ fn generate_with<R>(
|
|
|
180
211
|
if out.capacity() < cap_hint {
|
|
181
212
|
out.reserve(cap_hint);
|
|
182
213
|
}
|
|
214
|
+
emit_one(ruby, obj, cfg, out, keys)?;
|
|
215
|
+
finish(ruby, out)
|
|
216
|
+
}
|
|
183
217
|
|
|
218
|
+
/// Emit one value into `out` (appending, not clearing), mapping walker
|
|
219
|
+
/// failures onto the gem's exceptions. `inline(always)`: this is the
|
|
220
|
+
/// single-document hot path's only call layer.
|
|
221
|
+
#[inline(always)]
|
|
222
|
+
fn emit_one(
|
|
223
|
+
ruby: &Ruby,
|
|
224
|
+
obj: Value,
|
|
225
|
+
cfg: &opts::GenConfig,
|
|
226
|
+
out: &mut Vec<u8>,
|
|
227
|
+
keys: &mut GenKeyCache,
|
|
228
|
+
) -> Result<(), Error> {
|
|
184
229
|
let mut g = Gen {
|
|
185
230
|
out,
|
|
186
231
|
cfg,
|
|
@@ -193,9 +238,9 @@ fn generate_with<R>(
|
|
|
193
238
|
g.emit_value::<false>(obj.as_raw(), cfg.start_depth)
|
|
194
239
|
};
|
|
195
240
|
|
|
196
|
-
let Gen {
|
|
241
|
+
let Gen { fail, .. } = g;
|
|
197
242
|
match (result, fail) {
|
|
198
|
-
(Ok(()), None) =>
|
|
243
|
+
(Ok(()), None) => Ok(()),
|
|
199
244
|
(_, Some(fail)) => Err(raise_fail(ruby, fail)),
|
|
200
245
|
(Err(()), None) => Err(Error::new(
|
|
201
246
|
ruby.exception_runtime_error(),
|
|
@@ -203,3 +248,102 @@ fn generate_with<R>(
|
|
|
203
248
|
)),
|
|
204
249
|
}
|
|
205
250
|
}
|
|
251
|
+
|
|
252
|
+
/// Emit one value into `out` under `cfg`, borrowing the pooled key
|
|
253
|
+
/// cache. The splice/patch entries use this to generate replacement
|
|
254
|
+
/// values straight into their output buffer.
|
|
255
|
+
pub(crate) fn emit_into(
|
|
256
|
+
ruby: &Ruby,
|
|
257
|
+
obj: Value,
|
|
258
|
+
cfg: &opts::GenConfig,
|
|
259
|
+
out: &mut Vec<u8>,
|
|
260
|
+
) -> Result<(), Error> {
|
|
261
|
+
GEN_SCRATCH.with(|cell| match cell.try_borrow_mut() {
|
|
262
|
+
Ok(mut scratch) => emit_one(ruby, obj, cfg, out, &mut scratch.keys),
|
|
263
|
+
Err(_) => emit_one(ruby, obj, cfg, out, &mut GenKeyCache::default()),
|
|
264
|
+
})
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// Formatting strings holding a newline (or carriage return) would
|
|
268
|
+
/// split one value across NDJSON lines; the lines entries refuse them.
|
|
269
|
+
fn breaks_line_framing(cfg: &opts::GenConfig) -> bool {
|
|
270
|
+
[
|
|
271
|
+
&cfg.indent,
|
|
272
|
+
&cfg.space,
|
|
273
|
+
&cfg.space_before,
|
|
274
|
+
&cfg.object_nl,
|
|
275
|
+
&cfg.array_nl,
|
|
276
|
+
]
|
|
277
|
+
.into_iter()
|
|
278
|
+
.any(|v| v.iter().any(|&b| b == b'\n' || b == b'\r'))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/// `NOSJ.generate_lines_native(values, opts)`: NDJSON generation, one
|
|
282
|
+
/// compact document per element into ONE pooled buffer, newline after
|
|
283
|
+
/// each (every line terminated, per the format).
|
|
284
|
+
pub fn generate_lines_native(
|
|
285
|
+
ruby: &Ruby,
|
|
286
|
+
_rb_self: Value,
|
|
287
|
+
values: magnus::RArray,
|
|
288
|
+
opts: Value,
|
|
289
|
+
) -> Result<RString, Error> {
|
|
290
|
+
generate_lines_bytes_into(ruby, values, opts, finish_rstring)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/// The write-to-disk form of [`generate_lines_native`], used by
|
|
294
|
+
/// `NOSJ.write_lines`.
|
|
295
|
+
pub(crate) fn generate_lines_bytes_into<R>(
|
|
296
|
+
ruby: &Ruby,
|
|
297
|
+
values: magnus::RArray,
|
|
298
|
+
opts: Value,
|
|
299
|
+
finish: impl FnOnce(&Ruby, &[u8]) -> Result<R, Error>,
|
|
300
|
+
) -> Result<R, Error> {
|
|
301
|
+
use magnus::value::ReprValue;
|
|
302
|
+
let built;
|
|
303
|
+
let (cfg, cap_hint): (&opts::GenConfig, usize) = if opts.is_nil() {
|
|
304
|
+
(&opts::DEFAULT_CONFIG, 0)
|
|
305
|
+
} else {
|
|
306
|
+
let (c, hint) = opts::parse_gen_opts(ruby, opts)?;
|
|
307
|
+
built = c;
|
|
308
|
+
(&built, hint)
|
|
309
|
+
};
|
|
310
|
+
if breaks_line_framing(cfg) {
|
|
311
|
+
return Err(Error::new(
|
|
312
|
+
ruby.exception_arg_error(),
|
|
313
|
+
"formatting options containing newlines would break JSON Lines framing",
|
|
314
|
+
));
|
|
315
|
+
}
|
|
316
|
+
GEN_SCRATCH.with(|cell| match cell.try_borrow_mut() {
|
|
317
|
+
Ok(mut scratch) => {
|
|
318
|
+
let scratch = &mut *scratch;
|
|
319
|
+
let GenScratch { buf, keys, .. } = scratch;
|
|
320
|
+
generate_lines_with(ruby, values, cfg, cap_hint, buf, keys, finish)
|
|
321
|
+
}
|
|
322
|
+
Err(_) => {
|
|
323
|
+
let mut buf = Vec::new();
|
|
324
|
+
let mut keys = GenKeyCache::default();
|
|
325
|
+
generate_lines_with(ruby, values, cfg, cap_hint, &mut buf, &mut keys, finish)
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
fn generate_lines_with<R>(
|
|
331
|
+
ruby: &Ruby,
|
|
332
|
+
values: magnus::RArray,
|
|
333
|
+
cfg: &opts::GenConfig,
|
|
334
|
+
cap_hint: usize,
|
|
335
|
+
out: &mut Vec<u8>,
|
|
336
|
+
keys: &mut GenKeyCache,
|
|
337
|
+
finish: impl FnOnce(&Ruby, &[u8]) -> Result<R, Error>,
|
|
338
|
+
) -> Result<R, Error> {
|
|
339
|
+
out.clear();
|
|
340
|
+
if out.capacity() < cap_hint {
|
|
341
|
+
out.reserve(cap_hint);
|
|
342
|
+
}
|
|
343
|
+
for i in 0..values.len() {
|
|
344
|
+
let obj: Value = values.entry(i as isize)?;
|
|
345
|
+
emit_one(ruby, obj, cfg, out, keys)?;
|
|
346
|
+
out.push(b'\n');
|
|
347
|
+
}
|
|
348
|
+
finish(ruby, out)
|
|
349
|
+
}
|
data/ext/nosj/src/gen/opts.rs
CHANGED
|
@@ -5,18 +5,23 @@ use magnus::value::ReprValue;
|
|
|
5
5
|
use magnus::{Error, RHash, RString, Ruby, Value};
|
|
6
6
|
use nosj::emit::EscapeMode;
|
|
7
7
|
|
|
8
|
-
pub(
|
|
9
|
-
pub(
|
|
10
|
-
pub(
|
|
11
|
-
pub(
|
|
12
|
-
pub(
|
|
13
|
-
pub(
|
|
8
|
+
pub(crate) struct GenConfig {
|
|
9
|
+
pub(crate) indent: Vec<u8>,
|
|
10
|
+
pub(crate) space: Vec<u8>,
|
|
11
|
+
pub(crate) space_before: Vec<u8>,
|
|
12
|
+
pub(crate) object_nl: Vec<u8>,
|
|
13
|
+
pub(crate) array_nl: Vec<u8>,
|
|
14
14
|
/// 0 = unlimited.
|
|
15
15
|
pub(super) max_nesting: usize,
|
|
16
16
|
pub(super) start_depth: usize,
|
|
17
|
-
pub(
|
|
17
|
+
pub(crate) allow_nan: bool,
|
|
18
18
|
pub(super) strict: bool,
|
|
19
|
-
|
|
19
|
+
/// ActiveSupport walk semantics: non-native values recurse through
|
|
20
|
+
/// as_json instead of splicing to_json, and non-finite floats emit
|
|
21
|
+
/// null (Float#as_json parity). Set only by the Rails entry, never
|
|
22
|
+
/// from user option hashes.
|
|
23
|
+
pub(super) rails: bool,
|
|
24
|
+
pub(crate) mode: EscapeMode,
|
|
20
25
|
/// Precomputed "any formatting string set": scanning the five
|
|
21
26
|
/// vectors per call was measurable on tiny documents.
|
|
22
27
|
pub(super) pretty: bool,
|
|
@@ -27,7 +32,7 @@ pub(super) struct GenConfig {
|
|
|
27
32
|
/// on tiny documents (the json gem likewise reuses a cached State for
|
|
28
33
|
/// the default options). Safe as a static: `Vec::new` is const and
|
|
29
34
|
/// allocation-free, and generation only ever borrows the config.
|
|
30
|
-
pub(
|
|
35
|
+
pub(crate) static DEFAULT_CONFIG: GenConfig = GenConfig {
|
|
31
36
|
indent: Vec::new(),
|
|
32
37
|
space: Vec::new(),
|
|
33
38
|
space_before: Vec::new(),
|
|
@@ -37,6 +42,78 @@ pub(super) static DEFAULT_CONFIG: GenConfig = GenConfig {
|
|
|
37
42
|
start_depth: 0,
|
|
38
43
|
allow_nan: false,
|
|
39
44
|
strict: false,
|
|
45
|
+
rails: false,
|
|
46
|
+
mode: EscapeMode::Standard,
|
|
47
|
+
pretty: false,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/// The Rails-encoder configuration for ActiveSupport's default escape
|
|
51
|
+
/// flags (HTML entities and JS separators both on, the overwhelmingly
|
|
52
|
+
/// common case): escaping is fused into the crate's HtmlSafe kernels,
|
|
53
|
+
/// one pass, no post-scan.
|
|
54
|
+
pub(super) static RAILS_HTML_SAFE_CONFIG: GenConfig = GenConfig {
|
|
55
|
+
indent: Vec::new(),
|
|
56
|
+
space: Vec::new(),
|
|
57
|
+
space_before: Vec::new(),
|
|
58
|
+
object_nl: Vec::new(),
|
|
59
|
+
array_nl: Vec::new(),
|
|
60
|
+
max_nesting: 100,
|
|
61
|
+
start_depth: 0,
|
|
62
|
+
allow_nan: false,
|
|
63
|
+
strict: false,
|
|
64
|
+
rails: true,
|
|
65
|
+
mode: EscapeMode::HtmlSafe,
|
|
66
|
+
pretty: false,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
/// Rails-encoder configuration with HTML entities on and JS separators
|
|
70
|
+
/// off.
|
|
71
|
+
pub(super) static RAILS_HTML_ENTITIES_CONFIG: GenConfig = GenConfig {
|
|
72
|
+
indent: Vec::new(),
|
|
73
|
+
space: Vec::new(),
|
|
74
|
+
space_before: Vec::new(),
|
|
75
|
+
object_nl: Vec::new(),
|
|
76
|
+
array_nl: Vec::new(),
|
|
77
|
+
max_nesting: 100,
|
|
78
|
+
start_depth: 0,
|
|
79
|
+
allow_nan: false,
|
|
80
|
+
strict: false,
|
|
81
|
+
rails: true,
|
|
82
|
+
mode: EscapeMode::HtmlEntities,
|
|
83
|
+
pretty: false,
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
/// Rails-encoder configuration with JS separators on and HTML entities
|
|
87
|
+
/// off.
|
|
88
|
+
pub(super) static RAILS_JS_SEPARATORS_CONFIG: GenConfig = GenConfig {
|
|
89
|
+
indent: Vec::new(),
|
|
90
|
+
space: Vec::new(),
|
|
91
|
+
space_before: Vec::new(),
|
|
92
|
+
object_nl: Vec::new(),
|
|
93
|
+
array_nl: Vec::new(),
|
|
94
|
+
max_nesting: 100,
|
|
95
|
+
start_depth: 0,
|
|
96
|
+
allow_nan: false,
|
|
97
|
+
strict: false,
|
|
98
|
+
rails: true,
|
|
99
|
+
mode: EscapeMode::JsSeparators,
|
|
100
|
+
pretty: false,
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
/// The Rails-encoder configuration with every escape flag off
|
|
104
|
+
/// (encode(escape: false)). Mirrors JSONGemEncoder#stringify, which
|
|
105
|
+
/// generates with the json gem's defaults.
|
|
106
|
+
pub(super) static RAILS_CONFIG: GenConfig = GenConfig {
|
|
107
|
+
indent: Vec::new(),
|
|
108
|
+
space: Vec::new(),
|
|
109
|
+
space_before: Vec::new(),
|
|
110
|
+
object_nl: Vec::new(),
|
|
111
|
+
array_nl: Vec::new(),
|
|
112
|
+
max_nesting: 100,
|
|
113
|
+
start_depth: 0,
|
|
114
|
+
allow_nan: false,
|
|
115
|
+
strict: false,
|
|
116
|
+
rails: true,
|
|
40
117
|
mode: EscapeMode::Standard,
|
|
41
118
|
pretty: false,
|
|
42
119
|
};
|
|
@@ -53,6 +130,7 @@ impl Default for GenConfig {
|
|
|
53
130
|
start_depth: 0,
|
|
54
131
|
allow_nan: false,
|
|
55
132
|
strict: false,
|
|
133
|
+
rails: false,
|
|
56
134
|
mode: EscapeMode::Standard,
|
|
57
135
|
pretty: false,
|
|
58
136
|
}
|
|
@@ -86,7 +164,7 @@ fn opt_bool(ruby: &Ruby, opts: RHash, name: &str) -> Option<bool> {
|
|
|
86
164
|
|
|
87
165
|
/// Decode a non-nil options hash (nil takes [`DEFAULT_CONFIG`] at the
|
|
88
166
|
/// call site without constructing anything).
|
|
89
|
-
pub(
|
|
167
|
+
pub(crate) fn parse_gen_opts(ruby: &Ruby, opts: Value) -> Result<(GenConfig, usize), Error> {
|
|
90
168
|
let mut cfg = GenConfig::default();
|
|
91
169
|
let mut cap_hint = 0usize;
|
|
92
170
|
if opts.is_nil() {
|
data/ext/nosj/src/gen/ruby.rs
CHANGED
|
@@ -23,6 +23,57 @@ pub(super) fn protected_to_json(v: VALUE) -> Result<VALUE, Error> {
|
|
|
23
23
|
magnus::rb_sys::protect(|| unsafe { rb_sys::rb_funcall(v, to_json_id(), 0) })
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
+
/// `v.as_json`, protected. Argument-less on purpose: ActiveSupport's
|
|
27
|
+
/// JSONGemEncoder#jsonify recursion also calls as_json without
|
|
28
|
+
/// options (only the top-level value receives them).
|
|
29
|
+
pub(super) fn protected_as_json(v: VALUE) -> Result<VALUE, Error> {
|
|
30
|
+
magnus::rb_sys::protect(|| unsafe { rb_sys::rb_funcall(v, as_json_id(), 0) })
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Interned `as_json` method ID, resolved once per process.
|
|
34
|
+
fn as_json_id() -> rb_sys::ID {
|
|
35
|
+
static AS_JSON: OnceLock<usize> = OnceLock::new();
|
|
36
|
+
*AS_JSON.get_or_init(|| unsafe { rb_sys::rb_intern(c"as_json".as_ptr()) } as usize)
|
|
37
|
+
as rb_sys::ID
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/// Whether `v` is a `JSON::Fragment` (pre-rendered JSON to splice
|
|
41
|
+
/// verbatim: the gem accepts fragments even under `strict`, and
|
|
42
|
+
/// ActiveSupport's encoder passes them through). The class is resolved
|
|
43
|
+
/// lazily and cached only on success, so a json gem loaded after the
|
|
44
|
+
/// first generate is still found; a fragment instance existing implies
|
|
45
|
+
/// its class does. The cached VALUE is a constant of the JSON module,
|
|
46
|
+
/// so it can never be collected.
|
|
47
|
+
pub(super) fn is_json_fragment(v: VALUE) -> bool {
|
|
48
|
+
use std::sync::atomic::{AtomicUsize, Ordering};
|
|
49
|
+
static FRAGMENT: AtomicUsize = AtomicUsize::new(0);
|
|
50
|
+
let mut cls = FRAGMENT.load(Ordering::Relaxed);
|
|
51
|
+
if cls == 0 {
|
|
52
|
+
cls = resolve_json_fragment();
|
|
53
|
+
if cls == 0 {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
FRAGMENT.store(cls, Ordering::Relaxed);
|
|
57
|
+
}
|
|
58
|
+
unsafe { rb_sys::rb_obj_is_kind_of(v, cls as VALUE) != QFALSE }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
fn resolve_json_fragment() -> usize {
|
|
62
|
+
unsafe {
|
|
63
|
+
let object = rb_sys::rb_cObject;
|
|
64
|
+
let json_id = rb_sys::rb_intern(c"JSON".as_ptr());
|
|
65
|
+
if rb_sys::rb_const_defined(object, json_id) == 0 {
|
|
66
|
+
return 0;
|
|
67
|
+
}
|
|
68
|
+
let json = rb_sys::rb_const_get(object, json_id);
|
|
69
|
+
let fragment_id = rb_sys::rb_intern(c"Fragment".as_ptr());
|
|
70
|
+
if rb_sys::rb_const_defined(json, fragment_id) == 0 {
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
|
73
|
+
rb_sys::rb_const_get(json, fragment_id) as usize
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
26
77
|
/// Encode `v` to UTF-8, protected. `rb_str_encode` raises on
|
|
27
78
|
/// undefined/invalid conversions, matching the gem, which wraps that
|
|
28
79
|
/// exception as GeneratorError (`rb_str_export_to_enc` is lenient and
|
data/ext/nosj/src/gen/walker.rs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//! Compact and pretty modes are one const-generic body, so the compact
|
|
4
4
|
//! hot path carries no formatting branches.
|
|
5
5
|
|
|
6
|
-
use nosj::emit::{self, EscapeMode};
|
|
6
|
+
use nosj::emit::{self, copy_short_raw, EscapeMode};
|
|
7
7
|
use rb_sys::macros::{
|
|
8
8
|
FIX2LONG, FIXNUM_P, FLONUM_P, RARRAY_CONST_PTR, RARRAY_LEN, RB_BUILTIN_TYPE, RHASH_SIZE,
|
|
9
9
|
STATIC_SYM_P,
|
|
@@ -14,52 +14,16 @@ use super::errors::GenFail;
|
|
|
14
14
|
use super::keys::GenKeyCache;
|
|
15
15
|
use super::opts::GenConfig;
|
|
16
16
|
use super::ruby::{
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
QTRUE,
|
|
17
|
+
is_json_fragment, is_special_const, protected_as_json, protected_encode_utf8,
|
|
18
|
+
protected_to_json, protected_to_s, rstring_bytes, str_coderange, str_enc_index, to_json_id,
|
|
19
|
+
utf8_encindexes, CR_7BIT, CR_VALID, QFALSE, QNIL, QTRUE,
|
|
20
20
|
};
|
|
21
21
|
|
|
22
|
-
///
|
|
23
|
-
///
|
|
24
|
-
///
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
/// rare enough for the memcpy fallback.
|
|
28
|
-
///
|
|
29
|
-
/// # Safety
|
|
30
|
-
///
|
|
31
|
-
/// `n` readable bytes at `src`, `n` writable bytes at `dst`, and the
|
|
32
|
-
/// ranges must not overlap.
|
|
33
|
-
#[inline(always)]
|
|
34
|
-
unsafe fn copy_short_raw(src: *const u8, dst: *mut u8, n: usize) {
|
|
35
|
-
/// One overlapping pair: word-size chunks at offset 0 and at
|
|
36
|
-
/// `n - size` cover `size..=2*size` bytes; the overlapped middle
|
|
37
|
-
/// is written twice with identical data.
|
|
38
|
-
macro_rules! word_pair {
|
|
39
|
-
($t:ty) => {{
|
|
40
|
-
const SIZE: usize = size_of::<$t>();
|
|
41
|
-
dst.cast::<$t>()
|
|
42
|
-
.write_unaligned(src.cast::<$t>().read_unaligned());
|
|
43
|
-
dst.add(n - SIZE)
|
|
44
|
-
.cast::<$t>()
|
|
45
|
-
.write_unaligned(src.add(n - SIZE).cast::<$t>().read_unaligned());
|
|
46
|
-
}};
|
|
47
|
-
}
|
|
48
|
-
if n >= 16 {
|
|
49
|
-
if n <= 32 {
|
|
50
|
-
word_pair!(u128);
|
|
51
|
-
} else {
|
|
52
|
-
std::ptr::copy_nonoverlapping(src, dst, n);
|
|
53
|
-
}
|
|
54
|
-
} else if n >= 8 {
|
|
55
|
-
word_pair!(u64);
|
|
56
|
-
} else if n >= 4 {
|
|
57
|
-
word_pair!(u32);
|
|
58
|
-
} else if n >= 2 {
|
|
59
|
-
word_pair!(u16);
|
|
60
|
-
} else if n == 1 {
|
|
61
|
-
*dst = *src;
|
|
62
|
-
}
|
|
22
|
+
/// Whether keys escaped under `mode` may be cached: the cached bytes
|
|
23
|
+
/// bake in the escape mode, so the scratch keeps one cache per
|
|
24
|
+
/// cacheable mode and hands `Gen` the matching one.
|
|
25
|
+
pub(super) fn mode_cacheable(mode: EscapeMode) -> bool {
|
|
26
|
+
matches!(mode, EscapeMode::Standard | EscapeMode::HtmlSafe)
|
|
63
27
|
}
|
|
64
28
|
|
|
65
29
|
pub(super) struct Gen<'a> {
|
|
@@ -69,6 +33,7 @@ pub(super) struct Gen<'a> {
|
|
|
69
33
|
pub(super) fail: Option<GenFail>,
|
|
70
34
|
/// Pre-escaped key cache, borrowed for the whole document (one
|
|
71
35
|
/// thread-local borrow per generate call instead of one per key).
|
|
36
|
+
/// Always the cache matching `cfg.mode` (see [`mode_cacheable`]).
|
|
72
37
|
pub(super) keys: &'a mut GenKeyCache,
|
|
73
38
|
}
|
|
74
39
|
|
|
@@ -149,6 +114,11 @@ impl Gen<'_> {
|
|
|
149
114
|
emit::write_f64(&mut *self.out, f);
|
|
150
115
|
return Ok(());
|
|
151
116
|
}
|
|
117
|
+
if self.cfg.rails {
|
|
118
|
+
// ActiveSupport's Float#as_json: non-finite floats are null.
|
|
119
|
+
self.out.extend_from_slice(b"null");
|
|
120
|
+
return Ok(());
|
|
121
|
+
}
|
|
152
122
|
let name = if f.is_nan() {
|
|
153
123
|
"NaN"
|
|
154
124
|
} else if f > 0.0 {
|
|
@@ -166,13 +136,14 @@ impl Gen<'_> {
|
|
|
166
136
|
}
|
|
167
137
|
|
|
168
138
|
/// Emit an object key from the pre-escaped cache. Only frozen string
|
|
169
|
-
/// keys in
|
|
170
|
-
/// content behind the VALUE can't change, and the cached bytes bake
|
|
171
|
-
/// the escape mode
|
|
139
|
+
/// keys in a cacheable escape mode qualify: frozen guarantees the
|
|
140
|
+
/// content behind the VALUE can't change, and the cached bytes bake
|
|
141
|
+
/// in the escape mode, so each cacheable mode gets its own cache
|
|
142
|
+
/// instance from the scratch (see [`mode_cacheable`]).
|
|
172
143
|
fn emit_key_cached(&mut self, k: VALUE) -> Result<(), ()> {
|
|
173
144
|
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
174
145
|
let frozen = unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0;
|
|
175
|
-
if !frozen || self.cfg.mode
|
|
146
|
+
if !frozen || !mode_cacheable(self.cfg.mode) {
|
|
176
147
|
return self.emit_rstring_quoted(k);
|
|
177
148
|
}
|
|
178
149
|
if let Some(bytes) = self.keys.get(k) {
|
|
@@ -190,13 +161,13 @@ impl Gen<'_> {
|
|
|
190
161
|
}
|
|
191
162
|
|
|
192
163
|
/// The pre-escaped bytes for `k` when the cache may serve it:
|
|
193
|
-
/// frozen string key,
|
|
194
|
-
///
|
|
195
|
-
///
|
|
164
|
+
/// frozen string key, cacheable escape mode (see
|
|
165
|
+
/// [`Gen::emit_key_cached`]). An associated fn over the split-out
|
|
166
|
+
/// fields so callers keep `self.out` free.
|
|
196
167
|
#[inline(always)]
|
|
197
168
|
fn cached_key_bytes<'k>(cfg: &GenConfig, keys: &'k GenKeyCache, k: VALUE) -> Option<&'k [u8]> {
|
|
198
169
|
const FL_FREEZE: u64 = rb_sys::ruby_fl_type::RUBY_FL_FREEZE as u64;
|
|
199
|
-
if cfg.mode
|
|
170
|
+
if mode_cacheable(cfg.mode)
|
|
200
171
|
&& !is_special_const(k)
|
|
201
172
|
&& unsafe { RB_BUILTIN_TYPE(k) } == ruby_value_type::RUBY_T_STRING
|
|
202
173
|
&& unsafe { (*(k as *const rb_sys::RBasic)).flags } & FL_FREEZE != 0
|
|
@@ -303,11 +274,20 @@ impl Gen<'_> {
|
|
|
303
274
|
}
|
|
304
275
|
}
|
|
305
276
|
|
|
306
|
-
/// Non-native type: strict raises
|
|
307
|
-
///
|
|
308
|
-
///
|
|
309
|
-
|
|
277
|
+
/// Non-native type: strict raises (except `JSON::Fragment`, which
|
|
278
|
+
/// the gem splices even under strict); otherwise `to_json` if the
|
|
279
|
+
/// object responds (result appended verbatim), else `to_s` as a
|
|
280
|
+
/// JSON string, which is exactly what the gem's `Object#to_json`
|
|
281
|
+
/// does. Rails mode recurses through `as_json` instead
|
|
282
|
+
/// (JSONGemEncoder#jsonify).
|
|
283
|
+
fn emit_fallback<const PRETTY: bool>(&mut self, raw: VALUE, depth: usize) -> Result<(), ()> {
|
|
284
|
+
if self.cfg.rails {
|
|
285
|
+
return self.emit_rails_fallback::<PRETTY>(raw, depth);
|
|
286
|
+
}
|
|
310
287
|
if self.cfg.strict {
|
|
288
|
+
if is_json_fragment(raw) {
|
|
289
|
+
return self.splice_to_json(raw);
|
|
290
|
+
}
|
|
311
291
|
let name = unsafe {
|
|
312
292
|
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
313
293
|
.to_string_lossy()
|
|
@@ -341,6 +321,64 @@ impl Gen<'_> {
|
|
|
341
321
|
}
|
|
342
322
|
}
|
|
343
323
|
|
|
324
|
+
/// Splice `raw`'s `to_json` result verbatim: the JSON::Fragment
|
|
325
|
+
/// path (pre-rendered JSON, trusted like the gem trusts it).
|
|
326
|
+
fn splice_to_json(&mut self, raw: VALUE) -> Result<(), ()> {
|
|
327
|
+
match protected_to_json(raw) {
|
|
328
|
+
Ok(json)
|
|
329
|
+
if !is_special_const(json)
|
|
330
|
+
&& unsafe { RB_BUILTIN_TYPE(json) } == ruby_value_type::RUBY_T_STRING =>
|
|
331
|
+
{
|
|
332
|
+
self.append_rstring_raw(json);
|
|
333
|
+
Ok(())
|
|
334
|
+
}
|
|
335
|
+
Ok(_) => {
|
|
336
|
+
self.fail = Some(GenFail::Generator(
|
|
337
|
+
"JSON::Fragment#to_json did not return a String".to_string(),
|
|
338
|
+
));
|
|
339
|
+
Err(())
|
|
340
|
+
}
|
|
341
|
+
Err(exc) => {
|
|
342
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
343
|
+
Err(())
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/// Rails-mode fallback, mirroring JSONGemEncoder#jsonify:
|
|
349
|
+
/// fragments splice through (like ActiveSupport passes them to the
|
|
350
|
+
/// gem); everything else is asked for its as_json representation
|
|
351
|
+
/// (no arguments; only the top-level value receives the encoder
|
|
352
|
+
/// options), which is emitted in its place. An as_json returning
|
|
353
|
+
/// the receiver would recurse forever, so it raises instead.
|
|
354
|
+
fn emit_rails_fallback<const PRETTY: bool>(
|
|
355
|
+
&mut self,
|
|
356
|
+
raw: VALUE,
|
|
357
|
+
depth: usize,
|
|
358
|
+
) -> Result<(), ()> {
|
|
359
|
+
if is_json_fragment(raw) {
|
|
360
|
+
return self.splice_to_json(raw);
|
|
361
|
+
}
|
|
362
|
+
match protected_as_json(raw) {
|
|
363
|
+
Ok(json) if json == raw => {
|
|
364
|
+
let name = unsafe {
|
|
365
|
+
std::ffi::CStr::from_ptr(rb_sys::rb_obj_classname(raw))
|
|
366
|
+
.to_string_lossy()
|
|
367
|
+
.into_owned()
|
|
368
|
+
};
|
|
369
|
+
self.fail = Some(GenFail::Generator(format!(
|
|
370
|
+
"{name}#as_json returned the receiver"
|
|
371
|
+
)));
|
|
372
|
+
Err(())
|
|
373
|
+
}
|
|
374
|
+
Ok(json) => self.emit_value::<PRETTY>(json, depth),
|
|
375
|
+
Err(exc) => {
|
|
376
|
+
self.fail = Some(GenFail::Reraise(exc));
|
|
377
|
+
Err(())
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
344
382
|
fn nesting_check(&mut self, inner: usize) -> Result<(), ()> {
|
|
345
383
|
if self.cfg.max_nesting > 0 && inner > self.cfg.max_nesting {
|
|
346
384
|
self.fail = Some(GenFail::Nesting(self.cfg.max_nesting));
|
|
@@ -601,7 +639,7 @@ impl Gen<'_> {
|
|
|
601
639
|
let s = unsafe { rb_sys::rb_sym2str(raw) };
|
|
602
640
|
self.emit_rstring_quoted(s)
|
|
603
641
|
}
|
|
604
|
-
_ => self.emit_fallback(raw),
|
|
642
|
+
_ => self.emit_fallback::<PRETTY>(raw, depth),
|
|
605
643
|
};
|
|
606
644
|
}
|
|
607
645
|
if FIXNUM_P(raw) {
|
|
@@ -633,6 +671,6 @@ impl Gen<'_> {
|
|
|
633
671
|
let s = unsafe { rb_sys::rb_sym2str(raw) };
|
|
634
672
|
return self.emit_rstring_quoted(s);
|
|
635
673
|
}
|
|
636
|
-
self.emit_fallback(raw)
|
|
674
|
+
self.emit_fallback::<PRETTY>(raw, depth)
|
|
637
675
|
}
|
|
638
676
|
}
|