iodine 0.8.2 → 0.8.3

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.
@@ -33,7 +33,268 @@ Ruby API (Iodine::JSON):
33
33
  JSON Stringifier - Ruby to JSON String Conversion
34
34
  ***************************************************************************** */
35
35
 
36
- static char *iodine_json_stringify_key(char *dest, VALUE tmp) {
36
+ /** Minimal-overhead fio_bstr writer for the JSON stringifier.
37
+ *
38
+ * Mirrors the bstr's len/capa, so the hot path is an inline capacity check
39
+ * followed by a raw write - avoiding the per-call overhead of the generic
40
+ * fio_bstr_write API (which validates capacity on every single write).
41
+ * Growth goes through fio_bstr_reserve with added slack (capa/4) for
42
+ * geometric, amortized O(1) appends.
43
+ *
44
+ * NOTE: all writes MUST go through these helpers - len/capa are mirrors.
45
+ */
46
+ typedef struct {
47
+ char *s; /* the fio_bstr (may be NULL) */
48
+ size_t len; /* mirrors fio_bstr_len(s) */
49
+ size_t capa; /* mirrors fio_bstr_info(s).capa (0 when s is NULL) */
50
+ } iodine_json_w_s;
51
+
52
+ /** Ensures room for `add` more bytes (plus the NUL terminator).
53
+ * Returns non-zero on success, zero on allocation failure (write skipped,
54
+ * truncating the output - same semantics as fio_bstr_write). */
55
+ FIO_IFUNC int iodine_json_w_need(iodine_json_w_s *w, size_t add) {
56
+ if (FIO_LIKELY(w->capa - w->len > add))
57
+ return 1;
58
+ /* sync the bstr metadata (our len is a mirror) before reserve reads it */
59
+ w->s = fio_bstr_len_set(w->s, w->len);
60
+ /* grow with geometric slack (capa/4) - amortized O(1) appends */
61
+ char *n = fio_bstr_reserve(w->s, add + (w->capa >> 2) + 1);
62
+ fio_str_info_s i = fio_bstr_info(n);
63
+ if (FIO_UNLIKELY(i.capa - i.len <= add))
64
+ return 0; /* allocation failed */
65
+ w->s = n;
66
+ w->capa = i.capa; /* i.len == w->len (mirrors) */
67
+ return 1;
68
+ }
69
+
70
+ FIO_IFUNC void iodine_json_w_write(iodine_json_w_s *w,
71
+ const void *src,
72
+ size_t len) {
73
+ if (FIO_LIKELY(iodine_json_w_need(w, len))) {
74
+ FIO_MEMCPY(w->s + w->len, src, len);
75
+ w->len += len;
76
+ }
77
+ }
78
+
79
+ FIO_IFUNC void iodine_json_w_ch(iodine_json_w_s *w, const char c) {
80
+ if (FIO_LIKELY(iodine_json_w_need(w, 1)))
81
+ w->s[w->len++] = c;
82
+ }
83
+
84
+ FIO_IFUNC void iodine_json_w_i(iodine_json_w_s *w, int64_t num) {
85
+ /* write the number directly into the buffer (max 20 digits + sign + NUL) */
86
+ if (FIO_LIKELY(iodine_json_w_need(w, 24)))
87
+ w->len += fio_ltoa(w->s + w->len, num, 10);
88
+ }
89
+
90
+ /** Writes `num` to `dest` (up to 39 bytes + NUL), matching the output of
91
+ * fio_ftoa(dest, num, 10) - i.e., snprintf("%g") with the ".0" fixup -
92
+ * without the snprintf overhead.
93
+ *
94
+ * Falls back to fio_ftoa for non-finite values, extreme exponents, and
95
+ * values landing on a rounding boundary (where the exact snprintf
96
+ * half-to-even rounding of the exact decimal expansion is required).
97
+ *
98
+ * NOTE: uses the C STL's internal fio___aton_dec2d for correctly-rounded
99
+ * powers of ten. If a future fio-stl.h sync renames it, this breaks loudly
100
+ * at compile time (safe). */
101
+ static size_t iodine_json_ftoa(char *dest, double num) {
102
+ if (FIO_UNLIKELY(!isfinite(num)))
103
+ return fio_ftoa(dest, num, 10); /* "NaN" / "Infinity" / "-Infinity" */
104
+ char *p = dest;
105
+ double a = num;
106
+ uint64_t bits;
107
+ memcpy(&bits, &num, 8);
108
+ if (bits >> 63) {
109
+ *p++ = '-';
110
+ a = -a;
111
+ }
112
+ if (a == 0) { /* %g prints "0" (or "-0"), fio_ftoa adds ".0" */
113
+ memcpy(p, "0.0", 4);
114
+ return (size_t)(p + 3 - dest);
115
+ }
116
+ /* decimal exponent estimate: a = m * 2^e2, m in [0.5, 1) */
117
+ int e2;
118
+ frexp(a, &e2);
119
+ int e10 = (int)(((int64_t)e2 * 78913) >> 18); /* ~floor(log10(a)) +/- 1 */
120
+ if (FIO_UNLIKELY(e10 < -302 || e10 > 302))
121
+ return fio_ftoa(dest, num, 10); /* pow10 out of table range */
122
+ /* scale to a 6 digit mantissa (q ~= a * 10^(5-e10) in [1e5, 1e6)) */
123
+ double q = a * fio___aton_dec2d(1, 5 - e10);
124
+ if (q >= 1e6 || q < 1e5) { /* correct the estimate (off by one at most) */
125
+ e10 += (q >= 1e6) ? 1 : -1;
126
+ q = a * fio___aton_dec2d(1, 5 - e10);
127
+ }
128
+ double fl = floor(q);
129
+ if (FIO_UNLIKELY(q - fl > 0.4999999 && q - fl < 0.5000001))
130
+ return fio_ftoa(dest, num, 10); /* rounding boundary - exact tie break */
131
+ uint64_t r = (uint64_t)fl + (q - fl >= 0.5);
132
+ if (FIO_UNLIKELY(r == 1000000)) { /* rounded up to the next power of 10 */
133
+ r = 100000;
134
+ ++e10;
135
+ }
136
+ /* the 6 digits of r (d.ddddd * 10^e10) */
137
+ char dig[6];
138
+ dig[5] = (char)('0' + (r % 10));
139
+ r /= 10;
140
+ dig[4] = (char)('0' + (r % 10));
141
+ r /= 10;
142
+ dig[3] = (char)('0' + (r % 10));
143
+ r /= 10;
144
+ dig[2] = (char)('0' + (r % 10));
145
+ r /= 10;
146
+ dig[1] = (char)('0' + (r % 10));
147
+ r /= 10;
148
+ dig[0] = (char)('0' + r);
149
+ if (e10 >= -4 && e10 < 6) {
150
+ /* %f style (trimming trailing zeros after the decimal point) */
151
+ if (e10 >= 0) {
152
+ const int idig = e10 + 1; /* integer digits (1..6) */
153
+ memcpy(p, dig, (size_t)idig);
154
+ p += idig;
155
+ int f = 5;
156
+ while (f >= idig && dig[f] == '0')
157
+ --f;
158
+ if (f >= idig) {
159
+ *p++ = '.';
160
+ memcpy(p, dig + idig, (size_t)(f - idig + 1));
161
+ p += f - idig + 1;
162
+ } else {
163
+ memcpy(p, ".0", 3); /* fio_ftoa's ".0" fixup */
164
+ p += 2;
165
+ }
166
+ } else {
167
+ memcpy(p, "0.", 2);
168
+ p += 2;
169
+ for (int z = -e10 - 1; z--;)
170
+ *p++ = '0';
171
+ int f = 5;
172
+ while (f > 0 && dig[f] == '0')
173
+ --f;
174
+ memcpy(p, dig, (size_t)(f + 1));
175
+ p += f + 1;
176
+ }
177
+ } else {
178
+ /* %e style: d[.ddddd]e+XX (trimming trailing zeros in the mantissa) */
179
+ *p++ = dig[0];
180
+ int f = 5;
181
+ while (f > 0 && dig[f] == '0')
182
+ --f;
183
+ if (f) {
184
+ *p++ = '.';
185
+ memcpy(p, dig + 1, (size_t)f);
186
+ p += f;
187
+ }
188
+ *p++ = 'e';
189
+ *p++ = (e10 < 0) ? '-' : '+';
190
+ int x = (e10 < 0) ? -e10 : e10;
191
+ if (x >= 100)
192
+ *p++ = (char)('0' + (x / 100));
193
+ *p++ = (char)('0' + ((x / 10) % 10));
194
+ *p++ = (char)('0' + (x % 10));
195
+ }
196
+ *p = 0;
197
+ return (size_t)(p - dest);
198
+ }
199
+
200
+ FIO_IFUNC void iodine_json_w_f(iodine_json_w_s *w, double num) {
201
+ /* write the float directly into the buffer (max 39 bytes + NUL) */
202
+ if (FIO_LIKELY(iodine_json_w_need(w, 40)))
203
+ w->len += iodine_json_ftoa(w->s + w->len, num);
204
+ }
205
+
206
+ /** SWAR escape detection: returns non-zero if any byte in the word needs
207
+ * escaping (< 0x20, '"', '\\', 0x7F) or UTF-8 validation (>= 0x80).
208
+ * Byte-order independent (mask is only tested against zero). */
209
+ FIO_IFUNC uint64_t iodine_json_swar_dirty(uint64_t v) {
210
+ const uint64_t hi = 0x8080808080808080ULL;
211
+ uint64_t dirty = (v - 0x2020202020202020ULL) & ~v & hi; /* bytes < 0x20 */
212
+ uint64_t x = v ^ 0x2222222222222222ULL; /* '"' */
213
+ dirty |= (x - 0x0101010101010101ULL) & ~x & hi;
214
+ x = v ^ 0x5C5C5C5C5C5C5C5CULL; /* '\\' */
215
+ dirty |= (x - 0x0101010101010101ULL) & ~x & hi;
216
+ x = v ^ 0x7F7F7F7F7F7F7F7FULL; /* 0x7F */
217
+ dirty |= (x - 0x0101010101010101ULL) & ~x & hi;
218
+ return dirty | (v & hi); /* bytes >= 0x80 */
219
+ }
220
+
221
+ /** Writes a quoted JSON string, escaping as needed (single pass).
222
+ * Escape semantics identical to fio_bstr_write_escape: simple escapes for
223
+ * \b \f \n \r \t \\ \", \u00XX for control characters (incl. 0x7F),
224
+ * valid UTF-8 sequences copied as-is, invalid bytes escaped as \xXX. */
225
+ static void iodine_json_w_string(iodine_json_w_s *w,
226
+ const char *src,
227
+ size_t len) {
228
+ const uint8_t *s = (const uint8_t *)src;
229
+ const uint8_t *e = s + len;
230
+ const uint8_t *run = s;
231
+ const uint8_t *p = s;
232
+ iodine_json_w_ch(w, '"');
233
+ while (p < e) {
234
+ /* SWAR fast scan: skip 16 clean bytes at a time */
235
+ while ((size_t)(e - p) >= 16) {
236
+ uint64_t v0, v1;
237
+ memcpy(&v0, p, 8);
238
+ memcpy(&v1, p + 8, 8);
239
+ if (iodine_json_swar_dirty(v0) | iodine_json_swar_dirty(v1))
240
+ break;
241
+ p += 16;
242
+ }
243
+ /* byte-scan the dirty block (or the tail) */
244
+ const uint8_t *block_end = ((size_t)(e - p) >= 16) ? p + 16 : e;
245
+ while (p < block_end) {
246
+ const uint8_t c = *p;
247
+ /* fast path: printable ASCII (0x20..0x7E), except '"' and '\\' */
248
+ if (FIO_LIKELY((uint8_t)(c - 0x20) < (uint8_t)0x5F && c != '"' &&
249
+ c != '\\')) {
250
+ ++p;
251
+ continue;
252
+ }
253
+ const size_t utf8_len = fio_utf8_char_len(p);
254
+ if (utf8_len > 1 && (size_t)(e - p) >= utf8_len) {
255
+ p += utf8_len; /* valid UTF-8 sequence - copied as-is with the run */
256
+ continue;
257
+ }
258
+ /* (a truncated sequence at the end of the input is escaped, byte-wise) */
259
+ /* flush the clean run and escape this byte */
260
+ iodine_json_w_write(w, run, (size_t)(p - run));
261
+ ++p;
262
+ run = p;
263
+ switch (c) {
264
+ case '\b': iodine_json_w_write(w, "\\b", 2); break;
265
+ case '\f': iodine_json_w_write(w, "\\f", 2); break;
266
+ case '\n': iodine_json_w_write(w, "\\n", 2); break;
267
+ case '\r': iodine_json_w_write(w, "\\r", 2); break;
268
+ case '\t': iodine_json_w_write(w, "\\t", 2); break;
269
+ case '\\': iodine_json_w_write(w, "\\\\", 2); break;
270
+ case '"': iodine_json_w_write(w, "\\\"", 2); break;
271
+ default: {
272
+ /* control characters: \u00XX - invalid UTF-8 bytes: \xXX */
273
+ char buf[6] = {'\\',
274
+ 'u',
275
+ '0',
276
+ '0',
277
+ (char)fio_i2c((unsigned char)(c >> 4)),
278
+ (char)fio_i2c((unsigned char)(c & 15))};
279
+ if (c & 128) {
280
+ buf[1] = 'x';
281
+ buf[2] = buf[4];
282
+ buf[3] = buf[5];
283
+ iodine_json_w_write(w, buf, 4);
284
+ } else {
285
+ iodine_json_w_write(w, buf, 6);
286
+ }
287
+ } break;
288
+ }
289
+ }
290
+ }
291
+ iodine_json_w_write(w, run, (size_t)(e - run));
292
+ iodine_json_w_ch(w, '"');
293
+ }
294
+
295
+ static void iodine_json_w_value(iodine_json_w_s *w, VALUE o);
296
+
297
+ static void iodine_json_w_key(iodine_json_w_s *w, VALUE tmp) {
37
298
  /* Note: keys MUST be Strings */
38
299
  switch (rb_type(tmp)) {
39
300
  case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
@@ -42,126 +303,130 @@ static char *iodine_json_stringify_key(char *dest, VALUE tmp) {
42
303
  case RUBY_T_SYMBOL: tmp = rb_sym2str(tmp); /* fall through */
43
304
  case RUBY_T_STRING:
44
305
  string_key:
45
- dest = fio_bstr_write(dest, "\"", 1);
46
- dest =
47
- fio_bstr_write_escape(dest, RSTRING_PTR(tmp), (size_t)RSTRING_LEN(tmp));
48
- dest = fio_bstr_write(dest, "\"", 1);
306
+ iodine_json_w_string(w, RSTRING_PTR(tmp), (size_t)RSTRING_LEN(tmp));
49
307
  break;
50
308
  case RUBY_T_FIXNUM:
51
- dest = fio_bstr_write2(dest,
52
- FIO_STRING_WRITE_STR2("\"", 1),
53
- FIO_STRING_WRITE_NUM(NUM2LL(tmp)),
54
- FIO_STRING_WRITE_STR2("\"", 1));
309
+ iodine_json_w_ch(w, '"');
310
+ iodine_json_w_i(w, NUM2LL(tmp));
311
+ iodine_json_w_ch(w, '"');
55
312
  break;
56
- case RUBY_T_TRUE: dest = fio_bstr_write(dest, "\"true\"", 6); break;
57
- case RUBY_T_FALSE: dest = fio_bstr_write(dest, "\"false\"", 7); break;
58
- case RUBY_T_NIL: dest = fio_bstr_write(dest, "\"null\"", 6); break;
313
+ case RUBY_T_TRUE: iodine_json_w_write(w, "\"true\"", 6); break;
314
+ case RUBY_T_FALSE: iodine_json_w_write(w, "\"false\"", 7); break;
315
+ case RUBY_T_NIL: iodine_json_w_write(w, "\"null\"", 6); break;
59
316
  default:
60
317
  tmp = rb_any_to_s(tmp);
61
318
  if (RB_TYPE_P(tmp, RUBY_T_STRING))
62
319
  goto string_key;
63
- dest = fio_bstr_write(dest, "\"error\"", 7);
320
+ iodine_json_w_write(w, "\"error\"", 7);
64
321
  }
65
- return dest;
66
322
  }
67
323
 
68
- static char *iodine_json_stringify2bstr(char *dest, VALUE o);
69
-
70
- static int iodine_json_hash_each_callback(VALUE k, VALUE v, VALUE dest_) {
71
- char **pdest = (char **)dest_;
72
- *pdest = iodine_json_stringify_key(*pdest, k);
73
- *pdest = fio_bstr_write(*pdest, ":", 1);
74
- *pdest = iodine_json_stringify2bstr(*pdest, v);
75
- *pdest = fio_bstr_write(*pdest, ",", 1);
324
+ static int iodine_json_hash_each_callback(VALUE k, VALUE v, VALUE w_) {
325
+ iodine_json_w_s *w = (iodine_json_w_s *)w_;
326
+ iodine_json_w_key(w, k);
327
+ iodine_json_w_ch(w, ':');
328
+ iodine_json_w_value(w, v);
329
+ iodine_json_w_ch(w, ',');
76
330
  return ST_CONTINUE;
77
331
  }
78
332
 
79
- /* Converts a VALUE to a fio_bstr */
80
- static char *iodine_json_stringify2bstr(char *dest, VALUE o) {
333
+ /* Converts a VALUE to JSON, appending to the writer's bstr. */
334
+ static void iodine_json_w_value(iodine_json_w_s *w, VALUE o) {
81
335
  size_t tmp;
82
336
  switch (rb_type(o)) {
83
- case RUBY_T_NIL: return (dest = fio_bstr_write(dest, "null", 4));
84
- case RUBY_T_TRUE: return (dest = fio_bstr_write(dest, "true", 4));
85
- case RUBY_T_FALSE: return (dest = fio_bstr_write(dest, "false", 5));
337
+ case RUBY_T_NIL: iodine_json_w_write(w, "null", 4); return;
338
+ case RUBY_T_TRUE: iodine_json_w_write(w, "true", 4); return;
339
+ case RUBY_T_FALSE: iodine_json_w_write(w, "false", 5); return;
86
340
  case RUBY_T_ARRAY:
87
341
  tmp = rb_array_len(o);
88
342
  if (!tmp) {
89
- dest = fio_bstr_write(dest, "[]", 2);
90
- return dest;
343
+ iodine_json_w_write(w, "[]", 2);
344
+ return;
91
345
  }
92
- dest = fio_bstr_write(dest, "[", 1);
346
+ iodine_json_w_ch(w, '[');
93
347
  for (size_t i = 0; i < tmp; ++i) {
94
- dest = iodine_json_stringify2bstr(dest, RARRAY_PTR(o)[i]);
95
- dest = fio_bstr_write(dest, ",", 1);
348
+ iodine_json_w_value(w, RARRAY_PTR(o)[i]);
349
+ iodine_json_w_ch(w, ',');
96
350
  }
97
- dest[fio_bstr_len(dest) - 1] = ']';
98
- return dest;
351
+ if (FIO_LIKELY(w->len && w->s[w->len - 1] == ','))
352
+ w->s[w->len - 1] = ']';
353
+ else
354
+ iodine_json_w_ch(w, ']');
355
+ return;
99
356
  case RUBY_T_HASH:
100
357
  tmp = rb_hash_size_num(o);
101
358
  if (!tmp) {
102
- dest = fio_bstr_write(dest, "{}", 2);
103
- return dest;
359
+ iodine_json_w_write(w, "{}", 2);
360
+ return;
104
361
  }
105
- dest = fio_bstr_write(dest, "{", 1);
106
- rb_hash_foreach(o, iodine_json_hash_each_callback, (VALUE)(&dest));
107
- dest[fio_bstr_len(dest) - 1] = '}';
108
- return dest;
109
- case RUBY_T_FIXNUM: return (dest = fio_bstr_write_i(dest, RB_NUM2LL(o)));
362
+ iodine_json_w_ch(w, '{');
363
+ rb_hash_foreach(o, iodine_json_hash_each_callback, (VALUE)w);
364
+ if (FIO_LIKELY(w->len && w->s[w->len - 1] == ','))
365
+ w->s[w->len - 1] = '}';
366
+ else
367
+ iodine_json_w_ch(w, '}');
368
+ return;
369
+ case RUBY_T_FIXNUM: iodine_json_w_i(w, RB_NUM2LL(o)); return;
110
370
  case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
111
371
  o = rb_big2str(o, 10);
112
- return (dest =
113
- fio_bstr_write(dest, RSTRING_PTR(o), (size_t)RSTRING_LEN(o)));
114
- case RUBY_T_FLOAT: {
115
- FIO_STR_INFO_TMP_VAR(buf, 232);
116
- buf.len = fio_ftoa(buf.buf, RFLOAT_VALUE(o), 10);
117
- dest = fio_bstr_write(dest, buf.buf, buf.len);
118
- return dest;
119
- }
372
+ iodine_json_w_write(w, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
373
+ return;
374
+ case RUBY_T_FLOAT: iodine_json_w_f(w, RFLOAT_VALUE(o)); return;
120
375
  case RUBY_T_SYMBOL: o = rb_sym2str(o); /* fall through */
121
376
  case RUBY_T_STRING:
122
377
  add_string:
123
- dest = fio_bstr_write(dest, "\"", 1);
124
- dest = fio_bstr_write_escape(dest, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
125
- dest = fio_bstr_write(dest, "\"", 1);
126
- return dest;
378
+ iodine_json_w_string(w, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
379
+ return;
127
380
  default:
128
381
  if (1) {
129
382
  VALUE tmp = rb_check_funcall(o, IODINE_TO_JSON_ID, 0, NULL);
130
- if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING))
131
- return (dest = fio_bstr_write(dest,
132
- RSTRING_PTR(tmp),
133
- (size_t)RSTRING_LEN(tmp)));
383
+ if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING)) {
384
+ iodine_json_w_write(w, RSTRING_PTR(tmp), (size_t)RSTRING_LEN(tmp));
385
+ return;
386
+ }
134
387
  o = rb_funcallv(o, IODINE_TO_S_ID, 0, NULL);
135
388
  }
136
389
  if (!RB_TYPE_P(o, RUBY_T_STRING)) {
137
390
  FIO_LOG_ERROR("Iodine::JSON.stringify called with an object that doesn't "
138
391
  "respond to #to_s.");
139
- return (dest = fio_bstr_write(dest, "null", 4));
392
+ iodine_json_w_write(w, "null", 4);
393
+ return;
140
394
  }
141
395
  goto add_string;
142
396
  }
143
397
  }
144
398
 
399
+ /* Converts a VALUE to a fio_bstr */
400
+ static char *iodine_json_stringify2bstr(char *dest, VALUE o) {
401
+ iodine_json_w_s w = {.s = dest, .len = 0, .capa = 0};
402
+ if (dest) {
403
+ fio_str_info_s i = fio_bstr_info(dest);
404
+ w.len = i.len;
405
+ w.capa = i.capa;
406
+ }
407
+ iodine_json_w_value(&w, o);
408
+ return fio_bstr_len_set(w.s, w.len);
409
+ }
410
+
145
411
  /* *****************************************************************************
146
412
  JSON Beautifier - Pretty-Printed JSON Output
147
413
  ***************************************************************************** */
148
414
 
149
415
  typedef struct {
150
- char *o;
416
+ iodine_json_w_s w;
151
417
  size_t t;
152
418
  } iodine_json_beautify2bstr_s;
153
419
 
154
- static char *iodine_json_beautify2bstr(iodine_json_beautify2bstr_s *dest,
155
- VALUE o);
420
+ static void iodine_json_beautify2bstr(iodine_json_beautify2bstr_s *d, VALUE o);
156
421
 
157
422
  FIO_IFUNC void iodine_json_beautify2bstr_pad(iodine_json_beautify2bstr_s *d) {
158
423
  if (!d->t)
159
424
  return;
160
- size_t len = fio_bstr_len(d->o);
161
- d->o = fio_bstr_len_set(d->o, len + d->t + 1);
162
- char *tmp = d->o + len;
163
- *tmp++ = '\n';
164
- FIO_MEMSET(tmp, '\t', d->t);
425
+ iodine_json_w_ch(&d->w, '\n');
426
+ if (iodine_json_w_need(&d->w, d->t)) {
427
+ FIO_MEMSET(d->w.s + d->w.len, '\t', d->t);
428
+ d->w.len += d->t;
429
+ }
165
430
  }
166
431
 
167
432
  static int iodine_json_hash_each_beautify_callback(VALUE k,
@@ -169,85 +434,81 @@ static int iodine_json_hash_each_beautify_callback(VALUE k,
169
434
  VALUE dest_) {
170
435
  iodine_json_beautify2bstr_s *d = (iodine_json_beautify2bstr_s *)dest_;
171
436
  iodine_json_beautify2bstr_pad(d);
172
- d->o = iodine_json_stringify_key(d->o, k);
173
- d->o = fio_bstr_write(d->o, ": ", 2);
437
+ iodine_json_w_key(&d->w, k);
438
+ iodine_json_w_write(&d->w, ": ", 2);
174
439
  iodine_json_beautify2bstr(d, v);
175
- d->o = fio_bstr_write(d->o, ",", 1);
440
+ iodine_json_w_ch(&d->w, ',');
176
441
  return ST_CONTINUE;
177
442
  }
178
443
 
179
444
  /* Converts a VALUE to a fio_bstr */
180
- static char *iodine_json_beautify2bstr(iodine_json_beautify2bstr_s *d,
181
- VALUE o) {
445
+ static void iodine_json_beautify2bstr(iodine_json_beautify2bstr_s *d, VALUE o) {
446
+ iodine_json_w_s *w = &d->w;
182
447
  size_t tmp;
183
448
  switch (rb_type(o)) {
184
- case RUBY_T_NIL: return (d->o = fio_bstr_write(d->o, "null", 4));
185
- case RUBY_T_TRUE: return (d->o = fio_bstr_write(d->o, "true", 4));
186
- case RUBY_T_FALSE: return (d->o = fio_bstr_write(d->o, "false", 5));
449
+ case RUBY_T_NIL: iodine_json_w_write(w, "null", 4); return;
450
+ case RUBY_T_TRUE: iodine_json_w_write(w, "true", 4); return;
451
+ case RUBY_T_FALSE: iodine_json_w_write(w, "false", 5); return;
187
452
  case RUBY_T_ARRAY:
188
453
  tmp = rb_array_len(o);
189
454
  if (!tmp) {
190
- d->o = fio_bstr_write(d->o, "[]", 2);
191
- return d->o;
455
+ iodine_json_w_write(w, "[]", 2);
456
+ return;
192
457
  }
193
- d->o = fio_bstr_write(d->o, "[", 1);
458
+ iodine_json_w_ch(w, '[');
194
459
  ++d->t;
195
460
  for (size_t i = 0; i < tmp; ++i) {
196
461
  iodine_json_beautify2bstr_pad(d);
197
462
  iodine_json_beautify2bstr(d, RARRAY_PTR(o)[i]);
198
- d->o = fio_bstr_write(d->o, ",", 1);
463
+ iodine_json_w_ch(w, ',');
199
464
  }
200
- d->o[fio_bstr_len(d->o) - 1] = '\n';
465
+ if (FIO_LIKELY(w->len && w->s[w->len - 1] == ','))
466
+ w->s[w->len - 1] = '\n';
201
467
  --d->t;
202
468
  iodine_json_beautify2bstr_pad(d);
203
- d->o = fio_bstr_write(d->o, "]", 1);
204
- return d->o;
469
+ iodine_json_w_ch(w, ']');
470
+ return;
205
471
  case RUBY_T_HASH:
206
472
  tmp = rb_hash_size_num(o);
207
473
  if (!tmp) {
208
- d->o = fio_bstr_write(d->o, "{}", 2);
209
- return d->o;
474
+ iodine_json_w_write(w, "{}", 2);
475
+ return;
210
476
  }
211
- d->o = fio_bstr_write(d->o, "{", 1);
477
+ iodine_json_w_ch(w, '{');
212
478
  ++d->t;
213
- rb_hash_foreach(o, iodine_json_hash_each_beautify_callback, (VALUE)(d));
214
- d->o[fio_bstr_len(d->o) - 1] = '\n';
479
+ rb_hash_foreach(o, iodine_json_hash_each_beautify_callback, (VALUE)d);
480
+ if (FIO_LIKELY(w->len && w->s[w->len - 1] == ','))
481
+ w->s[w->len - 1] = '\n';
215
482
  --d->t;
216
483
  iodine_json_beautify2bstr_pad(d);
217
- d->o = fio_bstr_write(d->o, "}", 1);
218
- return d->o;
219
- case RUBY_T_FIXNUM: return (d->o = fio_bstr_write_i(d->o, RB_NUM2LL(o)));
484
+ iodine_json_w_ch(w, '}');
485
+ return;
486
+ case RUBY_T_FIXNUM: iodine_json_w_i(w, RB_NUM2LL(o)); return;
220
487
  case RUBY_T_BIGNUM: /* Bignum (Windows: Integers >= 2^30) */
221
488
  o = rb_big2str(o, 10);
222
- return (d->o =
223
- fio_bstr_write(d->o, RSTRING_PTR(o), (size_t)RSTRING_LEN(o)));
224
- case RUBY_T_FLOAT: {
225
- FIO_STR_INFO_TMP_VAR(buf, 232);
226
- buf.len = fio_ftoa(buf.buf, RFLOAT_VALUE(o), 10);
227
- d->o = fio_bstr_write(d->o, buf.buf, buf.len);
228
- return d->o;
229
- }
489
+ iodine_json_w_write(w, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
490
+ return;
491
+ case RUBY_T_FLOAT: iodine_json_w_f(w, RFLOAT_VALUE(o)); return;
230
492
  case RUBY_T_SYMBOL: o = rb_sym2str(o); /* fall through */
231
493
  case RUBY_T_STRING:
232
494
  add_string:
233
- d->o = fio_bstr_write(d->o, "\"", 1);
234
- d->o = fio_bstr_write_escape(d->o, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
235
- d->o = fio_bstr_write(d->o, "\"", 1);
236
- return d->o;
495
+ iodine_json_w_string(w, RSTRING_PTR(o), (size_t)RSTRING_LEN(o));
496
+ return;
237
497
  default:
238
498
  if (1) {
239
499
  VALUE tmp = rb_check_funcall(o, IODINE_TO_JSON_ID, 0, NULL);
240
- if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING))
241
- return (d->o = fio_bstr_write(d->o,
242
- RSTRING_PTR(tmp),
243
- (size_t)RSTRING_LEN(tmp)));
500
+ if (tmp != RUBY_Qundef && RB_TYPE_P(tmp, RUBY_T_STRING)) {
501
+ iodine_json_w_write(w, RSTRING_PTR(tmp), (size_t)RSTRING_LEN(tmp));
502
+ return;
503
+ }
244
504
  o = rb_funcallv(o, IODINE_TO_S_ID, 0, NULL);
245
505
  if (RB_TYPE_P(o, RUBY_T_STRING))
246
506
  goto add_string;
247
507
  }
248
508
  FIO_LOG_ERROR("Iodine::JSON.stringify called with an object that doesn't "
249
509
  "respond to neither #to_json nor #to_s.");
250
- return (d->o = fio_bstr_write(d->o, "null", 4));
510
+ iodine_json_w_write(w, "null", 4);
511
+ return;
251
512
  }
252
513
  }
253
514
 
@@ -440,35 +701,37 @@ This is the faster parsing path used by Iodine::JSON.parse().
440
701
 
441
702
  /** JSON parser callback implementations for direct Ruby object creation */
442
703
  /** NULL object was detected. Returns new object as `void *`. */
443
- FIO_SFUNC void *iodine___json_on_null(void *udata) {
704
+ FIO_SFUNC fio_json_obj_result_s iodine___json_on_null(void *udata) {
444
705
  (void)udata;
445
- return (void *)Qnil;
706
+ return (fio_json_obj_result_s){.obj = (void *)Qnil};
446
707
  }
447
708
  /** TRUE object was detected. Returns new object as `void *`. */
448
- FIO_SFUNC void *iodine___json_on_true(void *udata) {
709
+ FIO_SFUNC fio_json_obj_result_s iodine___json_on_true(void *udata) {
449
710
  (void)udata;
450
- return (void *)Qtrue;
711
+ return (fio_json_obj_result_s){.obj = (void *)Qtrue};
451
712
  }
452
713
  /** FALSE object was detected. Returns new object as `void *`. */
453
- FIO_SFUNC void *iodine___json_on_false(void *udata) {
714
+ FIO_SFUNC fio_json_obj_result_s iodine___json_on_false(void *udata) {
454
715
  (void)udata;
455
- return (void *)Qfalse;
716
+ return (fio_json_obj_result_s){.obj = (void *)Qfalse};
456
717
  }
457
718
  /** Number was detected (long long). Returns new object as `void *`. */
458
- FIO_SFUNC void *iodine___json_on_number(void *udata, int64_t i) {
719
+ FIO_SFUNC fio_json_obj_result_s iodine___json_on_number(void *udata,
720
+ int64_t i) {
459
721
  (void)udata;
460
- return (void *)(LL2NUM(((long long)i)));
722
+ return (fio_json_obj_result_s){.obj = (void *)LL2NUM(((long long)i))};
461
723
  }
462
724
  /** Float was detected (double).Returns new object as `void *`. */
463
- FIO_SFUNC void *iodine___json_on_float(void *udata, double f) {
725
+ FIO_SFUNC fio_json_obj_result_s iodine___json_on_float(void *udata, double f) {
464
726
  (void)udata;
465
- return (void *)DBL2NUM(f);
727
+ return (fio_json_obj_result_s){.obj = (void *)DBL2NUM(f)};
466
728
  }
467
729
  /** String was detected (int / float). update `pos` to point at ending */
468
- FIO_SFUNC void *iodine___json_on_string(void *udata,
469
- const void *start,
470
- size_t len) {
730
+ FIO_SFUNC fio_json_obj_result_s iodine___json_on_string(void *udata,
731
+ fio_buf_info_s input) {
471
732
  (void)udata;
733
+ const void *start = input.buf;
734
+ size_t len = input.len;
472
735
  VALUE str;
473
736
  if (len < 4096) {
474
737
  FIO_STR_INFO_TMP_VAR(buf, 4096);
@@ -479,35 +742,72 @@ FIO_SFUNC void *iodine___json_on_string(void *udata,
479
742
  str = rb_str_new(tmp, fio_bstr_len(tmp));
480
743
  fio_bstr_free(tmp);
481
744
  }
482
- return (void *)str;
745
+ return (fio_json_obj_result_s){.obj = (void *)str};
483
746
  }
484
- FIO_SFUNC void *iodine___json_on_string_simple(void *udata,
485
- const void *start,
486
- size_t len) {
747
+ FIO_SFUNC fio_json_obj_result_s
748
+ iodine___json_on_string_simple(void *udata, fio_buf_info_s input) {
487
749
  (void)udata;
488
- VALUE str = rb_str_new((const char *)start, len);
489
- return (void *)str;
490
- }
491
- /** Dictionary was detected. Returns ctx to hash map or NULL on error. */
492
- FIO_SFUNC void *iodine___json_on_map(void *udata, void *ctx, void *at) {
493
- (void)udata, (void)ctx, (void)at;
494
- VALUE map = rb_hash_new();
495
- return (void *)map;
496
- }
497
- /** Array was detected. Returns ctx to array or NULL on error. */
498
- FIO_SFUNC void *iodine___json_on_array(void *udata, void *ctx, void *at) {
499
- (void)udata, (void)ctx, (void)at;
500
- VALUE ary = rb_ary_new();
501
- return (void *)ary;
502
- }
503
- /** Map entry detected. Returns non-zero on error. */
504
- FIO_SFUNC int iodine___json_map_push(void *udata,
505
- void *ctx,
506
- void *key,
507
- void *val) {
750
+ VALUE str = rb_str_new(input.buf, input.len);
751
+ return (fio_json_obj_result_s){.obj = (void *)str};
752
+ }
753
+ /** Dictionary opens at `key` in `ctx` (NULL ctx = owned root). Places the
754
+ * map in its parent, reusing an existing Hash at `key` when present
755
+ * (duplicate map keys merge, matching the FIOBJ parser). JSON parsing keeps
756
+ * last-wins semantics: replaced values are NOT reported through `old` -
757
+ * conflict resolution is reserved for the HTTP params layer. */
758
+ FIO_SFUNC fio_json_push_result_s iodine___json_on_map(void *udata,
759
+ void *ctx,
760
+ fio_buf_info_s key) {
508
761
  (void)udata;
509
- rb_hash_aset((VALUE)ctx, (VALUE)key, (VALUE)val);
510
- return 0;
762
+ VALUE map;
763
+ if (!ctx)
764
+ return (fio_json_push_result_s){.obj = (void *)rb_hash_new()};
765
+ if (!key.buf) { /* array parent: append */
766
+ map = rb_hash_new();
767
+ rb_ary_push((VALUE)ctx, map);
768
+ return (fio_json_push_result_s){.obj = (void *)map};
769
+ }
770
+ VALUE k = rb_str_new(key.buf, (long)key.len);
771
+ VALUE cur = rb_hash_lookup2((VALUE)ctx, k, Qundef);
772
+ if (cur != Qundef && RB_TYPE_P(cur, RUBY_T_HASH))
773
+ return (fio_json_push_result_s){
774
+ .obj = (void *)cur}; /* reuse matching map (merge) */
775
+ map = rb_hash_new();
776
+ rb_hash_aset((VALUE)ctx, k, map);
777
+ return (fio_json_push_result_s){.obj = (void *)map};
778
+ }
779
+ /** Array opens at `key` in `ctx` (NULL ctx = owned root). Same placement
780
+ * and reuse contract as iodine___json_on_map (last-wins, `old` unused). */
781
+ FIO_SFUNC fio_json_push_result_s iodine___json_on_array(void *udata,
782
+ void *ctx,
783
+ fio_buf_info_s key) {
784
+ (void)udata;
785
+ VALUE ary;
786
+ if (!ctx)
787
+ return (fio_json_push_result_s){.obj = (void *)rb_ary_new()};
788
+ if (!key.buf) { /* array parent: append */
789
+ ary = rb_ary_new();
790
+ rb_ary_push((VALUE)ctx, ary);
791
+ return (fio_json_push_result_s){.obj = (void *)ary};
792
+ }
793
+ VALUE k = rb_str_new(key.buf, (long)key.len);
794
+ VALUE cur = rb_hash_lookup2((VALUE)ctx, k, Qundef);
795
+ if (cur != Qundef && RB_TYPE_P(cur, RUBY_T_ARRAY))
796
+ return (fio_json_push_result_s){.obj =
797
+ (void *)cur}; /* reuse matching array */
798
+ ary = rb_ary_new();
799
+ rb_hash_aset((VALUE)ctx, k, ary);
800
+ return (fio_json_push_result_s){.obj = (void *)ary};
801
+ }
802
+ /** Map entry detected (`key` is borrowed). Last value wins, matching the
803
+ * FIOBJ parser. Returns no replaced object to the parser. */
804
+ FIO_SFUNC fio_json_obj_result_s iodine___json_map_push(void *udata,
805
+ void *map,
806
+ fio_buf_info_s key,
807
+ void *val) {
808
+ (void)udata;
809
+ rb_hash_aset((VALUE)map, rb_str_new(key.buf, (long)key.len), (VALUE)val);
810
+ return (fio_json_obj_result_s){0};
511
811
  }
512
812
  /** Array entry detected. Returns non-zero on error. */
513
813
  FIO_SFUNC int iodine___json_array_push(void *udata, void *ctx, void *val) {
@@ -515,16 +815,19 @@ FIO_SFUNC int iodine___json_array_push(void *udata, void *ctx, void *val) {
515
815
  rb_ary_push((VALUE)ctx, (VALUE)val);
516
816
  return 0;
517
817
  }
518
- /** Called for the `key` element in case of error or NULL value. */
519
- FIO_SFUNC void iodine___json_free_unused_object(void *udata, void *ctx) {
520
- (void)udata, (void)ctx;
818
+ /** Conflict-resolution type checks (values are Ruby objects). */
819
+ FIO_SFUNC int iodine___json_is_array(void *udata, void *ctx) {
820
+ (void)udata;
821
+ return RB_TYPE_P((VALUE)ctx, RUBY_T_ARRAY);
822
+ }
823
+ FIO_SFUNC int iodine___json_is_map(void *udata, void *ctx) {
824
+ (void)udata;
825
+ return RB_TYPE_P((VALUE)ctx, RUBY_T_HASH);
521
826
  }
522
- /** the JSON parsing encountered an error - what to do with ctx? */
523
- FIO_SFUNC void *iodine___json_on_error(void *udata, void *ctx) {
524
- (void)udata, (void)ctx;
525
- return (void *)Qnil;
827
+ /** Ruby owns values; no manual cleanup is required on parser errors. */
828
+ FIO_SFUNC void iodine___json_free_unused_object(void *udata, void *ctx) {
829
+ (void)udata, (void)ctx; /* Ruby objects are GC managed */
526
830
  }
527
-
528
831
  static fio_json_parser_callbacks_s IODINE_JSON_PARSER_CALLBACKS = {
529
832
  .on_null = iodine___json_on_null,
530
833
  .on_true = iodine___json_on_true,
@@ -537,8 +840,9 @@ static fio_json_parser_callbacks_s IODINE_JSON_PARSER_CALLBACKS = {
537
840
  .on_array = iodine___json_on_array,
538
841
  .map_push = iodine___json_map_push,
539
842
  .array_push = iodine___json_array_push,
843
+ .is_array = iodine___json_is_array,
844
+ .is_map = iodine___json_is_map,
540
845
  .free_unused_object = iodine___json_free_unused_object,
541
- .on_error = iodine___json_on_error,
542
846
  };
543
847
 
544
848
  /* *****************************************************************************
@@ -562,11 +866,12 @@ static VALUE iodine_json_parse(VALUE self, VALUE rstr) {
562
866
  rb_check_type(rstr, RUBY_T_STRING);
563
867
  STORE.gc_stop();
564
868
  fio_json_result_s result = fio_json_parse(&IODINE_JSON_PARSER_CALLBACKS,
869
+ NULL,
565
870
  NULL,
566
871
  RSTRING_PTR(rstr),
567
872
  RSTRING_LEN(rstr));
568
873
  STORE.gc_start();
569
- r = (VALUE)(result.ctx);
874
+ r = result.err ? Qnil : (VALUE)result.ctx;
570
875
  STORE.cache(r);
571
876
  rb_str_buf_new(1); /* force GC to run if Ruby needs it */
572
877
  return r;
@@ -630,12 +935,16 @@ static VALUE iodine_json_pretty(VALUE self, VALUE object) {
630
935
  */
631
936
  static VALUE iodine_json_beautify(VALUE self, VALUE object) {
632
937
  VALUE r = Qnil;
633
- iodine_json_beautify2bstr_s dest = {
634
- .o = fio_bstr_reserve(NULL, ((size_t)1 << 12) - 64),
635
- };
938
+ iodine_json_beautify2bstr_s dest = {.w = {.s = NULL, .len = 0, .capa = 0},
939
+ .t = 0};
940
+ dest.w.s = fio_bstr_reserve(NULL, ((size_t)1 << 12) - 64);
941
+ fio_str_info_s i = fio_bstr_info(dest.w.s);
942
+ dest.w.len = i.len;
943
+ dest.w.capa = i.capa;
636
944
  iodine_json_beautify2bstr(&dest, object);
637
- r = rb_str_new(dest.o, (long)fio_bstr_len(dest.o));
638
- fio_bstr_free(dest.o);
945
+ dest.w.s = fio_bstr_len_set(dest.w.s, dest.w.len);
946
+ r = rb_str_new(dest.w.s, (long)fio_bstr_len(dest.w.s));
947
+ fio_bstr_free(dest.w.s);
639
948
  return r;
640
949
  }
641
950