yeptris 0.6.32.1-aarch64-linux → 0.6.33.1-aarch64-linux

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: 27054940f25d07b0af5eebd56ff85e79de42305a7ab59e34f714b48ab523458c
4
- data.tar.gz: aef30cec9183a9caf10864e3d112d109bf3c81f93000c11c87d5e4547b22ba50
3
+ metadata.gz: aa8d3b78ec99584df719ea52b83de134ccec4b1c5c00dabbfd54e2850c2789c5
4
+ data.tar.gz: f259f9122e64be66be515aa9e41a8bf8d74bec2aeaf4f6a6f9855ed416c899b4
5
5
  SHA512:
6
- metadata.gz: 5567d722a33e0f30f60ce64345be3c2ee57657bdd884b82f159f6ec9d5ec415ce6c83cd70ae317d3449e13824fd5a018b1e3be7c62440e02832c6c2b9bf479cf
7
- data.tar.gz: 1e178a8fe0eda68348d962919ed6523c0347cc851de265804307a1d09aa2c5c80cc2859452d19b4c0fe1c14463184974d017d0000220db234d135905b5ada08c
6
+ metadata.gz: 5bf5c78083f143525812c7d41ec69b515a9836f0f061e6a621c1b4f7a63a6d89e54eccf11cdda6c9cbc35943637d946cf39a2f06126d19e97acbe6495a1fe6fc
7
+ data.tar.gz: 2ff20e717f4351f3b74305464ceb7deab18442090748d5d79504574639df0ef9ee42149b426e813ca683daf2bec620472f3791b194820e8888ad004209da0709
Binary file
Binary file
Binary file
data/lib/yeptris/node.rb CHANGED
@@ -50,7 +50,7 @@ class Yeptris::Node
50
50
 
51
51
  # Scalar content / alias name (UTF-8 String), nil for collections.
52
52
  def value
53
- len = ::FFI::MemoryPointer.new(:size_t)
53
+ len = len_buffer
54
54
  ptr = alive { Yeptris::FFI.yeptris_node_value(@c_ptr, len) }
55
55
  return nil if ptr.null?
56
56
 
@@ -69,9 +69,18 @@ class Yeptris::Node
69
69
  STYLES[alive { Yeptris::FFI.yeptris_node_style(@c_ptr) }]
70
70
  end
71
71
 
72
+ # The out-length receiver: ONE buffer per thread, reused. A fresh
73
+ # FFI::MemoryPointer per accessor call was an allocation storm that
74
+ # stalled the Windows heap under the #182 churn loop (the #504
75
+ # family — the hang's stack sits in FFI::MemoryPointer#initialize,
76
+ # node.rb anchor/tag reads inside Integer#times with GC.start).
77
+ def len_buffer
78
+ Thread.current[:yeptris_len_buf] ||= ::FFI::MemoryPointer.new(:size_t)
79
+ end
80
+
72
81
  # Explicit tag URI when present, else nil.
73
82
  def tag
74
- len = ::FFI::MemoryPointer.new(:size_t)
83
+ len = len_buffer
75
84
  ptr = alive { Yeptris::FFI.yeptris_node_tag(@c_ptr, len) }
76
85
  return nil if ptr.null?
77
86
 
@@ -79,7 +88,7 @@ class Yeptris::Node
79
88
  end
80
89
 
81
90
  def anchor
82
- len = ::FFI::MemoryPointer.new(:size_t)
91
+ len = len_buffer
83
92
  ptr = alive { Yeptris::FFI.yeptris_node_anchor(@c_ptr, len) }
84
93
  return nil if ptr.null?
85
94
 
@@ -216,7 +216,7 @@ module Yeptris
216
216
  # explicit !!str tag (single-quoted) so it does not
217
217
  # re-load as a merge
218
218
  key_node = @tree.new_scalar("<<", :single_quoted)
219
- key_node.set_tag("!!str")
219
+ key_node.set_tag("tag:yaml.org,2002:str")
220
220
  m.map_add_node(key_node, visit(v))
221
221
  else
222
222
  m.map_add(key_text(k), visit(v))
data/lib/yeptris/yaml.rb CHANGED
@@ -364,7 +364,7 @@ module Yeptris
364
364
  # stdlib yaml_tree: a literal chevron key carries the
365
365
  # explicit !!str tag so it does not re-load as a merge
366
366
  key_node = doc.new_scalar("<<", :single_quoted)
367
- key_node.set_tag("!!str")
367
+ key_node.set_tag("tag:yaml.org,2002:str")
368
368
  m.map_add_node(key_node, build(doc, v, seen))
369
369
  else
370
370
  m.map_add(key_text(k), build(doc, v, seen))
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.32.1".freeze
6
+ VERSION = "0.6.33.1".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
data/libyeptris.so CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.32
6
+ VERSION 0.6.33
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -98,15 +98,27 @@ if(YEPTRIS_ENABLE_UBSAN)
98
98
  message(STATUS "UndefinedBehaviorSanitizer: ENABLED (fatal)")
99
99
  endif()
100
100
 
101
- # libFuzzer implies ASAN (libleptris TODO 40 pattern).
101
+ # libFuzzer implies ASAN+UBSan (the teptris fuzz.yml pattern: the
102
+ # memory-safety contracts ride the SAME binary as the fuzzer, so every
103
+ # nightly round reports UB at the crash site instead of libFuzzer's
104
+ # "combine with AddressSanitizer" note).
102
105
  if(YEPTRIS_ENABLE_FUZZING)
103
106
  # instrument everything, but link the fuzzer MAIN only into the
104
107
  # harnesses: a global -fsanitize=fuzzer link option demands
105
108
  # LLVMFuzzerTestOneInput from every executable (the CLI and the
106
109
  # test binaries died on it once CXX also pinned to clang++)
107
- add_compile_options(-fsanitize=fuzzer-no-link,address -fno-omit-frame-pointer -g)
108
- add_link_options(-fsanitize=address)
109
- message(STATUS "libFuzzer: ENABLED (implies ASAN)")
110
+ if(APPLE)
111
+ # Apple ld64 cannot link UBSan'd static archives
112
+ # ("invalid r_symbolnum"): ASan-only locally
113
+ add_compile_options(-fsanitize=fuzzer-no-link,address -fno-omit-frame-pointer -g)
114
+ add_link_options(-fsanitize=address)
115
+ message(STATUS "libFuzzer: ENABLED (implies ASAN; UBSan needs linux ld)")
116
+ else()
117
+ add_compile_options(-fsanitize=fuzzer-no-link,address,undefined
118
+ -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g)
119
+ add_link_options(-fsanitize=address,undefined)
120
+ message(STATUS "libFuzzer: ENABLED (implies ASAN+UBSan)")
121
+ endif()
110
122
  endif()
111
123
 
112
124
  # -------------------------------------------------------------------- LTO --
@@ -392,6 +392,27 @@ int yep_dom_on_event(void* ctx, const yep_event* ev) {
392
392
  if (d->depth == 0) {
393
393
  return -1;
394
394
  }
395
+ if (ev->type == YEP_EV_MAP_END && d->map_pending_key[d->depth - 1]) {
396
+ /* an odd child reached the mapping: the unpaired node —
397
+ * possibly an ANCHORED one — used to be dropped here, its
398
+ * anchor lost, and a later alias then emitted a dangling
399
+ * reference the re-parse rejected (the nightly's round
400
+ * 102). Pair it with a null: "k:" with nothing is a null
401
+ * value in every other position. */
402
+ yep_event nv;
403
+ memset(&nv, 0, sizeof(nv));
404
+ nv.type = YEP_EV_SCALAR;
405
+ nv.style = YEP_STYLE_PLAIN;
406
+ nv.implicit = 1;
407
+ uint32_t nid = dom_new_node(d, &nv, YEP_DOM_SCALAR);
408
+ if (nid == UINT32_MAX) {
409
+ return -1;
410
+ }
411
+ uint32_t top = d->stack[d->depth - 1];
412
+ dom_link(d, top, d->pending_key_id[d->depth - 1]);
413
+ dom_link(d, top, nid);
414
+ d->map_pending_key[d->depth - 1] = 0;
415
+ }
395
416
  d->depth--;
396
417
  return 0;
397
418
 
@@ -729,6 +750,7 @@ static int jb_value(jbuilder* b) {
729
750
  return 0;
730
751
  }
731
752
  for (;;) {
753
+ size_t member_start = b->i;
732
754
  int rc = jb_value(b);
733
755
  if (rc != 0) {
734
756
  return rc;
@@ -738,6 +760,14 @@ static int jb_value(jbuilder* b) {
738
760
  /* a value must be followed by , or } — the map's key
739
761
  * slot is pending until the ':' value completes */
740
762
  if (b->i < b->len && b->p[b->i] == ':' && d->map_pending_key[d->depth - 1] != 0) {
763
+ /* libyaml's simple-key law rides the JSON-class
764
+ * route too: a key whose colon distance crosses
765
+ * 1024 characters defers to the engine's clean
766
+ * error (run 38143486175) */
767
+ if (b->i - member_start > 1024 &&
768
+ yep_simple_key_chars(b->p + member_start, b->i - member_start) > 1024) {
769
+ return -2;
770
+ }
741
771
  b->i++;
742
772
  rc = jb_value(b);
743
773
  if (rc != 0) {
@@ -820,8 +850,11 @@ int dom_on_flow_build(void* ctx, const char* p, size_t open, size_t len, uint32_
820
850
  yep_json_walk w;
821
851
  yep_json_tok t;
822
852
  yep_json_walk_init(&w, p, len, open, max_depth);
823
- w.strict = d->flow_strict; /* the strict-JSON route rides the same
824
- fused walk (TODO.restructure/81) */
853
+ w.strict = 1; /* the same byte-for-byte JSON gate as the engine's
854
+ * pass 1: a lenient walk split number/literal keys
855
+ * across the fast/slow paths (whole {9->1} vs chunked
856
+ * {"9:1"->null}); flow_strict keeps owning the TAG
857
+ * route only (TODO.restructure/81) */
825
858
  for (;;) {
826
859
  yep_jw_status st = yep_json_walk_next(&w, &t);
827
860
  if (st == YEP_JW_REJECT) {
@@ -976,6 +1009,23 @@ int dom_from_tape(yep_dom* d, const yeptris_json_tape* t) {
976
1009
  if (d->depth == 0) {
977
1010
  return -1;
978
1011
  }
1012
+ if (d->nodes[d->stack[d->depth - 1]].kind == YEP_DOM_MAPPING &&
1013
+ d->map_pending_key[d->depth - 1]) {
1014
+ /* an odd child reached the mapping (see the event-path
1015
+ * MAP_END): pair it with a null instead of dropping it */
1016
+ yep_event nv;
1017
+ memset(&nv, 0, sizeof(nv));
1018
+ nv.type = YEP_EV_SCALAR;
1019
+ nv.style = YEP_STYLE_PLAIN;
1020
+ nv.implicit = 1;
1021
+ uint32_t nid = dom_new_node(d, &nv, YEP_DOM_SCALAR);
1022
+ if (nid == UINT32_MAX) {
1023
+ return -1;
1024
+ }
1025
+ dom_link(d, d->stack[d->depth - 1], d->pending_key_id[d->depth - 1]);
1026
+ dom_link(d, d->stack[d->depth - 1], nid);
1027
+ d->map_pending_key[d->depth - 1] = 0;
1028
+ }
979
1029
  d->depth--;
980
1030
  break;
981
1031
  case YEP_T_NULL:
@@ -20,6 +20,7 @@
20
20
  #include "dom/dom.h"
21
21
  #include "memory/allocator.h"
22
22
  #include "memory/pool.h"
23
+ #include "parse/scalars.h"
23
24
  #include "resolve/resolver.h"
24
25
  #include "scan/json.h"
25
26
  #include "scan/scan.h"
@@ -355,6 +356,14 @@ static uint32_t yt_flow_scan(const yeptris_json_tape* s, int* long_key) {
355
356
  return 0xFFFFu; /* deeper than the scratch stack: the
356
357
  * general kernel's depth path decides */
357
358
  }
359
+ /* the container consumes the parent frame's pending slot
360
+ * (key or value); after it closes the OTHER slot is next —
361
+ * a container value left key_next pinned at 1, and every
362
+ * key after a container value skipped the law (the
363
+ * nightly's round-108 shape: {"": [...], "giant": x}) */
364
+ if (d > 0 && st[d].map) {
365
+ st[d].key_next = (uint8_t)!st[d].key_next;
366
+ }
358
367
  d++;
359
368
  if ((uint32_t)d > maxd) {
360
369
  maxd = (uint32_t)d;
@@ -371,7 +380,12 @@ static uint32_t yt_flow_scan(const yeptris_json_tape* s, int* long_key) {
371
380
  }
372
381
  if (st[d].map && st[d].key_next == 0) {
373
382
  uint32_t mlen = (uint32_t)(r >> 8) & 0xFFFFFFu;
374
- if (kind == YEP_T_STR && mlen + 2u > 1024u) {
383
+ if (kind == YEP_T_STR && mlen + 2u > 1024u &&
384
+ yep_simple_key_chars((const char*)s->_src + (size_t)((uint32_t)(r >> 32)), mlen) +
385
+ 2 >
386
+ 1024) {
387
+ /* the budget counts characters (the round-108 shrink:
388
+ * a 2-byte code point at byte delta 1025 is legal) */
375
389
  *long_key = 1;
376
390
  }
377
391
  st[d].key_next = 1;
@@ -429,7 +443,9 @@ static int yt_flow_classify_record(yep_ytape* t, const char* p, size_t open, siz
429
443
  yep_json_walk w;
430
444
  yep_json_tok tok;
431
445
  yep_json_walk_init(&w, p, len, open, max_depth);
432
- w.strict = t->flow_strict;
446
+ w.strict = 1; /* mirrors the engine pass-1 / dom builder gates:
447
+ * reject anything non-JSON so the span falls to
448
+ * the general kernel (whole/chunked agreement) */
433
449
  int exhausted = 0;
434
450
  for (;;) {
435
451
  if (w.i - open > 256) {
@@ -1128,8 +1144,11 @@ static size_t yt_flat_run(yep_ytape* t, const char* p, size_t pos, size_t len, u
1128
1144
  }
1129
1145
  unsigned char c = (unsigned char)p[li];
1130
1146
  if (c == '\t' || c == '-' || c == '&' || c == '!' || c == '*' || c == '\'' || c == '"' ||
1131
- c == '[' || c == '{' || c == '|' || c == '>' || c == '%' || c == '?') {
1132
- return pos; /* indicators, tab-led: the full arms */
1147
+ c == '[' || c == '{' || c == '|' || c == '>' || c == '%' || c == '?' ||
1148
+ !yep_plain_first_ok(c)) {
1149
+ return pos; /* indicators, tab-led, and the plain-first law
1150
+ (a ']' key rode the flat-run — the nightly's
1151
+ marshal crasher "<: .ba\n]ge: 21"): full arms */
1133
1152
  }
1134
1153
  if (!stop_set || p[ls] != ':' || (ls + 1 < le && p[ls + 1] != ' ' && p[ls + 1] != '\t')) {
1135
1154
  return pos; /* not a plain key: value line */
@@ -1192,6 +1211,11 @@ static size_t yt_flat_run(yep_ytape* t, const char* p, size_t pos, size_t len, u
1192
1211
  (vt + 1 >= le || p[vt + 1] == ' ' || p[vt + 1] == '\t' || p[vt + 1] == '#'))) {
1193
1212
  return pos; /* block/flow/anchor values: the full arms */
1194
1213
  }
1214
+ if (!yep_plain_first_ok(vc)) {
1215
+ return pos; /* ']' '}' ',' '@' '`': no plain value can start
1216
+ * there (the nightly's "k1: --a &p{\n0: ]" marshal
1217
+ * crasher) — the engine errors cleanly */
1218
+ }
1195
1219
  /* a terminating ':' inside the value is the mapping-values error */
1196
1220
  {
1197
1221
  const char* h = (const char*)memchr(p + vt, ':', le - vt);
@@ -1233,6 +1257,15 @@ static int yt_f_run(yt_fused* F) {
1233
1257
  st[0].started = 1; /* the root's start event defers to its first pair */
1234
1258
  int root_open = 0;
1235
1259
  int root_evented = 0;
1260
+ uint32_t root_pair_col = 0; /* the root map's first pair line's column
1261
+ * (0 = none seen): the second-root dedent
1262
+ * law reads this, NOT content_col — the
1263
+ * pending arm never set content_col (" k:
1264
+ * &a", the nightly's run 39064966578,
1265
+ * issue #630) and arming content_col here
1266
+ * instead broke the " k: \n j: w" siblings
1267
+ * (the next nightly's crash-753ce4c1: the
1268
+ * map dissolved into four root scalars) */
1236
1269
  (void)yt_put(t, ((uint64_t)YTP_EV_COMPACT << 56) | 1u);
1237
1270
  (void)yt_put(t, ((uint64_t)YTP_EV_COMPACT << 56) | 3u);
1238
1271
  size_t pos = 0;
@@ -1362,6 +1395,28 @@ static int yt_f_run(yt_fused* F) {
1362
1395
  } else {
1363
1396
  uint8_t of = yt_f_open_form(&st[depth - 1], depth == 1, st[depth].indent);
1364
1397
  yt_f_open(F, &st[depth], of);
1398
+ if (depth == 1 && st[0].content_col == 0) {
1399
+ /* the root bookkeeping the anchored branch carries:
1400
+ * without it a dedent past the root map's column
1401
+ * pops to a content_col-0 st[0] and the pair arm
1402
+ * read "root pairs at column 0" — the nightly's
1403
+ * run 38128905644, " american:\n - Boston...\n
1404
+ * national:" split into PHANTOM DOCUMENTS
1405
+ * ("---\nb\n---\nv") while the engine rejected */
1406
+ st[0].content_col = st[depth].indent + 1;
1407
+ }
1408
+ if (st[depth].content_col == 0) {
1409
+ /* the opened map's pairs live at the KEY's
1410
+ * column (the anchored branch tracks this;
1411
+ * without it the off-column guard never fires
1412
+ * and a key line BETWEEN the resolved seq's
1413
+ * column and the parent's fell through to a
1414
+ * SILENTLY TRUNCATED document — the nightly's
1415
+ * run 38118641722, "ue:\n - one\n woc:\n
1416
+ * - one\n wt o- : thrt" dropped lines 3-5
1417
+ * while the engine rejected at 3:2) */
1418
+ st[depth].content_col = st[depth].indent + 1;
1419
+ }
1365
1420
  root_open = 1;
1366
1421
  st[depth].pending = 0;
1367
1422
  }
@@ -1380,6 +1435,22 @@ static int yt_f_run(yt_fused* F) {
1380
1435
  while (depth > 0 && (st[depth].kind == 1 ? (col < st[depth].indent ||
1381
1436
  (col <= st[depth].indent && !dash))
1382
1437
  : col < st[depth].indent)) {
1438
+ if (st[depth].kind == 1 && col == st[depth].indent && !dash &&
1439
+ !(depth > 1 && st[depth - 1].kind == 0 &&
1440
+ st[depth - 1].indent == st[depth].indent)) {
1441
+ return 1; /* a non-dash line at an INDENTED seq's
1442
+ * column: the seq owns its column, the
1443
+ * pair cannot join it and cannot close
1444
+ * it (libyaml: "did not find expected
1445
+ * '-' indicator"; the nightly's run
1446
+ * 38117287882, "amer:\n - j\n n:\n
1447
+ * - Bksl,} -- }" — the runner popped
1448
+ * the seq and SILENTLY DROPPED the
1449
+ * lines while the engine rejected).
1450
+ * Only the INDENTLESS seq — parent map
1451
+ * keys at the same column — closes on
1452
+ * a key line there ("k:\n- j\nn: v") */
1453
+ }
1383
1454
  if (st[depth].pending) {
1384
1455
  return 1; /* empty "key:" mid-doc: the engine owns it */
1385
1456
  }
@@ -1457,6 +1528,40 @@ static int yt_f_run(yt_fused* F) {
1457
1528
  !(c == '&' || c == '!' || c == '*' || c == '\'' || c == '"' || c == '[' ||
1458
1529
  c == '{' || c == '|' || c == '>' || c == '%' || c == '?' || c == '-') &&
1459
1530
  yep_plain_first_ok((unsigned char)c)) {
1531
+ if (depth == 0) {
1532
+ if (root_pair_col != 0 && col < root_pair_col) {
1533
+ /* a dedent below the root map's own pair column:
1534
+ * a SECOND root without "---" (issue #630, " k:
1535
+ * &a\n - r\na: b" — the pending-value root arm
1536
+ * never arms content_col, so the off-column
1537
+ * guard below cannot see this; the engine and
1538
+ * libyaml reject with "did not find expected
1539
+ * <document start>") */
1540
+ return 1;
1541
+ }
1542
+ if (root_pair_col == 0) {
1543
+ root_pair_col = col; /* the first root pair owns its column */
1544
+ }
1545
+ }
1546
+ if (st[depth].kind == 0 &&
1547
+ ((st[depth].content_col != 0 && col + 1 != st[depth].content_col &&
1548
+ (col != st[depth].indent || (depth == 0 && st[0].content_col > 1))) ||
1549
+ (st[depth].content_col == 0 && depth > 0 && !st[depth].pending &&
1550
+ col != st[depth].indent))) {
1551
+ /* an OFF-COLUMN pair. No pending key awaits it, so a
1552
+ * more-indented line cannot open a child and a
1553
+ * dedented one is a second root (the catch-39 law;
1554
+ * libyaml: "mapping values are not allowed in this
1555
+ * context"). Two states: a frame with its own content
1556
+ * column rejects every other column; a frame whose
1557
+ * content map already closed (the phantom close above)
1558
+ * only accepts pairs at its own key column. The pop
1559
+ * loop pops on col < indent alone — without these
1560
+ * checks "a:\n b: 1\nc: 2\n d: 3" rode the stale
1561
+ * frame as a phantom sibling (the nightly's run
1562
+ * 38179552341) */
1563
+ return 1;
1564
+ }
1460
1565
  uint32_t kend = line_stop;
1461
1566
  while (kend > t0 && (p[kend - 1] == ' ' || p[kend - 1] == '\t')) {
1462
1567
  kend--;
@@ -1488,6 +1593,12 @@ static int yt_f_run(yt_fused* F) {
1488
1593
  return 1; /* an anchor needs a name (libyaml):
1489
1594
  the engine owns the error */
1490
1595
  }
1596
+ if (ne < line_end && p[ne] == '#') {
1597
+ return 1; /* a glued '#' is junk on the name
1598
+ (libyaml: "did not find expected
1599
+ alphabetic or numeric character") —
1600
+ the engine owns it */
1601
+ }
1491
1602
  F->anchor_seq++;
1492
1603
  aid = F->anchor_seq;
1493
1604
  if (!yt_anchor_table(F)) {
@@ -1504,8 +1615,24 @@ static int yt_f_run(yt_fused* F) {
1504
1615
  * (aid rides it: an anchored container resolves through
1505
1616
  * the S2900 + anchored-start form, not on_block_open) */
1506
1617
  if (st[depth].kind == 0 && st[depth].content_col != 0 &&
1507
- col + 1 != st[depth].content_col && col != st[depth].indent) {
1508
- return 1; /* off-column: a continuation line — the multiline arm */
1618
+ col + 1 != st[depth].content_col &&
1619
+ (col != st[depth].indent || (depth == 0 && st[0].content_col > 1))) {
1620
+ return 1; /* off-column: a continuation line — the multiline arm;
1621
+ * at the ROOT the indent exemption dies once pairs
1622
+ * live above column 0: a col-0 line under a col-1
1623
+ * root is a DEDENT — a second root without "---",
1624
+ * the engine's error (libyaml: "did not find
1625
+ * expected <document start>") */
1626
+ }
1627
+ if (st[depth].kind != 0) {
1628
+ return 1; /* a pending "key:" over a SEQ frame: the
1629
+ * key cannot join the sequence (libyaml
1630
+ * "mapping values are not allowed in
1631
+ * this context"; the nightly's run
1632
+ * 38127584324, "k:\n- 42\n bar:\n
1633
+ * - 45 e." — the pending pushed UNDER
1634
+ * the seq and materialized as an ITEM
1635
+ * while the engine rejected) */
1509
1636
  }
1510
1637
  if (st[depth].kind == 0 && st[depth].content_col != 0 &&
1511
1638
  col + 1 != st[depth].content_col && st[depth].phantom) {
@@ -1513,10 +1640,13 @@ static int yt_f_run(yt_fused* F) {
1513
1640
  st[depth].phantom = 0;
1514
1641
  }
1515
1642
  if (depth > 0 && st[depth].pending) {
1516
- uint8_t of = yt_f_open_form(&st[depth - 1], depth == 1, st[depth].indent);
1517
- yt_f_open(F, &st[depth], of);
1518
- root_open = 1;
1519
- st[depth].pending = 0;
1643
+ /* chained same-column pending ("a:\na:"): resolving
1644
+ * the first via yt_f_open corrupts the frame mapping
1645
+ * when the second's container carries valued pairs —
1646
+ * the OPEN'd map swallows the second key as the
1647
+ * first pair's value (the nightly's run 38110714729
1648
+ * tree corruption). The engine owns the shape */
1649
+ return 1;
1520
1650
  }
1521
1651
  /* a spent key-SCALAR frame (resolved, phantom closed,
1522
1652
  * no map of its own) gives its slot to the sibling —
@@ -1540,10 +1670,22 @@ static int yt_f_run(yt_fused* F) {
1540
1670
  st[depth].anchored_first = (uint8_t)(aid != 0);
1541
1671
  } else {
1542
1672
  if (st[depth].kind == 0 && st[depth].content_col != 0 &&
1543
- col + 1 != st[depth].content_col && col != st[depth].indent) {
1544
- return 1; /* off-column: a continuation line — the multiline arm */
1673
+ col + 1 != st[depth].content_col &&
1674
+ (col != st[depth].indent || (depth == 0 && st[0].content_col > 1))) {
1675
+ return 1; /* off-column: a continuation line — the multiline arm;
1676
+ * the root's indent exemption dies once pairs
1677
+ * live above column 0 (dedent = second root) */
1545
1678
  }
1546
1679
  if (depth > 0 && st[depth].pending && st[depth].aid != 0) {
1680
+ if (depth > 1) {
1681
+ /* an anchored pending BELOW the root frame: the
1682
+ * deferral's records only flush at depth 1 — a
1683
+ * deeper one drops its pairs at EOF ("4:\n4: r\n
1684
+ * 5:\n &5\n 5: e" lost the anchored map; the
1685
+ * nightly's run 38202382251) — the engine
1686
+ * resolves it */
1687
+ return 1;
1688
+ }
1547
1689
  if (col <= st[depth].indent) {
1548
1690
  return 1; /* same-column "content": the anchored value is
1549
1691
  * null and this line is a sibling — the engine */
@@ -1588,6 +1730,14 @@ static int yt_f_run(yt_fused* F) {
1588
1730
  } else if (depth > 0 && st[depth].pending) {
1589
1731
  uint8_t of = yt_f_open_form(&st[depth - 1], depth == 1, st[depth].indent);
1590
1732
  yt_f_open(F, &st[depth], of);
1733
+ if (depth == 1 && st[0].content_col == 0) {
1734
+ /* the pending->MAP resolution needs the same
1735
+ * root bookkeeping the seq arm gained (run
1736
+ * 38134498534, " e:\n f: g\nh: i" — the
1737
+ * dedent popped to a content_col-0 root and
1738
+ * the doc accepted what the engine rejects) */
1739
+ st[0].content_col = st[depth].indent + 1;
1740
+ }
1591
1741
  root_open = 1;
1592
1742
  st[depth].pending = 0;
1593
1743
  if (col == st[depth].indent) {
@@ -1611,6 +1761,10 @@ static int yt_f_run(yt_fused* F) {
1611
1761
  if (st[depth].phantom) {
1612
1762
  yt_f_close(F, st[depth].kind);
1613
1763
  st[depth].phantom = 0;
1764
+ st[depth].content_col = 0; /* the content map is
1765
+ * gone: its column must not
1766
+ * greet later pairs (the
1767
+ * stale frame below) */
1614
1768
  }
1615
1769
  } else {
1616
1770
  yt_f_start(F, &st[depth]);
@@ -1633,6 +1787,29 @@ static int yt_f_run(yt_fused* F) {
1633
1787
  }
1634
1788
  ne++;
1635
1789
  }
1790
+ if (ne < line_end && p[ne] == '#') {
1791
+ return 1; /* a glued '#' is junk on the
1792
+ * name (the engine errors:
1793
+ * "did not find expected
1794
+ * alphabetic or numeric
1795
+ * character") */
1796
+ }
1797
+ {
1798
+ size_t ae = ne;
1799
+ while (ae < line_end && (p[ae] == ' ' || p[ae] == '\t')) {
1800
+ ae++;
1801
+ }
1802
+ if (ae < line_end && p[ae] != '#') {
1803
+ return 1; /* an alias node is COMPLETE —
1804
+ * on the line only a blank
1805
+ * then a comment may follow
1806
+ * ("<<: *x &y": the engine
1807
+ * errors, libyaml "did not
1808
+ * find expected key"; this
1809
+ * branch silently dropped the
1810
+ * bytes) */
1811
+ }
1812
+ }
1636
1813
  uint32_t taid = 0;
1637
1814
  if (!yt_anchor_get(F, (uint32_t)(vt + 1), (uint32_t)(ne - vt - 1), &taid)) {
1638
1815
  return 1; /* undefined alias: the engine errors */
@@ -1695,10 +1872,25 @@ static int yt_f_run(yt_fused* F) {
1695
1872
  }
1696
1873
  if (vc == '|' && aid == 0 && !st[depth].anchored_first &&
1697
1874
  (vt + 1 >= line_end || p[vt + 1] == ' ' || p[vt + 1] == '\t')) {
1875
+ /* the header tail: only blanks and an optional
1876
+ * comment may follow the indicator (libyaml:
1877
+ * "did not find expected comment or line break";
1878
+ * the nightly's run 38119775008, "ext: | c* " —
1879
+ * this arm took the literal and SILENTLY DROPPED
1880
+ * the trailing header bytes while the engine
1881
+ * rejected) */
1882
+ size_t ht = vt + 1;
1883
+ while (ht < line_end && (p[ht] == ' ' || p[ht] == '\t')) {
1884
+ ht++;
1885
+ }
1886
+ if (ht < line_end && p[ht] != '#') {
1887
+ return 1;
1888
+ }
1698
1889
  /* bare literal block, clip chomp: measure the block,
1699
1890
  * copy it into the finish pool, emit the pool form */
1700
1891
  size_t scan = next;
1701
1892
  uint32_t bindent = 0;
1893
+ uint32_t lead_blank_max = 0;
1702
1894
  int have_bi = 0, have_content = 0;
1703
1895
  size_t clen = 0, tb = 0;
1704
1896
  size_t end_scan = scan;
@@ -1730,6 +1922,9 @@ static int yt_f_run(yt_fused* F) {
1730
1922
  /* a blank line: spaces past the block indent are
1731
1923
  * CONTENT; only stripped-empty lines clip away */
1732
1924
  size_t ind = ie - ls;
1925
+ if (!have_content && ind > lead_blank_max) {
1926
+ lead_blank_max = (uint32_t)ind;
1927
+ }
1733
1928
  size_t res = have_bi && ind > bindent ? ind - bindent : 0;
1734
1929
  clen += res + 1;
1735
1930
  if (res == 0) {
@@ -1745,6 +1940,22 @@ static int yt_f_run(yt_fused* F) {
1745
1940
  if (ind <= col) {
1746
1941
  break; /* dedented past the block */
1747
1942
  }
1943
+ if (ind < lead_blank_max) {
1944
+ return 1; /* a LEADING blank line
1945
+ * with MORE indent
1946
+ * floors the block
1947
+ * (libyaml's
1948
+ * auto-detect takes
1949
+ * the blanks' max;
1950
+ * content below the
1951
+ * floor breaks the
1952
+ * map — Psych: "did
1953
+ * not find expected
1954
+ * key"; the nightly's
1955
+ * run 38163867815,
1956
+ * "a: |\n \n b"):
1957
+ * the engine's error */
1958
+ }
1748
1959
  bindent = ind;
1749
1960
  have_bi = 1;
1750
1961
  } else if (ind < bindent) {
@@ -1768,7 +1979,9 @@ static int yt_f_run(yt_fused* F) {
1768
1979
  spilled = 1;
1769
1980
  }
1770
1981
  end_scan =
1771
- (le < len && p[le] == '\r' && le + 1 < len) ? le + 2 : le + 1;
1982
+ (le < len && p[le] == '\r' && le + 1 < len && p[le + 1] == '\n')
1983
+ ? le + 2
1984
+ : le + 1; /* lone CR is a 1-byte break */
1772
1985
  scan = end_scan;
1773
1986
  }
1774
1987
  if (!have_content) {
@@ -1781,7 +1994,14 @@ static int yt_f_run(yt_fused* F) {
1781
1994
  return 1;
1782
1995
  }
1783
1996
  }
1784
- char* dst = (char*)yep_pool_alloc(F->pool, flen, 16);
1997
+ /* the BLOCK is clen-sized: both copy paths write
1998
+ * every measured line (the clip lives in the
1999
+ * SPAN, not the copy) — allocating flen wrote
2000
+ * tb bytes past the block whenever a literal
2001
+ * ended with stripped-empty lines (the nightly's
2002
+ * run 38137087116 ASan heap-buffer-overflow,
2003
+ * WRITE at ytape.c's re-walk memcpy) */
2004
+ char* dst = (char*)yep_pool_alloc(F->pool, clen, 16);
1785
2005
  if (dst == NULL) {
1786
2006
  return 1;
1787
2007
  }
@@ -1828,8 +2048,10 @@ static int yt_f_run(yt_fused* F) {
1828
2048
  q += le - (ls + bindent);
1829
2049
  *q++ = '\n';
1830
2050
  }
1831
- s2 = (le < len && p[le] == '\r' && le + 1 < len) ? le + 2
1832
- : le + 1;
2051
+ s2 = (le < len && p[le] == '\r' && le + 1 < len &&
2052
+ p[le + 1] == '\n')
2053
+ ? le + 2
2054
+ : le + 1; /* lone CR is a 1-byte break */
1833
2055
  }
1834
2056
  }
1835
2057
  if ((size_t)(q - dst) != clen) {