teptris 0.3.4.1 → 0.3.6.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.
@@ -0,0 +1,575 @@
1
+ /* Streaming TOML writer: an op-stream in, TOML text out.
2
+ *
3
+ * Fuses the builder's DOM construction and the emitter's tree walk
4
+ * into one pass: bindings that walk host objects (Python dicts, JS
5
+ * objects) encode an op-stream instead of calling builder_put_* per
6
+ * value and re-emitting from the DOM. Sections emit as
7
+ * [dotted.headers] / [[aot.headers]] with full paths; arrays and
8
+ * inline tables render inline.
9
+ *
10
+ * Op grammar (little-endian, mirroring the flatten payload styles):
11
+ * 0x00 END-OF-CHUNK
12
+ * 0x01 KEY u8 klen <bytes> (klen 0 clears the pending key)
13
+ * 0x02 STRING u32 len <bytes>
14
+ * 0x03 INT i64
15
+ * 0x04 FLOAT f64
16
+ * 0x05 FALSE
17
+ * 0x06 TRUE
18
+ * 0x07 DATETIME u8 kind(4..7) <25 B, same layout as flatten>
19
+ * 0x08 BEGIN-TABLE section header ([k] / [a.b])
20
+ * 0x09 BEGIN-ARRAY inline array as a value
21
+ * 0x0A BEGIN-AOT array-of-tables ([[k]], elements via BEGIN-TABLE)
22
+ * 0x0B BEGIN-INLINE-TABLE {inline table: array element or keyed value}
23
+ * 0x0C CLOSE
24
+ * 0x0D EMPTY-TABLE (value {})
25
+ * 0x0E EMPTY-ARRAY (value [])
26
+ *
27
+ * Caller contract: within each table, feed every scalar BEFORE any
28
+ * nested table/AOT - TOML forbids keys after a sub-table header, and
29
+ * the writer enforces that with ERR_STATE instead of emitting invalid
30
+ * TOML. */
31
+ #include <stdlib.h>
32
+ #include <string.h>
33
+
34
+ #include "emit_shared.h"
35
+ #include "teptris/teptris.h"
36
+
37
+ enum {
38
+ W_END = 0x00,
39
+ W_KEY = 0x01,
40
+ W_STRING = 0x02,
41
+ W_INT = 0x03,
42
+ W_FLOAT = 0x04,
43
+ W_FALSE = 0x05,
44
+ W_TRUE = 0x06,
45
+ W_DATETIME = 0x07,
46
+ W_BEGIN_TABLE = 0x08,
47
+ W_BEGIN_ARRAY = 0x09,
48
+ W_BEGIN_AOT = 0x0A,
49
+ W_BEGIN_INLINE_TABLE = 0x0B,
50
+ W_CLOSE = 0x0C,
51
+ W_EMPTY_TABLE = 0x0D,
52
+ W_EMPTY_ARRAY = 0x0E
53
+ };
54
+
55
+ enum {
56
+ F_TABLE, /* section table (root or [dotted]) or an AOT element */
57
+ F_ARRAY, /* inline array */
58
+ F_AOT, /* the AOT itself: contributes its key to element paths */
59
+ F_ITAB /* inline table */
60
+ };
61
+
62
+ typedef struct wframe {
63
+ struct wframe *down;
64
+ int kind;
65
+ char *key; /* TBL section component; AOT key; else NULL */
66
+ size_t key_len;
67
+ int first; /* inline separator state */
68
+ int saw_subtable; /* TBL: a nested section was opened under us */
69
+ } wframe;
70
+
71
+ struct teptris_writer {
72
+ ebuf out;
73
+ char *key; /* pending key */
74
+ size_t key_len;
75
+ wframe *top;
76
+ int finished;
77
+ teptris_status st;
78
+ };
79
+
80
+ static wframe *w_push(teptris_writer *w, int kind, const char *k,
81
+ size_t klen);
82
+
83
+ teptris_writer *teptris_writer_new(void)
84
+ {
85
+ teptris_writer *w = calloc(1, sizeof(*w));
86
+ if (w != NULL) {
87
+ /* implicit root table: tracks root-level value/sub-table
88
+ * ordering; never closed by CLOSE */
89
+ w_push(w, F_TABLE, NULL, 0);
90
+ w->st = TEPTRIS_OK;
91
+ }
92
+ return w;
93
+ }
94
+
95
+ void teptris_writer_free(teptris_writer *w)
96
+ {
97
+ if (w == NULL) {
98
+ return;
99
+ }
100
+ while (w->top != NULL) {
101
+ wframe *f = w->top;
102
+ w->top = f->down;
103
+ free(f->key);
104
+ free(f);
105
+ }
106
+ free(w->key);
107
+ free(w->out.p);
108
+ free(w);
109
+ }
110
+
111
+ static void w_take_key(teptris_writer *w, const char *k, size_t klen)
112
+ {
113
+ free(w->key);
114
+ w->key = NULL;
115
+ w->key_len = 0;
116
+ if (klen > 0) {
117
+ w->key = malloc(klen);
118
+ if (w->key == NULL) {
119
+ w->st = TEPTRIS_ERR_ALLOC;
120
+ return;
121
+ }
122
+ memcpy(w->key, k, klen);
123
+ w->key_len = klen;
124
+ }
125
+ }
126
+
127
+ static void w_clear_key(teptris_writer *w)
128
+ {
129
+ free(w->key);
130
+ w->key = NULL;
131
+ w->key_len = 0;
132
+ }
133
+
134
+ /* push with an OWNED key (transfers the malloc from the caller) */
135
+ static wframe *w_push_steal(teptris_writer *w, int kind, char *k,
136
+ size_t klen)
137
+ {
138
+ wframe *f = calloc(1, sizeof(*f));
139
+ if (f == NULL) {
140
+ free(k);
141
+ w->st = TEPTRIS_ERR_ALLOC;
142
+ return NULL;
143
+ }
144
+ f->down = w->top;
145
+ f->kind = kind;
146
+ f->key = k;
147
+ f->key_len = klen;
148
+ w->top = f;
149
+ return f;
150
+ }
151
+
152
+ static wframe *w_push(teptris_writer *w, int kind, const char *k,
153
+ size_t klen)
154
+ {
155
+ wframe *f = calloc(1, sizeof(*f));
156
+ if (f == NULL) {
157
+ w->st = TEPTRIS_ERR_ALLOC;
158
+ return NULL;
159
+ }
160
+ f->down = w->top;
161
+ f->kind = kind;
162
+ if (klen > 0) {
163
+ f->key = malloc(klen);
164
+ if (f->key == NULL) {
165
+ free(f);
166
+ w->st = TEPTRIS_ERR_ALLOC;
167
+ return NULL;
168
+ }
169
+ memcpy(f->key, k, klen);
170
+ f->key_len = klen;
171
+ }
172
+ w->top = f;
173
+ return f;
174
+ }
175
+
176
+ /* dotted path of all keyed frames down the stack, outermost first;
177
+ * with_tail appends the pending key as the last component */
178
+ static void w_path(teptris_writer *w, int with_tail)
179
+ {
180
+ wframe *stack[600];
181
+ int n = 0;
182
+ for (wframe *f = w->top; f != NULL && n < 600; f = f->down) {
183
+ stack[n++] = f;
184
+ }
185
+ int first = 1;
186
+ for (int i = n - 1; i >= 0; i--) {
187
+ wframe *f = stack[i];
188
+ if ((f->kind == F_TABLE || f->kind == F_AOT) && f->key != NULL) {
189
+ if (!first) {
190
+ eb_c(&w->out, '.');
191
+ }
192
+ emit_key(&w->out, (teptris_view){f->key, f->key_len});
193
+ first = 0;
194
+ }
195
+ }
196
+ if (with_tail && w->key != NULL) {
197
+ if (!first) {
198
+ eb_c(&w->out, '.');
199
+ }
200
+ emit_key(&w->out, (teptris_view){w->key, w->key_len});
201
+ }
202
+ }
203
+
204
+ /* section headers follow the previous line directly (emitter style) */
205
+ static void w_sep(teptris_writer *w)
206
+ {
207
+ (void)w;
208
+ }
209
+
210
+ static void w_sep_val(teptris_writer *w, wframe *f)
211
+ {
212
+ if (f->first) {
213
+ eb_put(&w->out, ", ", 2);
214
+ }
215
+ f->first = 1;
216
+ }
217
+
218
+ /* emit "key = " in a section/inline-table context; arrays take bare
219
+ * values with separators */
220
+ static void w_keyed_prefix(teptris_writer *w)
221
+ {
222
+ wframe *f = w->top;
223
+ if (f != NULL && f->kind == F_ARRAY) {
224
+ w_sep_val(w, f);
225
+ return;
226
+ }
227
+ if (w->key == NULL) {
228
+ w->st = TEPTRIS_ERR_ARG;
229
+ return;
230
+ }
231
+ if (f != NULL && f->saw_subtable) {
232
+ w->st = TEPTRIS_ERR_STATE; /* value after a nested table header */
233
+ return;
234
+ }
235
+ emit_key(&w->out, (teptris_view){w->key, w->key_len});
236
+ eb_put(&w->out, " = ", 3);
237
+ w_clear_key(w);
238
+ }
239
+
240
+ static void w_end_line(teptris_writer *w)
241
+ {
242
+ wframe *f = w->top;
243
+ if (f == NULL || f->kind == F_TABLE) {
244
+ eb_c(&w->out, '\n');
245
+ }
246
+ /* inside inline arrays/tables the separator or close comes next */
247
+ }
248
+
249
+ teptris_status teptris_writer_feed(teptris_writer *w, const uint8_t *ops,
250
+ size_t len)
251
+ {
252
+ if (w == NULL || (ops == NULL && len > 0)) {
253
+ return TEPTRIS_ERR_ARG;
254
+ }
255
+ if (w->finished) {
256
+ return TEPTRIS_ERR_STATE;
257
+ }
258
+ size_t i = 0;
259
+ while (i < len && w->st == TEPTRIS_OK) {
260
+ uint8_t op = ops[i++];
261
+ switch (op) {
262
+ case W_END:
263
+ return w->st;
264
+ case W_KEY: {
265
+ if (i >= len) {
266
+ w->st = TEPTRIS_ERR_ARG;
267
+ break;
268
+ }
269
+ uint8_t klen = ops[i++];
270
+ if (i + klen > len) {
271
+ w->st = TEPTRIS_ERR_ARG;
272
+ break;
273
+ }
274
+ w_take_key(w, (const char *)ops + i, klen);
275
+ i += klen;
276
+ break;
277
+ }
278
+ case W_STRING: {
279
+ if (i + 4 > len) {
280
+ w->st = TEPTRIS_ERR_ARG;
281
+ break;
282
+ }
283
+ uint32_t vlen = (uint32_t)ops[i] |
284
+ ((uint32_t)ops[i + 1] << 8) |
285
+ ((uint32_t)ops[i + 2] << 16) |
286
+ ((uint32_t)ops[i + 3] << 24);
287
+ i += 4;
288
+ if (i + vlen > len) {
289
+ w->st = TEPTRIS_ERR_ARG;
290
+ break;
291
+ }
292
+ w_keyed_prefix(w);
293
+ if (w->st != TEPTRIS_OK) {
294
+ break;
295
+ }
296
+ emit_string_value(&w->out,
297
+ (teptris_view){(const char *)ops + i, vlen});
298
+ i += vlen;
299
+ w_end_line(w);
300
+ break;
301
+ }
302
+ case W_INT: {
303
+ if (i + 8 > len) {
304
+ w->st = TEPTRIS_ERR_ARG;
305
+ break;
306
+ }
307
+ uint64_t u = 0;
308
+ for (int b = 0; b < 8; b++) {
309
+ u |= (uint64_t)ops[i + b] << (8 * b); /* little-endian */
310
+ }
311
+ int64_t v;
312
+ memcpy(&v, &u, 8);
313
+ i += 8;
314
+ w_keyed_prefix(w);
315
+ if (w->st != TEPTRIS_OK) {
316
+ break;
317
+ }
318
+ emit_i64(&w->out, v);
319
+ w_end_line(w);
320
+ break;
321
+ }
322
+ case W_FLOAT: {
323
+ if (i + 8 > len) {
324
+ w->st = TEPTRIS_ERR_ARG;
325
+ break;
326
+ }
327
+ uint64_t u = 0;
328
+ for (int b = 0; b < 8; b++) {
329
+ u |= (uint64_t)ops[i + b] << (8 * b); /* little-endian */
330
+ }
331
+ double d;
332
+ memcpy(&d, &u, 8);
333
+ i += 8;
334
+ w_keyed_prefix(w);
335
+ if (w->st != TEPTRIS_OK) {
336
+ break;
337
+ }
338
+ emit_float(&w->out, d);
339
+ w_end_line(w);
340
+ break;
341
+ }
342
+ case W_FALSE:
343
+ case W_TRUE: {
344
+ w_keyed_prefix(w);
345
+ if (w->st != TEPTRIS_OK) {
346
+ break;
347
+ }
348
+ if (op == W_TRUE) {
349
+ eb_put(&w->out, "true", 4);
350
+ } else {
351
+ eb_put(&w->out, "false", 5);
352
+ }
353
+ w_end_line(w);
354
+ break;
355
+ }
356
+ case W_DATETIME: {
357
+ if (i + 26 > len) { /* kind + i64 year + 5 + u32 ns + i64 off */
358
+ w->st = TEPTRIS_ERR_ARG;
359
+ break;
360
+ }
361
+ uint8_t kb = ops[i];
362
+ if (kb < 4 || kb > 7) {
363
+ w->st = TEPTRIS_ERR_ARG;
364
+ break;
365
+ }
366
+ teptris_datetime dt;
367
+ uint64_t yr = 0, off = 0, ns = 0;
368
+ for (int b = 0; b < 8; b++) {
369
+ yr |= (uint64_t)ops[i + 1 + b] << (8 * b); /* LE */
370
+ off |= (uint64_t)ops[i + 18 + b] << (8 * b);
371
+ }
372
+ for (int b = 0; b < 4; b++) {
373
+ ns |= (uint64_t)ops[i + 14 + b] << (8 * b);
374
+ }
375
+ dt.year = (int32_t)yr;
376
+ dt.month = ops[i + 9];
377
+ dt.day = ops[i + 10];
378
+ dt.hour = ops[i + 11];
379
+ dt.minute = ops[i + 12];
380
+ dt.second = ops[i + 13];
381
+ memcpy(&dt.nanosecond, &ns, 4);
382
+ dt.offset_seconds = (int32_t)off;
383
+ i += 26;
384
+ w_keyed_prefix(w);
385
+ if (w->st != TEPTRIS_OK) {
386
+ break;
387
+ }
388
+ emit_dt(&w->out, &dt, (teptris_kind)kb);
389
+ w_end_line(w);
390
+ break;
391
+ if (w->st != TEPTRIS_OK) {
392
+ break;
393
+ }
394
+ emit_dt(&w->out, &dt, (teptris_kind)kb);
395
+ w_end_line(w);
396
+ break;
397
+ }
398
+ case W_BEGIN_TABLE: {
399
+ wframe *t = w->top;
400
+ if (t != NULL && (t->kind == F_ARRAY || t->kind == F_ITAB)) {
401
+ /* sections cannot live inside inline containers; use
402
+ * BEGIN_INLINE_TABLE there */
403
+ w->st = TEPTRIS_ERR_ARG;
404
+ break;
405
+ }
406
+ if (t != NULL && t->kind == F_AOT) {
407
+ /* element: [[path]] header, keyless frame */
408
+ w_sep(w);
409
+ eb_put(&w->out, "[[", 2);
410
+ w_path(w, 0);
411
+ eb_put(&w->out, "]]\n", 3);
412
+ w_clear_key(w);
413
+ w_push(w, F_TABLE, NULL, 0);
414
+ break;
415
+ }
416
+ if (w->key == NULL) {
417
+ w->st = TEPTRIS_ERR_ARG;
418
+ break;
419
+ }
420
+ if (t != NULL) {
421
+ t->saw_subtable = 1;
422
+ }
423
+ w_sep(w);
424
+ eb_c(&w->out, '[');
425
+ w_path(w, 1);
426
+ eb_put(&w->out, "]\n", 2);
427
+ char *k = w->key; /* frame takes ownership */
428
+ size_t klen = w->key_len;
429
+ w->key = NULL;
430
+ w->key_len = 0;
431
+ w_push_steal(w, F_TABLE, k, klen);
432
+ break;
433
+ }
434
+ case W_BEGIN_AOT: {
435
+ if (w->key == NULL) {
436
+ w->st = TEPTRIS_ERR_ARG;
437
+ break;
438
+ }
439
+ wframe *t = w->top;
440
+ if (t != NULL && t->kind != F_TABLE) {
441
+ w->st = TEPTRIS_ERR_ARG;
442
+ break;
443
+ }
444
+ if (t != NULL) {
445
+ t->saw_subtable = 1;
446
+ }
447
+ char *k = w->key; /* frame takes ownership */
448
+ size_t klen = w->key_len;
449
+ w->key = NULL;
450
+ w->key_len = 0;
451
+ w_push_steal(w, F_AOT, k, klen);
452
+ break;
453
+ }
454
+ case W_BEGIN_ARRAY: {
455
+ wframe *t = w->top;
456
+ if (t != NULL && t->kind == F_AOT) {
457
+ w->st = TEPTRIS_ERR_ARG;
458
+ break;
459
+ }
460
+ if (t == NULL || t->kind == F_TABLE || t->kind == F_ITAB) {
461
+ if (w->key == NULL) {
462
+ w->st = TEPTRIS_ERR_ARG;
463
+ break;
464
+ }
465
+ if (t != NULL && t->saw_subtable) {
466
+ w->st = TEPTRIS_ERR_STATE;
467
+ break;
468
+ }
469
+ emit_key(&w->out, (teptris_view){w->key, w->key_len});
470
+ eb_put(&w->out, " = [", 4);
471
+ w_clear_key(w);
472
+ } else { /* F_ARRAY: nested inline array */
473
+ w_sep_val(w, t);
474
+ eb_c(&w->out, '[');
475
+ }
476
+ w_push(w, F_ARRAY, NULL, 0);
477
+ break;
478
+ }
479
+ case W_BEGIN_INLINE_TABLE: {
480
+ wframe *t = w->top;
481
+ if (t != NULL && t->kind == F_ARRAY) {
482
+ if (w->key != NULL) {
483
+ w->st = TEPTRIS_ERR_ARG;
484
+ break;
485
+ }
486
+ w_sep_val(w, t);
487
+ } else {
488
+ if (w->key == NULL) {
489
+ w->st = TEPTRIS_ERR_ARG;
490
+ break;
491
+ }
492
+ if (t != NULL && t->saw_subtable) {
493
+ w->st = TEPTRIS_ERR_STATE;
494
+ break;
495
+ }
496
+ emit_key(&w->out, (teptris_view){w->key, w->key_len});
497
+ eb_put(&w->out, " = ", 3);
498
+ w_clear_key(w);
499
+ }
500
+ eb_c(&w->out, '{');
501
+ w_push(w, F_ITAB, NULL, 0);
502
+ break;
503
+ }
504
+ case W_CLOSE: {
505
+ wframe *f = w->top;
506
+ if (f == NULL || f->down == NULL) {
507
+ w->st = TEPTRIS_ERR_ARG; /* nothing (or only the root) open */
508
+ break;
509
+ }
510
+ int kind = f->kind;
511
+ w->top = f->down;
512
+ free(f->key);
513
+ free(f);
514
+ if (kind == F_ARRAY) {
515
+ eb_c(&w->out, ']');
516
+ w_end_line(w);
517
+ } else if (kind == F_ITAB) {
518
+ eb_c(&w->out, '}');
519
+ w_end_line(w);
520
+ }
521
+ break;
522
+ }
523
+ case W_EMPTY_TABLE:
524
+ case W_EMPTY_ARRAY: {
525
+ w_keyed_prefix(w);
526
+ if (w->st != TEPTRIS_OK) {
527
+ break;
528
+ }
529
+ if (op == W_EMPTY_TABLE) {
530
+ eb_put(&w->out, "{}", 2);
531
+ } else {
532
+ eb_put(&w->out, "[]", 2);
533
+ }
534
+ w_end_line(w);
535
+ break;
536
+ }
537
+ default:
538
+ w->st = TEPTRIS_ERR_ARG;
539
+ break;
540
+ }
541
+ }
542
+ return w->st;
543
+ }
544
+
545
+ teptris_status teptris_writer_finish(teptris_writer *w, char **buf,
546
+ size_t *len)
547
+ {
548
+ if (w == NULL || buf == NULL || len == NULL) {
549
+ return TEPTRIS_ERR_ARG;
550
+ }
551
+ *buf = NULL;
552
+ *len = 0;
553
+ if (w->top == NULL || w->top->down != NULL) {
554
+ return TEPTRIS_ERR_STATE; /* unclosed containers */
555
+ }
556
+ if (w->st != TEPTRIS_OK) {
557
+ return w->st;
558
+ }
559
+ w->finished = 1;
560
+ if (w->out.p == NULL) {
561
+ char *empty = malloc(1);
562
+ if (empty == NULL) {
563
+ return TEPTRIS_ERR_ALLOC;
564
+ }
565
+ empty[0] = '\0';
566
+ *buf = empty;
567
+ *len = 0;
568
+ return TEPTRIS_OK;
569
+ }
570
+ w->out.p[w->out.len] = '\0';
571
+ *buf = w->out.p;
572
+ *len = w->out.len;
573
+ w->out.p = NULL; /* ownership transferred */
574
+ return TEPTRIS_OK;
575
+ }
@@ -9,7 +9,7 @@
9
9
  #define FMT_FORBID_TIME 2
10
10
  #define FMT_FORBID_DATE 4
11
11
 
12
- static VALUE eParseError, eError, cDate;
12
+ static VALUE eParseError, eError, cDate, cDoc;
13
13
 
14
14
  /* civil date -> days since epoch (Hinnant, inverse of the dump side) */
15
15
  static int64_t days_from_civil(int32_t y, uint8_t m, uint8_t d) {
@@ -297,6 +297,66 @@ static VALUE ext_load(int argc, VALUE *argv, VALUE self) {
297
297
  return out;
298
298
  }
299
299
 
300
+ /* ---------------- native-doc API: the retained DOM ----------------
301
+ * load_doc parses into an engine document the wrapper OWNS, together
302
+ * with a private copy of the input - the doc's keys/values are
303
+ * zero-copy views into that copy, so both live and die together.
304
+ * dump_doc serializes with the C emitter: the roundtrip
305
+ * dump_doc(load_doc(s)) never materializes Ruby objects. */
306
+
307
+ static void dump_check(teptris_status st);
308
+
309
+ typedef struct {
310
+ teptris_document *doc;
311
+ char *buf;
312
+ } doc_owner;
313
+
314
+ static void doc_owner_free(void *p) {
315
+ doc_owner *o = (doc_owner *)p;
316
+ if (o->doc != NULL) teptris_document_free(o->doc);
317
+ if (o->buf != NULL) xfree(o->buf);
318
+ xfree(o);
319
+ }
320
+ static size_t doc_owner_size(const void *p) { return sizeof(doc_owner); }
321
+ static const rb_data_type_t doc_owner_type = {
322
+ "TeptrisExt/Doc",
323
+ {0, doc_owner_free, doc_owner_size, NULL, 0},
324
+ NULL, NULL, RUBY_TYPED_FREE_IMMEDIATELY};
325
+
326
+ static VALUE ext_load_doc(VALUE self, VALUE str) {
327
+ StringValue(str);
328
+ long len = RSTRING_LEN(str);
329
+ char *cbuf = ALLOC_N(char, (size_t)len + 1);
330
+ memcpy(cbuf, RSTRING_PTR(str), (size_t)len);
331
+ teptris_document *doc = NULL;
332
+ struct gvl_parse_arg arg = {cbuf, (size_t)len, &doc, TEPTRIS_OK};
333
+ rb_thread_call_without_gvl(parse_without_gvl, &arg, RUBY_UBF_IO, NULL);
334
+ if (arg.st != TEPTRIS_OK) {
335
+ const teptris_error *e = teptris_document_error(doc);
336
+ VALUE ex = rb_exc_new(eParseError, e->message, (long)strlen(e->message));
337
+ rb_iv_set(ex, "@line", SIZET2NUM(e->line));
338
+ rb_iv_set(ex, "@column", SIZET2NUM(e->column));
339
+ xfree(cbuf);
340
+ teptris_document_free(doc);
341
+ rb_exc_raise(ex);
342
+ }
343
+ doc_owner *o = ALLOC(doc_owner);
344
+ o->doc = doc;
345
+ o->buf = cbuf;
346
+ return TypedData_Wrap_Struct(cDoc, &doc_owner_type, o);
347
+ }
348
+
349
+ static VALUE ext_dump_doc(VALUE self, VALUE rbdoc) {
350
+ doc_owner *o;
351
+ TypedData_Get_Struct(rbdoc, doc_owner, &doc_owner_type, o);
352
+ char *buf = NULL;
353
+ size_t len = 0;
354
+ dump_check(teptris_document_emit(o->doc, &buf, &len));
355
+ VALUE out = rb_utf8_str_new(buf, (long)len);
356
+ free(buf);
357
+ return out;
358
+ }
359
+
300
360
  static VALUE ext_version(VALUE self) {
301
361
  return rb_str_new_cstr(teptris_version_string());
302
362
  }
@@ -1209,7 +1269,13 @@ void Init_teptris_ext(void) {
1209
1269
  * call state (key cache, arenas, types) is per-object or per-call */
1210
1270
  rb_ext_ractor_safe(true);
1211
1271
  VALUE m = rb_define_module("TeptrisExt");
1272
+ cDoc = rb_define_class_under(m, "Doc", rb_cObject);
1273
+ /* opaque handle: no Ruby-side allocation (silences the
1274
+ * allocator-undefine warning on reload) */
1275
+ rb_undef_alloc_func(cDoc);
1212
1276
  rb_define_module_function(m, "load", ext_load, -1);
1277
+ rb_define_module_function(m, "load_doc", ext_load_doc, 1);
1278
+ rb_define_module_function(m, "dump_doc", ext_dump_doc, 1);
1213
1279
  rb_define_module_function(m, "load_lazy", ext_load_lazy, 1);
1214
1280
  rb_define_module_function(m, "load_batch", ext_load_batch, -1);
1215
1281
  rb_define_module_function(m, "load_slice", ext_load_slice, -1);
data/lib/teptris/toml.rb CHANGED
@@ -18,6 +18,24 @@ module Teptris::TOML
18
18
  TeptrisExt.load(toml)
19
19
  end
20
20
 
21
+ # Parse into a RETAINED engine document (an opaque TeptrisExt::Doc
22
+ # handle; no Ruby objects are materialized). Pair with dump_doc for
23
+ # a roundtrip that runs entirely at C speed - reserialization of
24
+ # an unchanged document skips both Ruby-object walks. The doc owns
25
+ # a private copy of the input: its values are views into it, so
26
+ # mutating the source string afterwards is invisible (and the doc
27
+ # keeps the copy alive).
28
+ def load_doc(toml)
29
+ raise ArgumentError, "input must be a String" unless toml.is_a?(String)
30
+
31
+ TeptrisExt.load_doc(toml)
32
+ end
33
+
34
+ # Emit a load_doc() document at C speed (canonical TOML).
35
+ def dump_doc(doc)
36
+ TeptrisExt.dump_doc(doc)
37
+ end
38
+
21
39
  # Safe-load shaped entry point for frameworks: permitted_classes
22
40
  # restricts datetime materialization (raise when a value would
23
41
  # materialize an unpermitted class); datetime_policy :string keeps
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Teptris
4
- VERSION = "0.3.4.1"
4
+ VERSION = "0.3.6.1"
5
5
  end