yeptris 0.6.45.0 → 0.6.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4a4d244d4611d9e716495e55c0f0e30dc276064e3ee04e1b320479d37615cece
4
- data.tar.gz: b0b642653599944cf0e7825dd82626eb3651fb5c932231c75dba643fabbbe4b5
3
+ metadata.gz: 3d7fa9e72a34720f8c6114d615873c0e94f1e29102f66809c2d6f5a471ee9815
4
+ data.tar.gz: b75bd539aa0a8ce533057d9aec02084773286c4c14d3606e309ad72208eef612
5
5
  SHA512:
6
- metadata.gz: c21e4b93052e6afe809b0ecfd7adf99d51a72607242c55d63f8a4764844768ac5bb9c704f9e828ac74b0610c91d1324c247415620e11ba316b641d19be9b30c6
7
- data.tar.gz: e0ffd5e366a32371f4f960429c4a44ddaa2f583f8bcce77640f017d6e1c9fb224153926d0766d2c6adc944695f19945aa3a1cdaba44a445bb8d48efcadc89ea3
6
+ metadata.gz: cbd63ecc686b1df74a3819870441df3c036a4b38d59d6ce87d4a0bf0140e4e43c013dbff328c22707903ac1a0ba3530e0034f96c76b93677354017a1e23cbf8a
7
+ data.tar.gz: 10bfc39f4d111ee179faacc9e408cd495fb1c4cdeb2842d4428beef38b8d45a9ce94121a6e7650eaf956845695a5da98e08a89b95df3608f62b3d34565f7ea5d
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.6.45.0".freeze
6
+ VERSION = "0.6.46.0".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.45
6
+ VERSION 0.6.46
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -56,6 +56,18 @@ YEPTRIS_API YeptrisStatus yeptris_marshal_node_ex(YeptrisNode node, uint32_t fla
56
56
 
57
57
  YEPTRIS_API void yeptris_marshal_free(char* out);
58
58
 
59
+ /* yeptris-ruby#322: the dump face's marshal reader — Ruby
60
+ * Marshal.dump bytes IN, JSON text OUT, one C call (no per-scalar
61
+ * crossings, no object reconstruction). Handles the JSON-relevant
62
+ * subset (nil/bool/int/string/float/array/hash + symbols/links/ivars);
63
+ * anything else — objects, structs, user marshal, bignums beyond i64,
64
+ * non-finite floats, cycles — returns YEPTRIS_ERROR_UNSUPPORTED and
65
+ * the binding falls back to its per-node walk. *out is malloc'd, free
66
+ * with yeptris_marshal_free exactly once; on failure nothing is
67
+ * allocated. compact selects the no-space separators. */
68
+ YEPTRIS_API YeptrisStatus yeptris_json_from_marshal(const char* m, size_t m_len, int compact,
69
+ char** out, size_t* out_len);
70
+
59
71
  #ifdef __cplusplus
60
72
  }
61
73
  #endif
@@ -9,6 +9,8 @@
9
9
 
10
10
  #include "dom.h"
11
11
 
12
+ #include "../scan/scan.h" /* the dq-forced SSOT (the fact bit) */
13
+
12
14
  #include <yeptris/resolve.h> /* the tag-id SSOT (the strict literal arm) */
13
15
 
14
16
  /* The bounded-parse caps: each node consumes at least one input byte
@@ -140,6 +142,16 @@ static yep_sview dom_str_in(yep_dom* d, const yep_view* v, int borrowed) {
140
142
  * given metadata; tag/anchor views encode through dom_str_in (tags and
141
143
  * event-path anchors copy to the arena; input-backed views borrow).
142
144
  * tag_id stays 0 — the caller resolves (scalars) or leaves 0. */
145
+ /* Value assign + the emit-side fact bit (issue #352 lane 2): the bit
146
+ * is "verified clean" of the dq-forced byte set — the serializer's
147
+ * sc_route then skips its per-derive rescan. The scan is the SSOT
148
+ * predicate (yep_scan_dq_forced); every value assignment routes
149
+ * through here so no path can leave a dirty scalar marked clean. */
150
+ static void dom_set_value(yep_dom* d, uint32_t id, const yep_view* v, int borrowed) {
151
+ d->nodes[id].value = dom_str_in(d, v, borrowed);
152
+ d->nodes[id].no_exotic = !yep_scan_dq_forced(v->p, v->len);
153
+ }
154
+
143
155
  uint32_t dom_open_node(yep_dom* d, uint8_t kind, const yep_view* tag, const yep_view* anchor,
144
156
  int anchor_borrowed, uint8_t style, uint8_t implicit, uint8_t flow) {
145
157
  if (!dom_grow_nodes(d, 1)) {
@@ -421,7 +433,7 @@ int yep_dom_on_event(void* ctx, const yep_event* ev) {
421
433
  if (id == UINT32_MAX) {
422
434
  return -1;
423
435
  }
424
- d->nodes[id].value = dom_str_in(d, &ev->value, ev->borrowed);
436
+ dom_set_value(d, id, &ev->value, ev->borrowed);
425
437
  if (ev->anchor_id != 0 && dom_anchor_set(d, ev->anchor_id, id) != 0) {
426
438
  return -1;
427
439
  }
@@ -437,7 +449,7 @@ int yep_dom_on_event(void* ctx, const yep_event* ev) {
437
449
  if (id == UINT32_MAX) {
438
450
  return -1;
439
451
  }
440
- d->nodes[id].value = dom_str_in(d, &ev->value, ev->borrowed);
452
+ dom_set_value(d, id, &ev->value, ev->borrowed);
441
453
  d->nodes[id].target = target;
442
454
  return dom_place(d, id);
443
455
  }
@@ -902,9 +914,11 @@ int dom_on_flow_build(void* ctx, const char* p, size_t open, size_t len, uint32_
902
914
  d->nodes[sid].value =
903
915
  yep_dom_str_commit(d, yep_finish_double_into(p, (uint32_t)(t.at + 1),
904
916
  (uint32_t)(t.end - 1), dst, span));
917
+ d->nodes[sid].no_exotic = 1; /* JSON-class strings: the
918
+ walker rejected every control byte */
905
919
  } else {
906
920
  yep_view v = {p + t.at + 1, (uint32_t)(t.end - t.at - 2)};
907
- d->nodes[sid].value = dom_str_in(d, &v, 1);
921
+ dom_set_value(d, sid, &v, 1);
908
922
  }
909
923
  if (dom_place(d, sid) != 0) {
910
924
  goto fail;
@@ -918,6 +932,8 @@ int dom_on_flow_build(void* ctx, const char* p, size_t open, size_t len, uint32_
918
932
  }
919
933
  yep_view v = {p + t.at, (uint32_t)(t.end - t.at)};
920
934
  d->nodes[sid].value = dom_str_in(d, &v, 1);
935
+ d->nodes[sid].no_exotic = 1; /* the walker validated the
936
+ span as a JSON number or literal */
921
937
  if (t.cls == '#') {
922
938
  d->nodes[sid].tag_id = yep_resolve_number(r, t.is_float);
923
939
  } else if (d->flow_strict) {
@@ -1087,13 +1103,15 @@ int dom_from_tape(yep_dom* d, const yeptris_json_tape* t) {
1087
1103
  }
1088
1104
  d->nodes[id].value = yep_dom_str_commit(
1089
1105
  d, yep_finish_double_into(src, off, off + len, dst, len));
1106
+ d->nodes[id].no_exotic = 1; /* JSON-class: escapes
1107
+ only, the walker's law */
1090
1108
  } else {
1091
1109
  yep_view v = {src + off, len};
1092
- d->nodes[id].value = dom_str_in(d, &v, 1);
1110
+ dom_set_value(d, id, &v, 1);
1093
1111
  }
1094
1112
  } else {
1095
1113
  yep_view v = {src + off, len};
1096
- d->nodes[id].value = dom_str_in(d, &v, 1);
1114
+ dom_set_value(d, id, &v, 1);
1097
1115
  }
1098
1116
  d->nodes[id].tag_id = tag;
1099
1117
  if (dom_place(d, id) != 0) {
@@ -1141,7 +1159,7 @@ int dom_on_block_open(void* ctx, const yep_view* key, uint32_t line, uint16_t ke
1141
1159
  d->depth--;
1142
1160
  return -1;
1143
1161
  }
1144
- d->nodes[kid].value = dom_str_in(d, key, 1);
1162
+ dom_set_value(d, kid, key, 1);
1145
1163
  d->nodes[kid].tag_id = r->resolve(NULL, key->p, key->len);
1146
1164
  return dom_place(d, kid) == 0 ? 1 : -1;
1147
1165
  }
@@ -1156,7 +1174,7 @@ int dom_on_block_item(void* ctx, const yep_block_value* v) {
1156
1174
  if (vid == UINT32_MAX) {
1157
1175
  return -1;
1158
1176
  }
1159
- d->nodes[vid].value = dom_str_in(d, &v->value, v->borrowed);
1177
+ dom_set_value(d, vid, &v->value, v->borrowed);
1160
1178
  d->nodes[vid].tag_id = r->resolve(NULL, v->value.p, v->value.len);
1161
1179
  return dom_place(d, vid) == 0 ? 1 : -1;
1162
1180
  }
@@ -1183,7 +1201,7 @@ int dom_on_block_pair(void* ctx, const yep_view* key, const yep_block_value* v,
1183
1201
  if (kid == UINT32_MAX) {
1184
1202
  return -1;
1185
1203
  }
1186
- d->nodes[kid].value = dom_str_in(d, key, 1);
1204
+ dom_set_value(d, kid, key, 1);
1187
1205
  d->nodes[kid].tag_id = r->resolve(NULL, key->p, key->len);
1188
1206
  if (dom_place(d, kid) != 0) {
1189
1207
  return -1;
@@ -1193,7 +1211,7 @@ int dom_on_block_pair(void* ctx, const yep_view* key, const yep_block_value* v,
1193
1211
  if (vid == UINT32_MAX) {
1194
1212
  return -1;
1195
1213
  }
1196
- d->nodes[vid].value = dom_str_in(d, &v->value, 1); /* borrowed — the
1214
+ dom_set_value(d, vid, &v->value, 1); /* borrowed — the
1197
1215
  event path stamps it too now (73); the gates compare decoded
1198
1216
  views, not arena placement */
1199
1217
  d->nodes[vid].target = target;
@@ -1205,7 +1223,7 @@ int dom_on_block_pair(void* ctx, const yep_view* key, const yep_block_value* v,
1205
1223
  if (vid == UINT32_MAX) {
1206
1224
  return -1;
1207
1225
  }
1208
- d->nodes[vid].value = dom_str_in(d, &v->value, v->borrowed);
1226
+ dom_set_value(d, vid, &v->value, v->borrowed);
1209
1227
  d->nodes[vid].tag_id = r->resolve(NULL, v->value.p, v->value.len);
1210
1228
  if (anchored && v->anchor_id != 0 && dom_anchor_set(d, v->anchor_id, vid) != 0) {
1211
1229
  return -1;
@@ -1224,7 +1242,7 @@ int dom_on_scalar(void* ctx, const yep_view* value, const yep_view* tag, const y
1224
1242
  if (id == UINT32_MAX) {
1225
1243
  return -1;
1226
1244
  }
1227
- d->nodes[id].value = dom_str_in(d, value, borrowed);
1245
+ dom_set_value(d, id, value, borrowed);
1228
1246
  d->nodes[id].tag_id = tag_id;
1229
1247
  if (anchor_id != 0 && dom_anchor_set(d, anchor_id, id) != 0) {
1230
1248
  return -1;
@@ -60,12 +60,16 @@ typedef struct yep_sview {
60
60
  * fields left the record — they live in the DOM's lazily-built side
61
61
  * tables (parse pays nothing; dom.c owns them). */
62
62
  typedef struct yep_dnode {
63
- uint8_t kind : 2; /* yep_dom_kind (0-3) */
64
- uint8_t style : 3; /* yep_scalar_style (0-5) */
65
- uint8_t flow : 1; /* collection opened in flow style */
66
- uint8_t implicit : 1; /* scalar written without quotes/tag */
67
- uint8_t tag_id; /* resolved tag (resolve/resolver.h) */
68
- uint32_t first_child; /* child link (mappings: key,value,key,value…) */
63
+ uint8_t kind : 2; /* yep_dom_kind (0-3) */
64
+ uint8_t style : 3; /* yep_scalar_style (0-5) */
65
+ uint8_t flow : 1; /* collection opened in flow style */
66
+ uint8_t implicit : 1; /* scalar written without quotes/tag */
67
+ uint8_t no_exotic : 1; /* value verified clean of the dq-forced
68
+ * byte set (#352 lane 2): the serializer's
69
+ * sc_route skips its rescan. Set at every
70
+ * value assignment; 0 = unknown = scan. */
71
+ uint8_t tag_id; /* resolved tag (resolve/resolver.h) */
72
+ uint32_t first_child; /* child link (mappings: key,value,key,value…) */
69
73
  uint32_t last_child;
70
74
  uint32_t next_sibling;
71
75
  uint32_t count; /* children (mappings: 2 × pairs) */
@@ -486,31 +486,12 @@ enum { YEP_SC_NOTHING = 0, YEP_SC_PLAIN, YEP_SC_SQ, YEP_SC_DQ, YEP_SC_LITERAL, Y
486
486
  * The block map's explicit-key promotion consults the same predicate:
487
487
  * a key whose content demotes to DQ re-parses as a QUOTED key and must
488
488
  * ride simple, or round two's render differs (the nightly's round 118). */
489
- static int sc_dq_forced(const char* p, uint32_t len) {
490
- for (uint32_t k = 0; k + 1 < len; k++) {
491
- if ((unsigned char)p[k] == 0xc2 && (unsigned char)p[k + 1] == 0x85) {
492
- return 1;
493
- }
494
- /* LS/PS are breaks like NEL: only their escapes are stable */
495
- if ((unsigned char)p[k] == 0xe2 && k + 2 < len && (unsigned char)p[k + 1] == 0x80 &&
496
- ((unsigned char)p[k + 2] == 0xa8 || (unsigned char)p[k + 2] == 0xa9)) {
497
- return 1;
498
- }
499
- if ((unsigned char)p[k] == 0xef && k + 2 < len && (unsigned char)p[k + 1] == 0xbb &&
500
- (unsigned char)p[k + 2] == 0xbf) {
501
- return 1;
502
- }
503
- if ((unsigned char)p[k] == 0xef && k + 2 < len && (unsigned char)p[k + 1] == 0xbf &&
504
- ((unsigned char)p[k + 2] == 0xbe || (unsigned char)p[k + 2] == 0xbf)) {
505
- return 1; /* noncharacter: only its escape re-parses */
506
- }
507
- }
508
- return 0;
509
- }
510
-
511
489
  static uint8_t sc_route(yep_writer* w, const char* p, uint32_t len, int as_key, uint8_t sty_in,
512
- int has_tag) {
513
- if (sc_dq_forced(p, len)) {
490
+ int has_tag, int no_exotic) {
491
+ /* the parse-time fact bit (#352 lane 2): a dnode verified clean of
492
+ * the dq-forced set skips the rescan — 0 (mutation-built or JSON
493
+ * classes) keeps the scan */
494
+ if (!no_exotic && yep_scan_dq_forced(p, len)) {
514
495
  return YEP_SC_DQ;
515
496
  }
516
497
  if (w->in_flow && len > 0) {
@@ -641,14 +622,14 @@ static void emit_scalar(yep_writer* w, const yep_dnode* n, int parent_col, int a
641
622
  * memoized long scalars, in visit order either way. */
642
623
  if (w->sc_dec != NULL && len >= YEP_SC_MEMO_MIN) {
643
624
  if (w->dry) {
644
- route = sc_route(w, p, len, as_key, n->style, n->tag.len > 0);
625
+ route = sc_route(w, p, len, as_key, n->style, n->tag.len > 0, n->no_exotic);
645
626
  w->sc_dec[w->sc_i] = route;
646
627
  } else {
647
628
  route = w->sc_dec[w->sc_i];
648
629
  }
649
630
  w->sc_i++;
650
631
  } else {
651
- route = sc_route(w, p, len, as_key, n->style, n->tag.len > 0);
632
+ route = sc_route(w, p, len, as_key, n->style, n->tag.len > 0, n->no_exotic);
652
633
  }
653
634
  switch (route) {
654
635
  case YEP_SC_NOTHING:
@@ -1190,7 +1171,7 @@ static void emit_block_map(yep_emitter* em, uint32_t id, int content_col) {
1190
1171
  * anyway (the nightly's run 38177622733, ":\n! |-\n r\n:") */
1191
1172
  if (!complex_key && kn->style >= 4 && kn->style <= 5 && kn->value.len > 0 &&
1192
1173
  kn->tag.len > 0 && memchr(wv(w, kn->value).p, '\n', wv(w, kn->value).len) == NULL &&
1193
- !sc_dq_forced(wv(w, kn->value).p, wv(w, kn->value).len)) {
1174
+ (kn->no_exotic || !yep_scan_dq_forced(wv(w, kn->value).p, wv(w, kn->value).len))) {
1194
1175
  /* a TAGGED SINGLE-LINE literal/folded key keeps the block
1195
1176
  * form in sc_route (the #290 law), so the value separator
1196
1177
  * would glue onto the block's CONTENT line (colons are
@@ -1403,6 +1384,560 @@ static void emit_node(yep_emitter* em, uint32_t id, int parent_col, int as_key)
1403
1384
  emit_scalar(w, n, parent_col, as_key);
1404
1385
  }
1405
1386
 
1387
+ /* ============================================================
1388
+ * yeptris-ruby#322: the marshal READER — Ruby Marshal.dump bytes
1389
+ * to JSON text in one C call (no per-scalar crossings, no object
1390
+ * reconstruction). The format facts mirror marshal.c's emission
1391
+ * contract (empirically pinned against Ruby 3.4): w_long small form
1392
+ * x==0 -> 0x00, |x| <= 122 -> sign*(|x|+5), size form tag +-k with
1393
+ * LE payload (2^8k - m for negatives); strings carry 'I' ivar
1394
+ * wrappers; arrays/hashes/strings/floats register for '@' links.
1395
+ * A JSON dump duplicates linked re-references (cycled or shared
1396
+ * subtrees re-walk from the registered position, depth-guarded).
1397
+ * The JSON-irrelevant constructs ('o' objects, 'S' structs, 'u'
1398
+ * user marshal, 'l' bignums beyond i64, non-finite floats) return
1399
+ * UNSUPPORTED — the binding falls back to its per-node walk.
1400
+ * ============================================================ */
1401
+
1402
+ #include <inttypes.h>
1403
+
1404
+ typedef struct {
1405
+ const char* p;
1406
+ size_t len;
1407
+ size_t at;
1408
+ } mj_stream;
1409
+
1410
+ typedef struct {
1411
+ uint32_t kind; /* 1 seq, 2 map */
1412
+ uint32_t items;
1413
+ } mj_frame;
1414
+
1415
+ typedef struct {
1416
+ yep_writer* w;
1417
+ mj_stream s;
1418
+ mj_frame* frames;
1419
+ int depth;
1420
+ int compact;
1421
+ /* the symbol table: ';' links resolve to these names */
1422
+ struct {
1423
+ const char* p;
1424
+ uint32_t len;
1425
+ }* syms;
1426
+ size_t syms_n, syms_cap;
1427
+ /* the object table: '@' links re-walk from these positions */
1428
+ struct {
1429
+ size_t pos;
1430
+ uint32_t chain_pos;
1431
+ }* objs;
1432
+ size_t objs_n, objs_cap;
1433
+ size_t* chain; /* the active positions (cycle guard) */
1434
+ size_t chain_n, chain_cap;
1435
+ int failed; /* 0 ok, YEPTRIS_ERROR_UNSUPPORTED */
1436
+ } mj_ctx;
1437
+
1438
+ static int mj_fail(mj_ctx* c) {
1439
+ c->failed = YEPTRIS_ERROR_UNSUPPORTED;
1440
+ return -1;
1441
+ }
1442
+
1443
+ static int mj_byte(mj_ctx* c, unsigned char* out) {
1444
+ if (c->s.at >= c->s.len)
1445
+ return mj_fail(c);
1446
+ *out = (unsigned char)c->s.p[c->s.at++];
1447
+ return 0;
1448
+ }
1449
+
1450
+ /* the w_long inverse — mirrors m_w_long's encode exactly:
1451
+ * 0x00 -> 0; 1..4 -> positive size form k=b; 5..127 -> small +(
1452
+ * b-5); 129..250 -> small -(251-b); 251..255 -> negative size form
1453
+ * k=256-b (payload = LE(2^(8k) - m)); 128/248..250 unreachable (the
1454
+ * writer takes the small form first). */
1455
+ static int mj_long(mj_ctx* c, int64_t* out) {
1456
+ unsigned char b;
1457
+ if (mj_byte(c, &b) != 0)
1458
+ return -1;
1459
+ if (b == 0) {
1460
+ *out = 0;
1461
+ return 0;
1462
+ }
1463
+ if (b <= 4) { /* positive size form, k = b */
1464
+ int64_t v = 0;
1465
+ for (int i = 0; i < (int)b; i++) {
1466
+ unsigned char d;
1467
+ if (mj_byte(c, &d) != 0)
1468
+ return -1;
1469
+ v |= (int64_t)d << (8 * i);
1470
+ }
1471
+ *out = v;
1472
+ return 0;
1473
+ }
1474
+ if (b <= 127) {
1475
+ *out = (int64_t)b - 5;
1476
+ return 0;
1477
+ }
1478
+ if (b >= 129 && b <= 250) {
1479
+ *out = (int64_t)b - 251;
1480
+ return 0;
1481
+ }
1482
+ { /* 251..255: negative size form */
1483
+ int bytes = 256 - (int)b;
1484
+ int64_t v = 0;
1485
+ for (int i = 0; i < bytes; i++) {
1486
+ unsigned char d;
1487
+ if (mj_byte(c, &d) != 0)
1488
+ return -1;
1489
+ v |= (int64_t)d << (8 * i);
1490
+ }
1491
+ *out = v - ((int64_t)1 << (8 * bytes));
1492
+ return 0;
1493
+ }
1494
+ }
1495
+
1496
+ static const char* mj_sym_name(mj_ctx* c, int64_t idx, uint32_t* len_out) {
1497
+ if (idx < 0 || (size_t)idx >= c->syms_n) {
1498
+ mj_fail(c);
1499
+ return NULL;
1500
+ }
1501
+ *len_out = c->syms[idx].len;
1502
+ return c->syms[idx].p;
1503
+ }
1504
+
1505
+ static int mj_ivar_skip(mj_ctx* c);
1506
+ static int mj_link_key(mj_ctx* c, int64_t idx);
1507
+
1508
+ static int mj_value(mj_ctx* c);
1509
+
1510
+ static int mj_string_body(mj_ctx* c, int64_t len) {
1511
+ if (len < 0 || (uint64_t)len > (uint64_t)(c->s.len - c->s.at))
1512
+ return mj_fail(c);
1513
+ emit_dq_json(c->w, c->s.p + c->s.at, (uint32_t)len);
1514
+ c->s.at += (size_t)len;
1515
+ return 0;
1516
+ }
1517
+
1518
+ /* a symbol: ':' + long(len) + bytes — registers in the table; the
1519
+ * name is emitted as a JSON string when emit is set */
1520
+ static int mj_symbol(mj_ctx* c, int emit) {
1521
+ int64_t len;
1522
+ if (mj_long(c, &len) != 0)
1523
+ return -1;
1524
+ if (len < 0 || (uint64_t)len > (uint64_t)(c->s.len - c->s.at))
1525
+ return mj_fail(c);
1526
+ const char* name = c->s.p + c->s.at;
1527
+ uint32_t nlen = (uint32_t)len;
1528
+ c->s.at += (size_t)len;
1529
+ if (c->syms_n == c->syms_cap) {
1530
+ c->syms_cap = c->syms_cap ? c->syms_cap * 2 : 16;
1531
+ c->syms = (void*)realloc(c->syms, c->syms_cap * sizeof(*c->syms));
1532
+ if (!c->syms)
1533
+ return mj_fail(c);
1534
+ }
1535
+ c->syms[c->syms_n].p = name;
1536
+ c->syms[c->syms_n].len = nlen;
1537
+ c->syms_n++;
1538
+ if (emit) {
1539
+ emit_dq_json(c->w, name, nlen);
1540
+ }
1541
+ return 0;
1542
+ }
1543
+
1544
+ /* an ivar wrapper 'I': the value then long(count) then count pairs of
1545
+ * (symbol, value) — read-and-discard (encodings and such) */
1546
+ static int mj_ivar_skip(mj_ctx* c);
1547
+
1548
+ static int mj_discard_value(mj_ctx* c) {
1549
+ unsigned char t;
1550
+ if (mj_byte(c, &t) != 0)
1551
+ return -1;
1552
+ switch (t) {
1553
+ case '0':
1554
+ case 'T':
1555
+ case 'F':
1556
+ return 0;
1557
+ case 'i': {
1558
+ int64_t v;
1559
+ return mj_long(c, &v);
1560
+ }
1561
+ case '"': {
1562
+ int64_t len;
1563
+ if (mj_long(c, &len) != 0)
1564
+ return -1;
1565
+ if (len < 0 || (uint64_t)len > (uint64_t)(c->s.len - c->s.at))
1566
+ return mj_fail(c);
1567
+ c->s.at += (size_t)len;
1568
+ return 0;
1569
+ }
1570
+ case ':':
1571
+ return mj_symbol(c, 0);
1572
+ case ';': {
1573
+ int64_t idx;
1574
+ if (mj_long(c, &idx) != 0)
1575
+ return -1;
1576
+ return mj_sym_name(c, idx, &(uint32_t){0}) ? 0 : -1;
1577
+ }
1578
+ case 'I':
1579
+ return mj_ivar_skip(c);
1580
+ default:
1581
+ return mj_fail(c); /* ivars on JSON trees carry symbol/E
1582
+ pairs only in practice */
1583
+ }
1584
+ }
1585
+
1586
+ /* the count + (symbol, value) pairs that follow an 'I' wrapper's
1587
+ * wrapped value */
1588
+ static int mj_ivar_pairs(mj_ctx* c) {
1589
+ int64_t count;
1590
+ if (mj_long(c, &count) != 0)
1591
+ return -1;
1592
+ for (int64_t i = 0; i < count; i++) {
1593
+ if (mj_discard_value(c) != 0)
1594
+ return -1; /* the name symbol */
1595
+ if (mj_discard_value(c) != 0)
1596
+ return -1; /* the ivar value */
1597
+ }
1598
+ return 0;
1599
+ }
1600
+
1601
+ /* a standalone 'I' wrapper: the wrapped value, then the pairs */
1602
+ static int mj_ivar_skip(mj_ctx* c) {
1603
+ if (mj_discard_value(c) != 0)
1604
+ return -1; /* the wrapped value */
1605
+ return mj_ivar_pairs(c);
1606
+ }
1607
+
1608
+ static int mj_value(mj_ctx* c) {
1609
+ unsigned char t;
1610
+ size_t tpos = c->s.at;
1611
+ if (mj_byte(c, &t) != 0)
1612
+ return -1;
1613
+ /* arrays, hashes, strings and floats register for '@' links (the
1614
+ * marshal.c emission contract, 0-based stream order); integers,
1615
+ * bignums, symbols and immediates never register */
1616
+ if (t == '[' || t == '{' || t == '"' || t == 'f') {
1617
+ if (c->objs_n == c->objs_cap) {
1618
+ c->objs_cap = c->objs_cap ? c->objs_cap * 2 : 16;
1619
+ c->objs = (void*)realloc(c->objs, c->objs_cap * sizeof(*c->objs));
1620
+ if (!c->objs)
1621
+ return mj_fail(c);
1622
+ }
1623
+ c->objs[c->objs_n].pos = tpos;
1624
+ c->objs[c->objs_n].chain_pos = 0;
1625
+ c->objs_n++;
1626
+ }
1627
+ switch (t) {
1628
+ case '0':
1629
+ wr_put(c->w, "null", 4);
1630
+ return 0;
1631
+ case 'T':
1632
+ wr_put(c->w, "true", 4);
1633
+ return 0;
1634
+ case 'F':
1635
+ wr_put(c->w, "false", 5);
1636
+ return 0;
1637
+ case 'i': {
1638
+ int64_t v;
1639
+ if (mj_long(c, &v) != 0)
1640
+ return -1;
1641
+ char buf[24];
1642
+ int n = snprintf(buf, sizeof buf, "%lld", (long long)v);
1643
+ wr_put(c->w, buf, (uint32_t)n);
1644
+ return 0;
1645
+ }
1646
+ case '"': {
1647
+ int64_t len;
1648
+ if (mj_long(c, &len) != 0)
1649
+ return -1;
1650
+ int rc = mj_string_body(c, len);
1651
+ if (rc != 0)
1652
+ return -1;
1653
+ /* the 'I' wrapper follows when the string carries ivars: the
1654
+ * value is already parsed — pairs only */
1655
+ if (c->s.at < c->s.len && c->s.p[c->s.at] == 'I') {
1656
+ c->s.at++;
1657
+ if (mj_ivar_pairs(c) != 0)
1658
+ return -1;
1659
+ }
1660
+ return 0;
1661
+ }
1662
+ case 'I': {
1663
+ /* the wrapped value IS the payload: re-dispatch into it, then
1664
+ * the ivar pairs ride */
1665
+ if (mj_value(c) != 0)
1666
+ return -1;
1667
+ return mj_ivar_pairs(c);
1668
+ }
1669
+ case 'l': {
1670
+ /* the bignum: sign byte + w_long(limb count) + LE 16-bit
1671
+ * limbs; the i64-representable range emits, wider falls back */
1672
+ unsigned char sign;
1673
+ if (mj_byte(c, &sign) != 0)
1674
+ return -1;
1675
+ if (sign != 0x2b && sign != 0x2d)
1676
+ return mj_fail(c); /* + / - */
1677
+ int64_t words;
1678
+ if (mj_long(c, &words) != 0)
1679
+ return -1;
1680
+ if (words < 0 || words > 4)
1681
+ return mj_fail(c);
1682
+ int64_t v = 0;
1683
+ for (int64_t i = 0; i < words; i++) {
1684
+ for (int b = 0; b < 2; b++) {
1685
+ unsigned char d;
1686
+ if (mj_byte(c, &d) != 0)
1687
+ return -1;
1688
+ v |= (int64_t)d << (16 * i + 8 * b);
1689
+ }
1690
+ }
1691
+ if (sign == 0x2d) {
1692
+ v = -v;
1693
+ }
1694
+ char buf[24];
1695
+ int n = snprintf(buf, sizeof buf, "%lld", (long long)v);
1696
+ wr_put(c->w, buf, (uint32_t)n);
1697
+ return 0;
1698
+ }
1699
+ case 'f': {
1700
+ int64_t len;
1701
+ if (mj_long(c, &len) != 0)
1702
+ return -1;
1703
+ if (len < 0 || (uint64_t)len > (uint64_t)(c->s.len - c->s.at))
1704
+ return mj_fail(c);
1705
+ char tmp[64];
1706
+ uint32_t tl = (uint32_t)len < sizeof tmp - 1 ? (uint32_t)len : (uint32_t)sizeof tmp - 1;
1707
+ memcpy(tmp, c->s.p + c->s.at, tl);
1708
+ tmp[tl] = '\0';
1709
+ c->s.at += (size_t)len;
1710
+ if (strcmp(tmp, "inf") == 0 || strcmp(tmp, "-inf") == 0 || strcmp(tmp, "nan") == 0) {
1711
+ return mj_fail(c); /* JSON has no inf/nan spelling */
1712
+ }
1713
+ double d = strtod(tmp, NULL);
1714
+ char buf[40];
1715
+ int wl = yep_d2s_json(d, buf);
1716
+ wr_put(c->w, buf, (uint32_t)wl);
1717
+ return 0;
1718
+ }
1719
+ case '[':
1720
+ case '{': {
1721
+ int64_t count;
1722
+ if (mj_long(c, &count) != 0)
1723
+ return -1;
1724
+ if (count < 0)
1725
+ return mj_fail(c);
1726
+ if (c->depth >= 512)
1727
+ return mj_fail(c);
1728
+ c->depth++;
1729
+ wr_byte(c->w, t == '[' ? '[' : '{');
1730
+ for (int64_t i = 0; i < count; i++) {
1731
+ if (i > 0) {
1732
+ wr_put(c->w, c->compact ? "," : ", ", c->compact ? 1 : 2);
1733
+ }
1734
+ if (t == '{') {
1735
+ /* the key: a string or a symbol — JSON keys are strings */
1736
+ unsigned char kt;
1737
+ if (mj_byte(c, &kt) != 0)
1738
+ return -1;
1739
+ if (kt == 'I') {
1740
+ /* an ivar-wrapped key: the inner value emits, then
1741
+ * the pairs (the 'I' is consumed) */
1742
+ if (mj_value(c) != 0)
1743
+ return -1;
1744
+ if (mj_ivar_pairs(c) != 0)
1745
+ return -1;
1746
+ } else if (kt == '@') {
1747
+ /* a linked key: re-emit the target (#322's corpus
1748
+ * links the users' repeated key strings) */
1749
+ int64_t idx;
1750
+ if (mj_long(c, &idx) != 0)
1751
+ return -1;
1752
+ if (mj_link_key(c, idx) != 0)
1753
+ return -1;
1754
+ } else if (kt == '"') {
1755
+ int64_t len;
1756
+ if (mj_long(c, &len) != 0)
1757
+ return -1;
1758
+ int rc = mj_string_body(c, len);
1759
+ if (rc != 0)
1760
+ return -1;
1761
+ if (c->s.at < c->s.len && c->s.p[c->s.at] == 'I') {
1762
+ c->s.at++;
1763
+ if (mj_ivar_pairs(c) != 0)
1764
+ return -1;
1765
+ }
1766
+ } else if (kt == ':') {
1767
+ if (mj_symbol(c, 1) != 0)
1768
+ return -1;
1769
+ } else if (kt == ';') {
1770
+ int64_t idx;
1771
+ if (mj_long(c, &idx) != 0)
1772
+ return -1;
1773
+ uint32_t nlen;
1774
+ const char* nm = mj_sym_name(c, idx, &nlen);
1775
+ if (!nm)
1776
+ return -1;
1777
+ emit_dq_json(c->w, nm, nlen);
1778
+ } else {
1779
+ return mj_fail(c);
1780
+ }
1781
+ wr_put(c->w, c->compact ? ":" : ": ", c->compact ? 1 : 2);
1782
+ if (mj_value(c) != 0)
1783
+ return -1;
1784
+ } else {
1785
+ if (mj_value(c) != 0)
1786
+ return -1;
1787
+ }
1788
+ }
1789
+ c->depth--;
1790
+ wr_byte(c->w, t == '[' ? ']' : '}');
1791
+ return 0;
1792
+ }
1793
+ case ';': {
1794
+ int64_t idx;
1795
+ if (mj_long(c, &idx) != 0)
1796
+ return -1;
1797
+ uint32_t nlen;
1798
+ const char* nm = mj_sym_name(c, idx, &nlen);
1799
+ if (!nm)
1800
+ return -1;
1801
+ emit_dq_json(c->w, nm, nlen);
1802
+ return 0;
1803
+ }
1804
+ case ':': {
1805
+ /* a bare symbol VALUE: emitted as its name string */
1806
+ if (mj_symbol(c, 1) != 0)
1807
+ return -1;
1808
+ return 0;
1809
+ }
1810
+ case '@': {
1811
+ /* an object link: re-walk from the registered position */
1812
+ int64_t idx;
1813
+ if (mj_long(c, &idx) != 0)
1814
+ return -1;
1815
+ if (idx < 0 || (size_t)idx >= c->objs_n)
1816
+ return mj_fail(c);
1817
+ size_t save = c->s.at;
1818
+ c->s.at = c->objs[idx].pos;
1819
+ /* cycle guard: the link target is in the active chain */
1820
+ for (size_t k = 0; k < c->chain_n; k++) {
1821
+ if (c->chain[k] == c->objs[idx].pos) {
1822
+ c->s.at = save;
1823
+ return mj_fail(c);
1824
+ }
1825
+ }
1826
+ if (c->chain_n == c->chain_cap) {
1827
+ c->chain_cap = c->chain_cap ? c->chain_cap * 2 : 16;
1828
+ c->chain = (size_t*)realloc(c->chain, c->chain_cap * sizeof(*c->chain));
1829
+ if (!c->chain) {
1830
+ c->s.at = save;
1831
+ return mj_fail(c);
1832
+ }
1833
+ }
1834
+ c->chain[c->chain_n++] = c->objs[idx].pos;
1835
+ int rc = mj_value(c);
1836
+ c->chain_n--;
1837
+ c->s.at = save;
1838
+ if (rc != 0)
1839
+ return -1;
1840
+ return 0;
1841
+ }
1842
+ default:
1843
+ return mj_fail(c);
1844
+ }
1845
+ }
1846
+
1847
+ /* an '@' object link in KEY position: the link's target re-emitted as
1848
+ * the key string (Ruby links repeated key objects — the users' "id"
1849
+ * keys, yeptris-ruby#322's corpus). The link consumes no stream
1850
+ * bytes; the key walk resumes at the caller's cursor. */
1851
+ static int mj_link_key(mj_ctx* c, int64_t idx) {
1852
+ if (idx < 0 || (size_t)idx >= c->objs_n)
1853
+ return mj_fail(c);
1854
+ size_t save = c->s.at;
1855
+ for (size_t k = 0; k < c->chain_n; k++) {
1856
+ if (c->chain[k] == c->objs[idx].pos) {
1857
+ c->s.at = save;
1858
+ return mj_fail(c);
1859
+ }
1860
+ }
1861
+ c->s.at = c->objs[idx].pos;
1862
+ if (c->chain_n == c->chain_cap) {
1863
+ c->chain_cap = c->chain_cap ? c->chain_cap * 2 : 16;
1864
+ c->chain = (size_t*)realloc(c->chain, c->chain_cap * sizeof(*c->chain));
1865
+ if (!c->chain) {
1866
+ c->s.at = save;
1867
+ return mj_fail(c);
1868
+ }
1869
+ }
1870
+ c->chain[c->chain_n++] = c->objs[idx].pos;
1871
+ int rc = mj_value(c);
1872
+ c->chain_n--;
1873
+ c->s.at = save;
1874
+ return rc;
1875
+ }
1876
+
1877
+ YEPTRIS_API YeptrisStatus yeptris_json_from_marshal(const char* m, size_t m_len, int compact,
1878
+ char** out, size_t* out_len) {
1879
+ if (out == NULL || out_len == NULL)
1880
+ return YEPTRIS_ERROR_ARG;
1881
+ *out = NULL;
1882
+ *out_len = 0;
1883
+ if (m == NULL || m_len < 2 || (unsigned char)m[0] != 0x04 || (unsigned char)m[1] != 0x08) {
1884
+ return YEPTRIS_ERROR_UNSUPPORTED;
1885
+ }
1886
+ yep_emitter em;
1887
+ memset(&em, 0, sizeof(em));
1888
+ em.doc = NULL;
1889
+ yep_writer* w = &em.w;
1890
+ w->sc_dec = NULL;
1891
+ w->sc_i = 0;
1892
+ w->json = 1;
1893
+ w->json_compact = compact ? 1 : 0;
1894
+ w->json_pretty = 0;
1895
+ w->canonical = 0;
1896
+ w->explicit_doc_start = 0;
1897
+ w->best_width = 0;
1898
+ w->sv_input = NULL;
1899
+ w->sv_arena = NULL;
1900
+ w->sink = NULL;
1901
+ w->force_flow = 0;
1902
+ w->in_flow = 0;
1903
+ w->flow_item = 0;
1904
+ w->unserializable = 0;
1905
+
1906
+ mj_ctx c;
1907
+ memset(&c, 0, sizeof(c));
1908
+ c.w = w;
1909
+ c.s.p = m;
1910
+ c.s.len = m_len;
1911
+ c.s.at = 2; /* the 4.8 header */
1912
+ c.compact = compact ? 1 : 0;
1913
+
1914
+ /* one wet pass into a growable buffer (the #352 slice-2 shape) */
1915
+ size_t est = m_len * 2 + 64;
1916
+ char* buf = (char*)malloc(est);
1917
+ if (!buf)
1918
+ return YEPTRIS_ERROR_MEMORY;
1919
+ w->p = buf;
1920
+ w->cap = est;
1921
+ w->grow = 1;
1922
+
1923
+ mj_value(&c);
1924
+
1925
+ int failed = c.failed;
1926
+ size_t wrote = w->len;
1927
+ buf = w->p;
1928
+ free(c.syms);
1929
+ free(c.objs);
1930
+ free(c.chain);
1931
+ if (failed || w->oom) {
1932
+ free(buf);
1933
+ return failed ? (YeptrisStatus)failed : YEPTRIS_ERROR_MEMORY;
1934
+ }
1935
+ buf[wrote] = '\0';
1936
+ *out = buf;
1937
+ *out_len = wrote;
1938
+ return YEPTRIS_OK;
1939
+ }
1940
+
1406
1941
  size_t yep_emit_run(yep_emitter* em, int dry) {
1407
1942
  yep_writer* w = &em->w;
1408
1943
  const yep_dom* d = em->doc->dom;
@@ -1895,7 +1895,12 @@ static int e_flow_key_after(const char* p, size_t len, size_t close) {
1895
1895
  static int e_flow_floor_ok(const char* p, size_t open, size_t close, uint16_t floor) {
1896
1896
  size_t j = open;
1897
1897
  while (j < close) {
1898
- if (p[j] == '\n') {
1898
+ /* the break rule is the scan's: \n, or a \r the next byte does
1899
+ * not join into CRLF — a \n-only walk let a CR-broken content
1900
+ * line escape the indent check (issue #771: the whole parse
1901
+ * took the fast path past a floor violation the windowed
1902
+ * parse's general kernel rejected) */
1903
+ if (p[j] == '\n' || (p[j] == '\r' && (j + 1 >= close || p[j + 1] != '\n'))) {
1899
1904
  size_t k = j + 1;
1900
1905
  uint32_t ind = 0;
1901
1906
  while (k < close && p[k] == ' ') {
@@ -1908,12 +1913,17 @@ static int e_flow_floor_ok(const char* p, size_t open, size_t close, uint16_t fl
1908
1913
  }
1909
1914
  j++;
1910
1915
  }
1911
- if (close < (size_t)-1 && close > open) {
1912
- size_t ls = close;
1913
- uint32_t ind = 0;
1914
- while (ls > 0 && p[ls - 1] != '\n') {
1915
- ls--;
1916
+ if (close > open) {
1917
+ size_t from = open;
1918
+ uint32_t ln = 0;
1919
+ size_t ls = open;
1920
+ yep_scan_advance_line(p, &from, close, &ln, &ls);
1921
+ /* advance_line cannot peek past its `to`: a lone \r at
1922
+ * close - 1 ends the closer's line AT close */
1923
+ if (p[close - 1] == '\r' && p[close] != '\n') {
1924
+ ls = close;
1916
1925
  }
1926
+ uint32_t ind = 0;
1917
1927
  while (ls < close && p[ls] == ' ') {
1918
1928
  ind++;
1919
1929
  ls++;
@@ -1950,7 +1960,8 @@ static int e_flow_json(yep_engine* e, yep_view anchor, yep_view tag, uint32_t an
1950
1960
  return 0; /* not JSON-class: the general kernel */
1951
1961
  }
1952
1962
  if (rc == 1) {
1953
- int single = (memchr(p + open_pos, '\n', bclose - open_pos) == NULL);
1963
+ int single = (memchr(p + open_pos, '\n', bclose - open_pos) == NULL) &&
1964
+ (memchr(p + open_pos, '\r', bclose - open_pos) == NULL);
1954
1965
  if (e_flow_key_after(p, len, bclose) ||
1955
1966
  (e->flow_enforce && !single &&
1956
1967
  !e_flow_floor_ok(p, open_pos, bclose, e->flow_floor))) {
@@ -153,8 +153,46 @@ int yep_scan_prop_char(unsigned char c) {
153
153
  return !yep_ct_any(c, YEP_CT_BLANK | YEP_CT_LBREAK | YEP_CT_FLOW_IND) && c != ',' && c != '#';
154
154
  }
155
155
 
156
+ int yep_scan_dq_forced(const char* p, uint32_t len) {
157
+ for (uint32_t k = 0; k + 1 < len; k++) {
158
+ if ((unsigned char)p[k] == 0xc2 && (unsigned char)p[k + 1] == 0x85) {
159
+ return 1;
160
+ }
161
+ /* LS/PS are breaks like NEL: only their escapes are stable */
162
+ if ((unsigned char)p[k] == 0xe2 && k + 2 < len && (unsigned char)p[k + 1] == 0x80 &&
163
+ ((unsigned char)p[k + 2] == 0xa8 || (unsigned char)p[k + 2] == 0xa9)) {
164
+ return 1;
165
+ }
166
+ if ((unsigned char)p[k] == 0xef && k + 2 < len && (unsigned char)p[k + 1] == 0xbb &&
167
+ (unsigned char)p[k + 2] == 0xbf) {
168
+ return 1;
169
+ }
170
+ if ((unsigned char)p[k] == 0xef && k + 2 < len && (unsigned char)p[k + 1] == 0xbf &&
171
+ ((unsigned char)p[k + 2] == 0xbe || (unsigned char)p[k + 2] == 0xbf)) {
172
+ return 1; /* noncharacter: only its escape re-parses */
173
+ }
174
+ }
175
+ return 0;
176
+ }
177
+
156
178
  size_t yep_scan_prop_end(const char* p, size_t len, size_t pos) {
157
- while (pos < len && yep_scan_prop_char((unsigned char)p[pos])) {
179
+ while (pos < len) {
180
+ unsigned char c = (unsigned char)p[pos];
181
+ if (!yep_scan_prop_char(c)) {
182
+ break;
183
+ }
184
+ /* NEL/LS/PS end the name (libyaml's IS_BLANKZ anchor law: the
185
+ * suite's ns-anchor-char excludes breaks): the byte-level
186
+ * table cannot classify a multibyte break, and the name run
187
+ * then swallowed it — the r69/r70 nightly divergence class
188
+ * ("&k:":(<PS>..."), shape-dependent clip downstream */
189
+ if (c == 0xC2 && pos + 1 < len && (unsigned char)p[pos + 1] == 0x85) {
190
+ break;
191
+ }
192
+ if (c == 0xE2 && pos + 2 < len && (unsigned char)p[pos + 1] == 0x80 &&
193
+ ((unsigned char)p[pos + 2] & 0xFE) == 0xA8) {
194
+ break;
195
+ }
158
196
  pos++;
159
197
  }
160
198
  return pos;
@@ -98,6 +98,12 @@ int yep_scan_prop_char(unsigned char c);
98
98
  /* First offset at/after pos that cannot continue an anchor/alias name. */
99
99
  size_t yep_scan_prop_end(const char* p, size_t len, size_t pos);
100
100
 
101
+ /* The dq-forced byte set (issue #352 lane 2): NEL, LS/PS, the BOM,
102
+ * and the noncharacters — the bytes whose only stable spelling is
103
+ * the double-quoted escape. One home for the emitter's route check
104
+ * AND the DOM's parse-time fact bit, so the two cannot drift. */
105
+ int yep_scan_dq_forced(const char* p, uint32_t len);
106
+
101
107
  /* Tag-shorthand suffix byte: libyaml's URI set (";/?:@&=+$,_.!~*'()%"
102
108
  * plus alphanumerics; flow indicators and ',' terminate instead).
103
109
  * Stray non-set bytes stay visible as the props follower, which
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.45.0
4
+ version: 0.6.46.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-10-26 00:00:00.000000000 Z
11
+ date: 2026-10-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ffi