yeptris 0.6.34.2 → 0.6.35.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.
@@ -130,11 +130,18 @@ struct yep_engine {
130
130
  yep_view handle, prefix;
131
131
  } tagmap[8];
132
132
  int tagmap_n;
133
- int saw_yaml; /* %YAML seen for the pending document */
134
- size_t doc_begin; /* #179: first directive line of the pending doc
135
- * (SIZE_MAX = none) — libyaml's DOCUMENT_START
136
- * start_mark includes directives */
137
- void* step; /* yep_stepstate: resumable stepping (07) */
133
+ int saw_yaml; /* %YAML seen for the pending document */
134
+ size_t doc_begin; /* #179: first directive line of the pending doc
135
+ * (SIZE_MAX = none) — libyaml's DOCUMENT_START
136
+ * start_mark includes directives. An offset into
137
+ * THIS run's buffer. */
138
+ uint32_t doc_begin_line; /* the same directive line as a stream-
139
+ * absolute LINE: offsets die at a step cut
140
+ * (the tail is memmoved), but a directive
141
+ * line split from its --- must still mark
142
+ * the DOCUMENT_START (#645's neighbor:
143
+ * whole 85:1 vs chunked 86:1) */
144
+ void* step; /* yep_stepstate: resumable stepping (07) */
138
145
 
139
146
  /* Flow single-pair deferral: a sequence entry's events are buffered
140
147
  * until we know whether ':' follows ("[a: b]" needs MAP_START before
@@ -1224,8 +1231,12 @@ static yep_view e_resolve_tag(yep_engine* e, yep_view tag) {
1224
1231
  return e_tag_uri_decode(e, e_resolve_tag_raw(e, tag));
1225
1232
  }
1226
1233
 
1227
- /* Parses &anchor / !tag runs at the cursor into *anchor / *tag. */
1234
+ /* Parses &anchor / !tag runs at the cursor into *anchor / *tag. A node
1235
+ * carries at most one of each (c-ns-properties): a second property of
1236
+ * the same kind is the suite's 4JVG error. Keeping the last one let a
1237
+ * mutated anchor run read differently from libyaml (catch 156). */
1228
1238
  static void e_props(yep_engine* e, yep_view* anchor, yep_view* tag) {
1239
+ int have_anchor = 0, have_tag = 0;
1229
1240
  for (;;) {
1230
1241
  e_skip_inline_space(e);
1231
1242
  if (e->pos >= e->len) {
@@ -1233,6 +1244,11 @@ static void e_props(yep_engine* e, yep_view* anchor, yep_view* tag) {
1233
1244
  }
1234
1245
  unsigned char c = (unsigned char)e->p[e->pos];
1235
1246
  if (c == '&') {
1247
+ if (have_anchor) {
1248
+ e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
1249
+ return;
1250
+ }
1251
+ have_anchor = 1;
1236
1252
  size_t start = e->pos + 1;
1237
1253
  e->pos = yep_scan_prop_end(e->p, e->len, start);
1238
1254
  anchor->p = e->p + start;
@@ -1257,6 +1273,11 @@ static void e_props(yep_engine* e, yep_view* anchor, yep_view* tag) {
1257
1273
  return;
1258
1274
  }
1259
1275
  } else if (c == '!') {
1276
+ if (have_tag) {
1277
+ e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
1278
+ return;
1279
+ }
1280
+ have_tag = 1;
1260
1281
  size_t start = e->pos;
1261
1282
  e->pos++;
1262
1283
  if (e->pos < e->len && e->p[e->pos] == '<') {
@@ -1478,12 +1499,18 @@ static int e_skip_flow(yep_engine* e) {
1478
1499
  }
1479
1500
  if (c == '[') {
1480
1501
  if (n >= e->max_depth) {
1481
- return e_fail(e, YEP_ERR_DEPTH, e->pos);
1502
+ /* NOT-A-KEY deferral (the unterminated-quote law
1503
+ * above): the pre-scan fires before the flow's open
1504
+ * events, while the chunked feed had already streamed
1505
+ * them, so the whole stream lost MAP_START/SEQ_START
1506
+ * (the nightly's run 39502647277). The real parse owns
1507
+ * the depth error after the events. */
1508
+ return 0;
1482
1509
  }
1483
1510
  stk[n++] = 0;
1484
1511
  } else if (c == '{') {
1485
1512
  if (n >= e->max_depth) {
1486
- return e_fail(e, YEP_ERR_DEPTH, e->pos);
1513
+ return 0; /* the same deferral */
1487
1514
  }
1488
1515
  stk[n++] = 1;
1489
1516
  } else if (c == ']') {
@@ -2098,6 +2125,8 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2098
2125
  uint8_t just_closed;
2099
2126
  uint8_t pair_open;
2100
2127
  uint8_t pair_value;
2128
+ uint8_t pair_q; /* the pair opened on an explicit '?': its
2129
+ * JSON-style ':' separator is still ahead */
2101
2130
  uint8_t pair_wrap;
2102
2131
  uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2103
2132
  uint8_t key_coll; /* map: the nested collection opened in key position */
@@ -2113,6 +2142,7 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2113
2142
  st[idx].key_coll = 0;
2114
2143
  st[idx].pair_open = 0;
2115
2144
  st[idx].pair_value = 0;
2145
+ st[idx].pair_q = 0;
2116
2146
  st[idx].pair_wrap = 0;
2117
2147
  st[idx].q_coll = 0;
2118
2148
  st[idx].buf_from = -1;
@@ -2128,6 +2158,8 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2128
2158
  uint8_t just_closed; /* a nested collection closed here (key candidate) */
2129
2159
  uint8_t pair_open; /* seq: a buffered single-pair awaits its MAP_END */
2130
2160
  uint8_t pair_value; /* seq: the open pair already has its value */
2161
+ uint8_t pair_q; /* seq: the pair opened on an explicit '?':
2162
+ * its JSON-style ':' separator is still ahead */
2131
2163
  uint8_t pair_wrap; /* map frame pushed as a seq single-pair */
2132
2164
  uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2133
2165
  uint8_t key_coll; /* map: a nested collection opened in key position */
@@ -2191,6 +2223,31 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2191
2223
  return e_fail(e, YEP_ERR_BAD_INDENT, e->pos + fl.indent);
2192
2224
  }
2193
2225
  }
2226
+ if (st[n - 1].pair_open && st[n - 1].pair_q && e->pos < e->len && e->p[e->pos] == ':') {
2227
+ /* "#671: the explicit key's entry separator may sit flush
2228
+ * against the value ("[? &a :b]") -- the JSON-style colon
2229
+ * the map frame's pending-key law consumes; an IMPLICIT
2230
+ * key's separator is already spent here, so its value's
2231
+ * leading ':' stays content ("[s: :v]") */
2232
+ size_t n2 = e->pos + 1;
2233
+ if (n2 >= e->len ||
2234
+ !(e->p[n2] == ' ' || e->p[n2] == '\t' || e->p[n2] == '\n' || e->p[n2] == '\r' ||
2235
+ yep_ct_is((unsigned char)e->p[n2], YEP_CT_FLOW_IND))) {
2236
+ st[n - 1].pair_q = 0;
2237
+ e->pos++;
2238
+ continue;
2239
+ }
2240
+ }
2241
+ if (st[n - 1].kind == 1 && st[n - 1].pending_key == 2 && e->pos < e->len &&
2242
+ e->p[e->pos] == ':') {
2243
+ /* an explicit '?'-key entry's separator may follow on a
2244
+ * LATER line (s-separate spans breaks in flow: "{?{a}\n:
2245
+ * b}" — the nightly's catch 161); the post-node colon law
2246
+ * never sees a colon at the node start */
2247
+ st[n - 1].pending_key = 1;
2248
+ e->pos++;
2249
+ continue;
2250
+ }
2194
2251
  if (e->pos < e->len && e->p[e->pos] != ',' && e->p[e->pos] != ']' && e->p[e->pos] != '}' &&
2195
2252
  !(e->p[e->pos] == ':' &&
2196
2253
  ((st[n - 1].kind == 1 && st[n - 1].pending_key == 1) || st[n - 1].just_closed)) &&
@@ -2389,6 +2446,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2389
2446
  return -2;
2390
2447
  }
2391
2448
  st[n - 1].pair_open = 1;
2449
+ st[n - 1].pair_q = (uint8_t)q_key;
2392
2450
  continue;
2393
2451
  } else {
2394
2452
  yep_event mk;
@@ -2420,6 +2478,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2420
2478
  return -2;
2421
2479
  }
2422
2480
  st[n - 1].pair_open = 1;
2481
+ st[n - 1].pair_q = (uint8_t)q_key;
2423
2482
  if (e->ev_scopes == 0 && e->ev_buf_n > 0) {
2424
2483
  int brc = e_buf_send(e, 0);
2425
2484
  if (brc != 0) {
@@ -2500,6 +2559,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2500
2559
  }
2501
2560
  st[n - 1].pair_open = 0;
2502
2561
  st[n - 1].pair_value = 0;
2562
+ st[n - 1].pair_q = 0;
2503
2563
  }
2504
2564
  if (st[n - 1].buf_from >= 0) {
2505
2565
  st[n - 1].buf_from = -1;
@@ -2739,6 +2799,12 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2739
2799
  }
2740
2800
  }
2741
2801
  st[n - 1].pair_open = 1;
2802
+ /* the wrapped key's own open marked pair_value=1 ("a
2803
+ * VALUE collection completes the pending pair"); as a
2804
+ * KEY it completes nothing — the pair awaits a fresh
2805
+ * value, else the boundary flush skips the null ("[[a]:
2806
+ * ]" lost it, the nightly's round 9) */
2807
+ st[n - 1].pair_value = 0;
2742
2808
  continue; /* value next */
2743
2809
  }
2744
2810
  if (st[n - 1].kind == 0 && n >= 1) {
@@ -3710,8 +3776,9 @@ static int e_parse_value(yep_engine* e, yep_ctx ctx, uint16_t floor_col) {
3710
3776
  * may align with the '?' column. Flow nodes and property runs
3711
3777
  * continue the value even at the parent column ("k:\n!!seq\n[a]"). */
3712
3778
  if (ctx == YEP_CTX_AFTER_Q && li.indent == floor_col && li.first == '-' &&
3713
- li.offset + 1 < li.end && e->p[li.offset + 1] != ' ' && e->p[li.offset + 1] != '\t' &&
3714
- e->p[li.offset + 1] != '\n' && e->p[li.offset + 1] != '\r') {
3779
+ li.offset + li.indent + 1 < li.end && e->p[li.offset + li.indent + 1] != ' ' &&
3780
+ e->p[li.offset + li.indent + 1] != '\t' && e->p[li.offset + li.indent + 1] != '\n' &&
3781
+ e->p[li.offset + li.indent + 1] != '\r') {
3715
3782
  /* "-:" and friends: a dash with a non-space follower is a
3716
3783
  * PLAIN key ("-" => null), never the pending key's content
3717
3784
  * ("?\n-:" lost the null pair, the nightly's run
@@ -3756,9 +3823,33 @@ static int e_parse_value(yep_engine* e, yep_ctx ctx, uint16_t floor_col) {
3756
3823
  goto empty_value;
3757
3824
  }
3758
3825
  }
3826
+ /* A flow collection at the parent column is normally the pending
3827
+ * value ("k:\n[a]"), but a flow followed by a same-line ':' is a
3828
+ * KEY of the parent map: the pending value is empty and the main
3829
+ * loop must own the line (crash-c0169e98: "a:\n{----}: 1" made
3830
+ * the flow a's value and then errored on its key colon). Probe
3831
+ * with the same pre-scan the block flow-key branch uses. */
3832
+ int flow_value = 0;
3833
+ if ((li.first == '[' || li.first == '{') && li.indent == floor_col) {
3834
+ flow_value = 1;
3835
+ size_t fsave = e->pos;
3836
+ uint32_t fline = e->line;
3837
+ size_t fls = e->line_start;
3838
+ e->pos = li.offset + li.indent;
3839
+ if (e_skip_flow(e) == 0) {
3840
+ e_skip_inline_space(e);
3841
+ if (e_colon_at(e, e->pos) && e->line == fline) {
3842
+ flow_value = 0;
3843
+ }
3844
+ } else {
3845
+ yep_error_clear(&e->err); /* e_node re-raises the real error */
3846
+ }
3847
+ e->pos = fsave;
3848
+ e->line = fline;
3849
+ e->line_start = fls;
3850
+ }
3759
3851
  if (li.indent > floor_col || e->depth == 0 ||
3760
- (ctx == YEP_CTX_AFTER_Q && li.indent == floor_col && li.first != ':') ||
3761
- ((li.first == '[' || li.first == '{') && li.indent == floor_col) ||
3852
+ (ctx == YEP_CTX_AFTER_Q && li.indent == floor_col && li.first != ':') || flow_value ||
3762
3853
  /* libyaml never validates a block-scalar HEADER's column —
3763
3854
  * only the content must out-indent the parent (e_block_scalar
3764
3855
  * enforces that), so "|" / ">" at the key's own column is the
@@ -3941,48 +4032,366 @@ const yep_stopset yep_norm_break_leads = {
3941
4032
  0x02, 0x00}},
3942
4033
  };
3943
4034
 
4035
+ /* The context walk behind the break normalizer (issue #638): the fold
4036
+ * is quote-aware — NEL (C2 85) folds EVERYWHERE (libyaml's READ_LINE
4037
+ * folds it inside quoted scalars too), LS/PS (E2 80 A8/A9) fold
4038
+ * everywhere EXCEPT inside single/double-quoted regions, where libyaml
4039
+ * keeps them raw as content (the JSON suite's y_string_u+2028_line_sep
4040
+ * / y_string_u+2029_par_sep demand it). The arms mirror step_scan's
4041
+ * context tracking minus the cut/hold machinery — complete input, no
4042
+ * truncation points — with two engine-faithful corrections: the value
4043
+ * position survives spaces (`k: 'x'` opens the quote; a block plain
4044
+ * continuation `a 'b'` does not), and the block-scalar floor is the
4045
+ * header LINE's indent (step_scan's walk-back only serves col-0
4046
+ * headers). Verified against libyaml by the NormLsPs pins and the
4047
+ * fuzz-diff LS/PS comparator. */
4048
+ static int step_bs_header(const char* p, size_t len, size_t i);
4049
+
4050
+ typedef struct yep_norm_ctx {
4051
+ int quote; /* 0 none, else the opening quote char */
4052
+ int in_comment;
4053
+ int in_verbatim;
4054
+ int in_flow;
4055
+ int tok_start;
4056
+ int word_led;
4057
+ int at_break;
4058
+ int bs_open;
4059
+ int bs_content;
4060
+ uint32_t bs_indent; /* the header line's indent; (uint32_t)-1 = the
4061
+ * doc-inline root rule (markers only) */
4062
+ } yep_norm_ctx;
4063
+
4064
+ typedef struct yep_norm_tok {
4065
+ size_t n; /* bytes consumed */
4066
+ int fold_off; /* -1 verbatim, else the offset inside the token
4067
+ * whose bytes become one '\n' */
4068
+ int fold_len;
4069
+ } yep_norm_tok;
4070
+
4071
+ static void norm_break_arms(yep_norm_ctx* c) {
4072
+ c->tok_start = 1;
4073
+ c->word_led = 0;
4074
+ c->at_break = 1;
4075
+ }
4076
+
4077
+ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t i) {
4078
+ yep_norm_tok t = {1, -1, 0};
4079
+ unsigned char ch = (unsigned char)p[i];
4080
+ int nel = (ch == 0xC2 && i + 1 < len && (unsigned char)p[i + 1] == 0x85);
4081
+ int lps = (ch == 0xE2 && i + 2 < len && (unsigned char)p[i + 1] == 0x80 &&
4082
+ ((unsigned char)p[i + 2] & 0xFE) == 0xA8);
4083
+
4084
+ if (c->in_comment) {
4085
+ /* the folds end a comment like any line break (step_scan's
4086
+ * law: the post-break '#' must read as a comment) */
4087
+ if (ch == '\n') {
4088
+ c->in_comment = 0;
4089
+ norm_break_arms(c);
4090
+ } else if (ch == '\r' && (i + 1 >= len || p[i + 1] != '\n')) {
4091
+ c->in_comment = 0;
4092
+ norm_break_arms(c);
4093
+ } else if (nel) {
4094
+ t.n = 2;
4095
+ t.fold_off = 0;
4096
+ t.fold_len = 2;
4097
+ c->in_comment = 0;
4098
+ norm_break_arms(c);
4099
+ } else if (lps) {
4100
+ t.n = 3;
4101
+ t.fold_off = 0;
4102
+ t.fold_len = 3;
4103
+ c->in_comment = 0;
4104
+ norm_break_arms(c);
4105
+ }
4106
+ return t;
4107
+ }
4108
+ if (c->quote != 0) {
4109
+ /* NEL folds inside quotes too; LS/PS stay raw content */
4110
+ if (nel) {
4111
+ t.n = 2;
4112
+ t.fold_off = 0;
4113
+ t.fold_len = 2;
4114
+ return t;
4115
+ }
4116
+ if (lps) {
4117
+ t.n = 3;
4118
+ return t;
4119
+ }
4120
+ if (c->quote == '\'') {
4121
+ if (ch == '\'') {
4122
+ if (i + 1 < len && p[i + 1] == '\'') {
4123
+ t.n = 2; /* '' escaped quote */
4124
+ return t;
4125
+ }
4126
+ c->quote = 0;
4127
+ }
4128
+ return t;
4129
+ }
4130
+ if (ch == '\\') {
4131
+ /* the escape consumes the next byte — but a multibyte
4132
+ * break under the escape still folds (an escaped break is
4133
+ * a fold continuation, and the blind pass folded it) */
4134
+ if (i + 1 >= len) {
4135
+ return t;
4136
+ }
4137
+ unsigned char nx = (unsigned char)p[i + 1];
4138
+ if (nx == 0xC2 && i + 2 < len && (unsigned char)p[i + 2] == 0x85) {
4139
+ t.n = 3;
4140
+ t.fold_off = 1;
4141
+ t.fold_len = 2;
4142
+ return t;
4143
+ }
4144
+ if (nx == 0xE2 && i + 3 < len && (unsigned char)p[i + 2] == 0x80 &&
4145
+ ((unsigned char)p[i + 3] & 0xFE) == 0xA8) {
4146
+ t.n = 4;
4147
+ t.fold_off = 1;
4148
+ t.fold_len = 3;
4149
+ return t;
4150
+ }
4151
+ t.n = 2;
4152
+ return t;
4153
+ }
4154
+ if (ch == '"') {
4155
+ c->quote = 0;
4156
+ }
4157
+ return t;
4158
+ }
4159
+ if (c->in_verbatim) {
4160
+ if (ch == '>') {
4161
+ c->in_verbatim = 0;
4162
+ }
4163
+ return t;
4164
+ }
4165
+ if (ch == '\n' || (ch == '\r' && (i + 1 >= len || p[i + 1] != '\n'))) {
4166
+ norm_break_arms(c);
4167
+ return t;
4168
+ }
4169
+ if (nel) {
4170
+ t.n = 2;
4171
+ t.fold_off = 0;
4172
+ t.fold_len = 2;
4173
+ norm_break_arms(c);
4174
+ return t;
4175
+ }
4176
+ if (lps) {
4177
+ t.n = 3;
4178
+ t.fold_off = 0;
4179
+ t.fold_len = 3;
4180
+ norm_break_arms(c);
4181
+ return t;
4182
+ }
4183
+ if (ch == '#' && (i == 0 || c->at_break || p[i - 1] == '\n' || p[i - 1] == '\r' ||
4184
+ p[i - 1] == ' ' || p[i - 1] == '\t')) {
4185
+ c->in_comment = 1;
4186
+ c->at_break = 0;
4187
+ return t;
4188
+ }
4189
+ if (ch == ' ' || ch == '\t') {
4190
+ /* a value position survives spaces in block context ("k: 'x'"
4191
+ * opens; the plain continuation "a 'b'" does not — its word
4192
+ * did not begin with an indicator) */
4193
+ c->tok_start = (c->in_flow > 0) ? 1 : (c->word_led || c->tok_start);
4194
+ return t;
4195
+ }
4196
+
4197
+ int line_start = (i == 0 || c->at_break || p[i - 1] == '\n' || p[i - 1] == '\r');
4198
+
4199
+ /* block scalars: inside the fold no quote opens and every break
4200
+ * keeps its fold; the region ends at a col-0 marker, a %-line
4201
+ * after content, or a dedent to the header line's indent */
4202
+ if (line_start && c->in_flow == 0 && c->bs_open) {
4203
+ size_t k = i;
4204
+ uint32_t ind = 0;
4205
+ while (k < len && p[k] == ' ') {
4206
+ ind++;
4207
+ k++;
4208
+ }
4209
+ if (k >= len) {
4210
+ t.n = len - i;
4211
+ c->at_break = 0;
4212
+ return t; /* a final line without its break */
4213
+ }
4214
+ if (p[k] == '\n' || p[k] == '\r' || p[k] == '#') {
4215
+ size_t e = k;
4216
+ while (e < len && p[e] != '\n') {
4217
+ e++;
4218
+ }
4219
+ t.n = (e < len) ? (e - i + 1) : (len - i);
4220
+ c->at_break = (e < len);
4221
+ return t; /* blank or comment-content line */
4222
+ }
4223
+ int marker = (ind == 0 && k + 2 < len &&
4224
+ ((p[k] == '-' && p[k + 1] == '-' && p[k + 2] == '-') ||
4225
+ (p[k] == '.' && p[k + 1] == '.' && p[k + 2] == '.')) &&
4226
+ (k + 3 >= len || p[k + 3] == ' ' || p[k + 3] == '\t' || p[k + 3] == '\n' ||
4227
+ p[k + 3] == '\r'));
4228
+ if (marker || (p[k] == '%' && ind == 0 && c->bs_content) ||
4229
+ (!marker && c->bs_indent != (uint32_t)-1 && ind <= (uint32_t)c->bs_indent)) {
4230
+ c->bs_open = 0; /* the fold ends: this line processes normally */
4231
+ } else {
4232
+ size_t e = k;
4233
+ while (e < len && p[e] != '\n') {
4234
+ e++;
4235
+ }
4236
+ if (p[k] != '#' && p[k] != '\n' && p[k] != '\r') {
4237
+ c->bs_content = 1;
4238
+ }
4239
+ t.n = (e < len) ? (e - i + 1) : (len - i);
4240
+ c->at_break = (e < len);
4241
+ return t;
4242
+ }
4243
+ }
4244
+
4245
+ /* the header: a '>' / '|' at a value position with a valid
4246
+ * follower opens the fold (the engine's own floor is the parent's
4247
+ * column, which the walk cannot know — the header LINE's indent
4248
+ * bounds it from above and the dedent law agrees wherever the
4249
+ * parse succeeds) */
4250
+ if (line_start && c->in_flow == 0 && !c->bs_open && c->tok_start && (ch == '>' || ch == '|') &&
4251
+ (i + 1 >= len || p[i + 1] == ' ' || p[i + 1] == '\t' || p[i + 1] == '\n' ||
4252
+ p[i + 1] == '\r' || (p[i + 1] >= '0' && p[i + 1] <= '9') || p[i + 1] == '+' ||
4253
+ p[i + 1] == '-')) {
4254
+ int hdr = step_bs_header(p, len, i);
4255
+ if (hdr > 0) {
4256
+ size_t ls = i;
4257
+ while (ls > 0 && p[ls - 1] != '\n' && p[ls - 1] != '\r') {
4258
+ ls--;
4259
+ }
4260
+ uint32_t ind = 0;
4261
+ size_t k = ls;
4262
+ while (k < len && p[k] == ' ') {
4263
+ ind++;
4264
+ k++;
4265
+ }
4266
+ c->bs_open = 1;
4267
+ c->bs_indent = ind;
4268
+ c->bs_content = 0;
4269
+ size_t e = i;
4270
+ while (e < len && p[e] != '\n') {
4271
+ e++;
4272
+ }
4273
+ t.n = (e < len) ? (e - i + 1) : (len - i);
4274
+ c->at_break = (e < len);
4275
+ return t;
4276
+ }
4277
+ /* not a header ("| : [" is a mapping key): fall through */
4278
+ }
4279
+ /* the doc-inline twin ("--- |"): the node hangs off the document,
4280
+ * so only a later marker ends its fold (col-0 lines are content) */
4281
+ if (line_start && c->in_flow == 0 && !c->bs_open && ch == '-' && i + 2 < len &&
4282
+ p[i + 1] == '-' && p[i + 2] == '-' &&
4283
+ (i + 3 >= len || p[i + 3] == ' ' || p[i + 3] == '\t' || p[i + 3] == '\n' ||
4284
+ p[i + 3] == '\r')) {
4285
+ size_t k = i + 3;
4286
+ while (k < len && (p[k] == ' ' || p[k] == '\t')) {
4287
+ k++;
4288
+ }
4289
+ if (k < len && (p[k] == '>' || p[k] == '|') &&
4290
+ (k + 1 >= len || p[k + 1] == ' ' || p[k + 1] == '\t' || p[k + 1] == '\n' ||
4291
+ p[k + 1] == '\r' || (p[k + 1] >= '0' && p[k + 1] <= '9') || p[k + 1] == '+' ||
4292
+ p[k + 1] == '-') &&
4293
+ step_bs_header(p, len, k) > 0) {
4294
+ c->bs_open = 1;
4295
+ c->bs_indent = (uint32_t)-1;
4296
+ c->bs_content = 0;
4297
+ size_t e = i;
4298
+ while (e < len && p[e] != '\n') {
4299
+ e++;
4300
+ }
4301
+ t.n = (e < len) ? (e - i + 1) : (len - i);
4302
+ c->at_break = (e < len);
4303
+ return t;
4304
+ }
4305
+ t.n = 3; /* the marker rides; the line's tail processes normally */
4306
+ return t;
4307
+ }
4308
+
4309
+ if (c->tok_start) {
4310
+ c->at_break = 0; /* any content byte ends the line-start state */
4311
+ if (ch == '!' && i + 1 < len && p[i + 1] == '<') {
4312
+ c->in_verbatim = 1;
4313
+ t.n = 2;
4314
+ return t;
4315
+ }
4316
+ c->word_led = (ch == '&' || ch == '*' || ch == '!' || ch == '-' || ch == '?' || ch == ':' ||
4317
+ ch == ',' || ch == '[' || ch == '{' || ch == ']' || ch == '}');
4318
+ if (ch == '\'' || ch == '"') {
4319
+ c->quote = ch; /* a quote at a value position IS a quoted token */
4320
+ c->tok_start = 0;
4321
+ return t;
4322
+ }
4323
+ c->tok_start = 0;
4324
+ }
4325
+ if (ch == ',' || ch == ':' || ch == '[' || ch == '{' || ch == ']' || ch == '}') {
4326
+ c->tok_start = 1; /* flow separators: the next byte may begin a
4327
+ * token with no space ("{\"a\":...") */
4328
+ }
4329
+ if (ch == '[' || ch == '{') {
4330
+ c->in_flow++;
4331
+ } else if (ch == ']' || ch == '}') {
4332
+ if (c->in_flow > 0) {
4333
+ c->in_flow--;
4334
+ }
4335
+ }
4336
+ return t;
4337
+ }
4338
+
3944
4339
  char* yep_engine_normalize_breaks(const yep_allocator* sys, const char* p, size_t len,
3945
4340
  size_t* out_len) {
3946
4341
  const unsigned char* q = (const unsigned char*)p;
3947
4342
  const yep_text_kernels* k = yep_text_active();
3948
4343
  size_t i = 0;
3949
- int found = 0;
4344
+ int exotic = 0;
3950
4345
  while (i < len) {
3951
4346
  ptrdiff_t hit = k->stopset_find(&yep_norm_break_leads, (const char*)q + i, len - i);
3952
4347
  if (hit < 0) {
3953
4348
  break;
3954
4349
  }
3955
4350
  i += (size_t)hit;
3956
- if (q[i] == 0xC2 && i + 1 < len && q[i + 1] == 0x85) {
3957
- found = 1;
3958
- break;
3959
- }
3960
- if (q[i] == 0xE2 && i + 2 < len && q[i + 1] == 0x80 &&
3961
- (q[i + 2] == 0xA8 || q[i + 2] == 0xA9)) {
4351
+ exotic = 1; /* any NEL/LS/PS lead byte needs the context walk */
4352
+ break;
4353
+ }
4354
+ if (!exotic) {
4355
+ return NULL;
4356
+ }
4357
+ yep_norm_ctx probe;
4358
+ memset(&probe, 0, sizeof(probe));
4359
+ probe.tok_start = 1;
4360
+ int found = 0;
4361
+ for (size_t j = 0; j < len;) {
4362
+ yep_norm_tok t = norm_step(&probe, p, len, j);
4363
+ if (t.fold_off >= 0) {
3962
4364
  found = 1;
3963
4365
  break;
3964
4366
  }
3965
- i++;
4367
+ j += t.n;
3966
4368
  }
3967
4369
  if (!found) {
3968
- return NULL;
4370
+ return NULL; /* the exotic bytes all ride inside quoted regions:
4371
+ * the zero-copy path keeps them raw (the fix) */
3969
4372
  }
3970
4373
  char* out = yep_alloc(sys, len + 1);
3971
4374
  if (out == NULL) {
3972
4375
  return NULL;
3973
4376
  }
4377
+ yep_norm_ctx cp;
4378
+ memset(&cp, 0, sizeof(cp));
4379
+ cp.tok_start = 1;
3974
4380
  size_t o = 0;
3975
4381
  for (size_t j = 0; j < len;) {
3976
- if (q[j] == 0xC2 && j + 1 < len && q[j + 1] == 0x85) {
3977
- out[o++] = '\n';
3978
- j += 2;
3979
- } else if (q[j] == 0xE2 && j + 2 < len && q[j + 1] == 0x80 &&
3980
- (q[j + 2] == 0xA8 || q[j + 2] == 0xA9)) {
3981
- out[o++] = '\n';
3982
- j += 3;
4382
+ yep_norm_tok t = norm_step(&cp, p, len, j);
4383
+ if (t.fold_off < 0) {
4384
+ memcpy(out + o, p + j, t.n);
4385
+ o += t.n;
3983
4386
  } else {
3984
- out[o++] = p[j++];
4387
+ memcpy(out + o, p + j, (size_t)t.fold_off);
4388
+ o += (size_t)t.fold_off;
4389
+ out[o++] = '\n';
4390
+ memcpy(out + o, p + j + t.fold_off + t.fold_len,
4391
+ t.n - (size_t)t.fold_off - (size_t)t.fold_len);
4392
+ o += t.n - (size_t)t.fold_off - (size_t)t.fold_len;
3985
4393
  }
4394
+ j += t.n;
3986
4395
  }
3987
4396
  out[o] = '\0';
3988
4397
  if (out_len != NULL) {
@@ -4101,6 +4510,48 @@ void yep_engine_destroy(yep_engine* e) {
4101
4510
  /* A quote opens a scalar only where a value can begin; anywhere else
4102
4511
  * it is literal plain text (the apostrophe in "don't"). This is exact
4103
4512
  * on valid YAML: a quote at a value position always opens. */
4513
+ /* A line-start '|' / '>' is a block-scalar header only when the rest
4514
+ * of the line fits the grammar: optional chomping (+/-) and indent
4515
+ * (1-9) indicators in either order, then spaces/tabs, then an optional
4516
+ * space-led comment, then EOL. "| : [" is a plain mapping key whose
4517
+ * value opens a flow — treating it as a header skipped the '[' and let
4518
+ * a '---' cut land inside the still-open flow (#645: whole buffered
4519
+ * that flow to the depth cap, chunked cut and errored at 2:1).
4520
+ * Returns 1 valid, 0 definitely not a header, -1 truncated (the tail
4521
+ * may still complete a valid header). */
4522
+ static int step_bs_header(const char* p, size_t len, size_t i) {
4523
+ size_t k = i + 1;
4524
+ int chomp = 0, indent = 0, space = 0;
4525
+ while (k < len) {
4526
+ char c = p[k];
4527
+ if (c == '+' || c == '-') {
4528
+ if (chomp || space)
4529
+ return 0;
4530
+ chomp = 1;
4531
+ k++;
4532
+ continue;
4533
+ }
4534
+ if (c >= '1' && c <= '9') {
4535
+ if (indent || space)
4536
+ return 0;
4537
+ indent = 1;
4538
+ k++;
4539
+ continue;
4540
+ }
4541
+ if (c == ' ' || c == '\t') {
4542
+ space = 1;
4543
+ k++;
4544
+ continue;
4545
+ }
4546
+ if (c == '\n' || c == '\r')
4547
+ return 1;
4548
+ if (c == '#' && space)
4549
+ return 1; /* a comment needs separation */
4550
+ return 0;
4551
+ }
4552
+ return -1; /* the chunk ended mid-line */
4553
+ }
4554
+
4104
4555
  /* Advances quote/flow/comment state over buf[scanned, len) and returns
4105
4556
  * the offset of the LAST safe cut (0 = none). When the final bytes'
4106
4557
  * meaning depends on bytes not yet fed (a quote that may be doubled,
@@ -4386,24 +4837,33 @@ static size_t step_scan(yep_stepstate* st) {
4386
4837
  (i + 1 >= st->len || p[i + 1] == ' ' || p[i + 1] == '\t' || p[i + 1] == '\n' ||
4387
4838
  p[i + 1] == '\r' || (p[i + 1] >= '0' && p[i + 1] <= '9') || p[i + 1] == '+' ||
4388
4839
  p[i + 1] == '-')) {
4389
- uint32_t ind = 0;
4390
- size_t k = i;
4391
- while (k > 0 && p[k - 1] == ' ') {
4392
- k--;
4393
- ind++;
4394
- }
4395
- st->bs_open = 1;
4396
- st->bs_indent = ind;
4397
- size_t e = i;
4398
- while (e < st->len && p[e] != '\n') {
4399
- e++;
4400
- }
4401
- if (e >= st->len) {
4402
- hold = i; /* an incomplete header line */
4840
+ int hdr = step_bs_header(p, st->len, i);
4841
+ if (hdr < 0) {
4842
+ hold = i; /* the header line may still complete */
4403
4843
  break;
4404
4844
  }
4405
- i = e;
4406
- continue;
4845
+ if (hdr > 0) {
4846
+ uint32_t ind = 0;
4847
+ size_t k = i;
4848
+ while (k > 0 && p[k - 1] == ' ') {
4849
+ k--;
4850
+ ind++;
4851
+ }
4852
+ st->bs_open = 1;
4853
+ st->bs_indent = ind;
4854
+ size_t e = i;
4855
+ while (e < st->len && p[e] != '\n') {
4856
+ e++;
4857
+ }
4858
+ if (e >= st->len) {
4859
+ hold = i; /* an incomplete header line */
4860
+ break;
4861
+ }
4862
+ i = e;
4863
+ continue;
4864
+ }
4865
+ /* Not a header ("| : [" is a mapping key): fall through
4866
+ * so the line's flow/quote bytes are counted. */
4407
4867
  }
4408
4868
  if (c == '-') {
4409
4869
  if (i + 3 >= st->len) {
@@ -4660,9 +5120,23 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
4660
5120
  p0++;
4661
5121
  }
4662
5122
  size_t p1 = p0;
4663
- while (p1 < li.end && e->p[p1] != ' ' && e->p[p1] != '\t' && e->p[p1] != '#') {
5123
+ while (p1 < li.end && e->p[p1] != ' ' && e->p[p1] != '\t') {
4664
5124
  p1++;
4665
5125
  }
5126
+ {
5127
+ /* a '#' inside the prefix token is NOT a comment:
5128
+ * libyaml wants whitespace before a comment and
5129
+ * rejects the glued form ("did not find expected
5130
+ * whitespace or line break while scanning a %TAG
5131
+ * directive"). Treating it as a comment registered
5132
+ * a bogus prefix and split the whole/chunked
5133
+ * verdicts (catch 154). */
5134
+ const char* hash = (const char*)memchr(e->p + p0, '#', p1 - p0);
5135
+ if (hash != NULL) {
5136
+ e_fail(e, YEP_ERR_BAD_DIRECTIVE, (size_t)(hash - e->p));
5137
+ return -1;
5138
+ }
5139
+ }
4666
5140
  {
4667
5141
  /* libyaml: only blanks and an optional comment may
4668
5142
  * follow the prefix ("did not find expected comment
@@ -4678,6 +5152,36 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
4678
5152
  return -1;
4679
5153
  }
4680
5154
  }
5155
+ {
5156
+ /* libyaml validates the handle: "!", "!!", or
5157
+ * "!word!" (word = alnum/-), and a prefix is
5158
+ * required ("did not find expected whitespace
5159
+ * while scanning a %TAG directive"). The lenient
5160
+ * accept registered "!'" and let the whole parse
5161
+ * succeed where the chunked one rejected
5162
+ * (catch 154). */
5163
+ int handle_ok = 0;
5164
+ size_t hl = h1 - h0;
5165
+ if (hl == 1 && e->p[h0] == '!') {
5166
+ handle_ok = 1;
5167
+ } else if (hl == 2 && e->p[h0] == '!' && e->p[h0 + 1] == '!') {
5168
+ handle_ok = 1;
5169
+ } else if (hl >= 3 && e->p[h0] == '!' && e->p[h1 - 1] == '!') {
5170
+ handle_ok = 1;
5171
+ for (size_t k = h0 + 1; k + 1 < h1; k++) {
5172
+ if (!(yep_ct_is((unsigned char)e->p[k], YEP_CT_ALPHA) ||
5173
+ yep_ct_is((unsigned char)e->p[k], YEP_CT_DIGIT) ||
5174
+ e->p[k] == '-')) {
5175
+ handle_ok = 0;
5176
+ break;
5177
+ }
5178
+ }
5179
+ }
5180
+ if (!handle_ok || p1 == p0) {
5181
+ e_fail(e, YEP_ERR_BAD_DIRECTIVE, h0);
5182
+ return -1;
5183
+ }
5184
+ }
4681
5185
  if (h1 > h0 && p1 > p0 && e->p[h0] == '!') {
4682
5186
  e->tagmap[e->tagmap_n].handle.p = e->p + h0;
4683
5187
  e->tagmap[e->tagmap_n].handle.len = (uint32_t)(h1 - h0);
@@ -4690,6 +5194,7 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
4690
5194
  e->doc_begin = li.offset; /* libyaml: DOCUMENT_START's
4691
5195
  * start_mark includes the
4692
5196
  * leading directive lines */
5197
+ e->doc_begin_line = e->line;
4693
5198
  }
4694
5199
  e_line_done(e, li.end);
4695
5200
  continue;
@@ -4734,10 +5239,19 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
4734
5239
  }
4735
5240
  e_event_init(&ev, YEP_EV_DOCUMENT_START);
4736
5241
  ev.style = 1; /* explicit marker */
4737
- e_pos_mark(e, e->doc_begin != (size_t)-1 ? e->doc_begin : li.offset + li.indent,
4738
- &ev.line, &ev.col);
5242
+ if (e->doc_begin != (size_t)-1) {
5243
+ e_pos_mark(e, e->doc_begin, &ev.line, &ev.col);
5244
+ } else if (e->doc_begin_line != 0) {
5245
+ /* the directive line was consumed by an EARLIER step
5246
+ * run: its offset is gone, the absolute line is not */
5247
+ ev.line = e->doc_begin_line;
5248
+ ev.col = 1;
5249
+ } else {
5250
+ e_pos_mark(e, li.offset + li.indent, &ev.line, &ev.col);
5251
+ }
4739
5252
  e_pos_mark(e, li.offset + li.indent + 3, &ev.end_line, &ev.end_col);
4740
5253
  e->doc_begin = (size_t)-1;
5254
+ e->doc_begin_line = 0;
4741
5255
  if (emit_now(e, &ev) != 0) {
4742
5256
  return -2;
4743
5257
  }
@@ -4816,6 +5330,7 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
4816
5330
  e->doc_content = 0;
4817
5331
  e->saw_yaml = 0;
4818
5332
  e->doc_begin = (size_t)-1;
5333
+ e->doc_begin_line = 0;
4819
5334
  }
4820
5335
  e_line_done(e, li.end);
4821
5336
  continue;
@@ -4825,12 +5340,16 @@ static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep
4825
5340
  e_event_init(&ev, YEP_EV_DOCUMENT_START);
4826
5341
  if (e->doc_begin != (size_t)-1) {
4827
5342
  e_pos_mark(e, e->doc_begin, &ev.line, &ev.col);
5343
+ } else if (e->doc_begin_line != 0) {
5344
+ ev.line = e->doc_begin_line; /* a directive split across runs */
5345
+ ev.col = 1;
4828
5346
  } else {
4829
5347
  e_pos_mark(e, li.offset + li.indent, &ev.line, &ev.col);
4830
5348
  }
4831
5349
  ev.end_line = ev.line;
4832
5350
  ev.end_col = ev.col;
4833
5351
  e->doc_begin = (size_t)-1;
5352
+ e->doc_begin_line = 0;
4834
5353
  if (emit_now(e, &ev) != 0) {
4835
5354
  return -2;
4836
5355
  }