yeptris 0.6.39.1-arm-linux-eabihf → 0.6.42.0-arm-linux-eabihf

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: b550d3511828de23b6a8c925382eff1d637a9c8391ab8952ef7fbfa3b2d57549
4
- data.tar.gz: bbe90991005ccb7718ecd3f465fe5563b937153c5ee39d02014baed5a52b725f
3
+ metadata.gz: 7abcdd568d0a95540be90ecbe988bb575aa4e97d8a7bda0292dc7b578ff7ca5f
4
+ data.tar.gz: 834db1e48082ef9b40cc0ceda0ffcdd17a4fc1bcb5324a6ce53f09be6cc9213a
5
5
  SHA512:
6
- metadata.gz: c9e5c875e40a7220c4a9183f3f000a39d169b1462e425dc37d4cc1389cc2496d52b9d53746ee4759e47d1e2cbc9eea0f4af5e3cd910e085d6d893d3082b971dd
7
- data.tar.gz: 331d2e087e1dc215eb0a5a3dc7e2f8a0e2ca9755ef49e201eed2c08a04c3931653850a26cfa7cf0af88ed99b02e14a14d6f6c9e4a018712d5ccdb234245d2f81
6
+ metadata.gz: '039d4d530fb04d72b3afe2f67ab249137d88d3e0c275141aeffd2dd8cc15a7f686c22e6416e11d7d7ade99a5ead7a9f465c7ebbad8a87a417af8c9222e5aff23'
7
+ data.tar.gz: 894301188585b5318d4d6896e0c2c4e8ff6a74e2f746361294e242dbcf52fadc7481b3f1fe6061bfafe92106b841ca155df35339102da048659e0e9d7345ef2b
@@ -1,47 +1,93 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ # Builds the native JSON/CBOR materializer (yeptris-ruby#311): the
4
+ # per-minor prebuilt bundle loads FIRST ("lib/yeptris/<minor>/native")
5
+ # and THIS build is the fallback slot ("require 'yeptris/native'") —
6
+ # compiled by the installing Ruby, so it is ABI-correct on every
7
+ # minor, present and future. Installs must NEVER fail: any miss (no
8
+ # engine binary, no headers, no usable toolchain) degrades to a no-op
9
+ # Makefile and the FFI ladder carries the load, exactly as today.
10
+
3
11
  require "mkmf"
12
+ require "fileutils"
13
+
14
+ gem_root = File.expand_path("../..", __dir__)
15
+ vendored = File.join(gem_root, "vendor", "libyeptris")
4
16
 
5
- # libyeptris location: YEPTRIS_LIB_PATH (file or dir), then sibling
6
- # checkouts. CI sets YEPTRIS_LIB_PATH (the built shared library) and
7
- # YEPTRIS_SRC (the C checkout) explicitly.
8
- lib_path = ENV["YEPTRIS_LIB_PATH"]
9
- candidates = []
10
- if lib_path
11
- candidates << File.dirname(lib_path)
12
- candidates << lib_path if File.directory?(lib_path)
17
+ def write_dummy_makefile(reason)
18
+ File.write("Makefile", <<~MAKE)
19
+ all:
20
+ \t@echo "#{reason}"
21
+ install:
22
+ clean:
23
+ MAKE
13
24
  end
14
- candidates << File.expand_path("../../../../yeptris/build-validate/src", __dir__)
15
- candidates << File.expand_path("../../../../yeptris/build/src", __dir__)
16
- candidates << File.expand_path("../../../yeptris/build-validate/src", __dir__)
17
- candidates << File.expand_path("../../../yeptris/build/src", __dir__)
18
25
 
19
- # Source root: YEPTRIS_SRC (CI / explicit), then sibling checkouts.
26
+ # 1. The engine binary to LINK. Order: CI/dev explicit, then the gem
27
+ # root — platform gems vendor the prebuilt lib there, and the pure
28
+ # gem's ext/libyeptris extconf builds it there BEFORE this extension
29
+ # runs (rubygems builds extensions in gemspec order).
30
+ lib = ENV["YEPTRIS_LIB_PATH"]
31
+ lib = nil unless lib && File.file?(lib)
32
+ lib ||= ["libyeptris.so", "libyeptris.dylib", "yeptris.dll"]
33
+ .map { |n| File.join(gem_root, n) }
34
+ .find { |f| File.exist?(f) }
35
+
36
+ # 2. The engine headers: CI/dev checkout, then the vendored tree that
37
+ # rides every gem artifact (the packaging doctrine's recompile half).
20
38
  src_roots = [ENV["YEPTRIS_SRC"]].compact
21
- src_roots << File.expand_path("../../../../yeptris/src", __dir__)
22
- src_roots << File.expand_path("../../../yeptris/src", __dir__)
39
+ src_roots << File.join(vendored, "src")
23
40
  src_root = src_roots.find { |d| d && File.directory?(File.join(d, "include")) }
24
- abort "yeptris sources not found (set YEPTRIS_SRC)" unless src_root
25
41
 
26
- $INCFLAGS << " -I#{src_root}/include -I#{src_root}/yeptris"
27
- %w[build-validate/generated build/generated].each do |g|
28
- d = File.join(File.dirname(src_root), g)
29
- $INCFLAGS << " -I#{d}" if File.directory?(d)
30
- end
42
+ # 0. The toolchain probe — the docstring's third miss (yeptris-ruby#317):
43
+ # a toolchain-free image (the alpine musl smoke has no compiler) must
44
+ # degrade to the no-op Makefile, not abort inside mkmf's sanity link.
45
+ if !have_devel?
46
+ write_dummy_makefile("yeptris: no usable toolchain for the materializer; the FFI ladder carries the load")
47
+ elsif lib.nil?
48
+ write_dummy_makefile("yeptris: no libyeptris binary for the materializer; the FFI ladder carries the load")
49
+ elsif src_root.nil?
50
+ write_dummy_makefile("yeptris: no engine headers for the materializer; the FFI ladder carries the load")
51
+ else
52
+ $INCFLAGS << " -I#{src_root}/include -I#{src_root}/yeptris"
53
+ %w[build-validate/generated build/generated].each do |g|
54
+ d = File.join(File.dirname(src_root), g)
55
+ $INCFLAGS << " -I#{d}" if File.directory?(d)
56
+ end
57
+ # the generated version.h: nothing in the materializer's include
58
+ # chain needs it today; generate it into the workdir so a future
59
+ # include cannot break installs (the vendored tree ships only .in)
60
+ vhin = File.join(src_root, "include", "yeptris", "version.h.in")
61
+ unless File.exist?(File.join(src_root, "include", "yeptris", "version.h"))
62
+ if File.exist?(vhin)
63
+ # CMakeLists sits at the engine tree's root: the vendored gem
64
+ # tree (vendor/libyeptris) or the CI checkout (libyeptris-src)
65
+ ver = File.read(File.join(File.dirname(src_root), "CMakeLists.txt"))[/project\s*\([^)]*?VERSION\s+([0-9.]+)/m, 1] || "0.0.0"
66
+ maj, min, pat = ver.split(".")
67
+ File.write("version.h", File.read(vhin)
68
+ .gsub("@PROJECT_VERSION_MAJOR@", maj || "0")
69
+ .gsub("@PROJECT_VERSION_MINOR@", min || "0")
70
+ .gsub("@PROJECT_VERSION_PATCH@", pat || "0")
71
+ .gsub("@PROJECT_VERSION@", ver))
72
+ $INCFLAGS << " -I."
73
+ end
74
+ end
75
+
76
+ libdir = File.dirname(lib)
77
+ $LIBPATH << libdir
78
+ # the bundle must find libyeptris at require time; the gem root is
79
+ # stable within an installation
80
+ $LDFLAGS << " -Wl,-rpath,#{libdir}" if RUBY_PLATFORM =~ /linux|darwin/
31
81
 
32
- libdir = candidates.find do |d|
33
- d && File.directory?(d) && Dir[File.join(d, "libyeptris*.{dylib,so,dll}")].any?
82
+ if have_library("yeptris", "yep_json_string") && have_library("yeptris", "yeptris_visit_json")
83
+ # Windows resolves DLL dependencies beside the loading module —
84
+ # install the dll beside the built bundle
85
+ if RUBY_PLATFORM =~ /mswin|mingw/ && File.dirname(lib) != gem_root
86
+ ($INSTALLFILES ||= []) << [lib, "$(RUBYARCHDIR)/yeptris"]
87
+ end
88
+ $CFLAGS << " -O3 -fvisibility=hidden" unless RUBY_PLATFORM =~ /mswin|mingw/
89
+ create_makefile("yeptris/native")
90
+ else
91
+ write_dummy_makefile("yeptris: materializer link probe failed; the FFI ladder carries the load")
92
+ end
34
93
  end
35
- abort "libyeptris not found (set YEPTRIS_LIB_PATH)" unless libdir
36
-
37
- $LIBPATH << libdir
38
- # Platform gems bake a RELATIVE rpath so the bundle finds the
39
- # vendored libyeptris next to itself; absolute build-time paths
40
- # would dangle on user machines.
41
- rpath = ENV["YEPTRIS_RPATH"] || libdir
42
- $LDFLAGS << " -Wl,-rpath,#{rpath}" if RUBY_PLATFORM =~ /linux|darwin/
43
- have_library("yeptris", "yep_json_string") or abort "yep_json_string not exported — rebuild libyeptris"
44
- have_library("yeptris", "yeptris_visit_json") or abort "yeptris_visit_json missing"
45
-
46
- $CFLAGS << " -O3 -fvisibility=hidden" unless RUBY_PLATFORM =~ /mswin|mingw/
47
- create_makefile("yeptris/native")
@@ -63,7 +63,18 @@ static int yep_cache_mode = YEP_CACHE_ON;
63
63
  * rb_ary_new() so 3-element arrays stay EMBEDDED in the RVALUE, and
64
64
  * bulk-path hashes get exact capacity from the counted pairs. */
65
65
  enum { YEP_SHAPE_PRE = 0, YEP_SHAPE_NATURAL = 1 };
66
- static int yep_shape_mode = YEP_SHAPE_PRE;
66
+ /* Default per arch (the #310 arm64 referee matrix, 24 cells over the
67
+ * json-users-shaped corpus): NATURAL — rb_ary_new() keeps 3-element
68
+ * arrays embedded in the RVALUE instead of forcing heap buffers, and
69
+ * it won EVERY cell (1.00-1.04x vs pre's 1.10-1.22x vs JSON.parse).
70
+ * x86 keeps PRE until its referee says otherwise (the GATE judges the
71
+ * per-arch default on CI). */
72
+ #if defined(__aarch64__) || defined(__arm__) || defined(__ARMEL__) || defined(_M_ARM64)
73
+ #define YEP_SHAPE_DEFAULT YEP_SHAPE_NATURAL
74
+ #else
75
+ #define YEP_SHAPE_DEFAULT YEP_SHAPE_PRE
76
+ #endif
77
+ static int yep_shape_mode = YEP_SHAPE_DEFAULT;
67
78
 
68
79
  static uint64_t jr_hash(const char* sp, long sl) {
69
80
  /* 8-byte-prefix key (the leptris nametab trick): one safe load
@@ -0,0 +1,29 @@
1
+ /* version.h — generated from the CMake project VERSION.
2
+ *
3
+ * CMakeLists.txt project(yeptris VERSION ...) is the single source of truth;
4
+ * scripts/bump-version.sh syncs vcpkg.json and CHANGELOG.md from it. Never
5
+ * hand-edit the generated file (it lives in the build tree).
6
+ */
7
+ #ifndef YEPTRIS_VERSION_H
8
+ #define YEPTRIS_VERSION_H
9
+
10
+ #include <yeptris/api.h>
11
+
12
+ #define YEPTRIS_VERSION_MAJOR 0
13
+ #define YEPTRIS_VERSION_MINOR 6
14
+ #define YEPTRIS_VERSION_PATCH 42
15
+ #define YEPTRIS_VERSION_STRING "0.6.42"
16
+
17
+ #ifdef __cplusplus
18
+ extern "C" {
19
+ #endif
20
+
21
+ /* Returns the library version string ("major.minor.patch").
22
+ * Memory: static storage; valid for the lifetime of the process. */
23
+ YEPTRIS_API const char* yeptris_version(void);
24
+
25
+ #ifdef __cplusplus
26
+ }
27
+ #endif
28
+
29
+ #endif /* YEPTRIS_VERSION_H */
Binary file
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.39.1".freeze
6
+ VERSION = "0.6.42.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
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.39
6
+ VERSION 0.6.42
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -55,6 +55,19 @@ YEPTRIS_API char* yeptris_serialize_ex(YeptrisDocument doc, const yeptris_emit_o
55
55
  * returned. Returns 0 on a NULL document. */
56
56
  YEPTRIS_API size_t yeptris_serialize_into(YeptrisDocument doc, char* buf, size_t cap);
57
57
 
58
+ /* Trusted-cap emission (#352): ONE write pass into the caller's
59
+ * fixed buffer — no sizing pass, no per-call allocation. Returns the
60
+ * byte count written when ret < cap (the document is complete and
61
+ * NUL-terminated at buf[ret]); when ret >= cap the output did not
62
+ * fit — buf holds a partial document and ret is the exact total the
63
+ * document needs (re-call with cap >= ret + 1; the retry writes
64
+ * identical bytes). opts == NULL selects the fidelity mode. Returns 0
65
+ * on a NULL document or buffer (use yeptris_serialize_into for the
66
+ * measure-only form). */
67
+ YEPTRIS_API size_t yeptris_serialize_into_trusted(YeptrisDocument doc,
68
+ const yeptris_emit_options* opts, char* buf,
69
+ size_t cap);
70
+
58
71
  /* Streaming emission (13C): bytes are delivered in chunks to sink as
59
72
  * the walk proceeds — memory bounded by the largest scalar, never the
60
73
  * document. Returns the total byte count, or 0 on a NULL argument or
@@ -44,6 +44,7 @@ YEPTRIS_API size_t yeptris_serialize_into_ex(YeptrisDocument handle,
44
44
  em.w.cap = 0;
45
45
  em.w.grow = 0;
46
46
  em.w.oom = 0;
47
+ em.w.bounded = 0;
47
48
  em.doc = (const yeptris_document*)handle;
48
49
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
49
50
  return 0;
@@ -97,6 +98,69 @@ YEPTRIS_API size_t yeptris_serialize_into_ex(YeptrisDocument handle,
97
98
  return wrote;
98
99
  }
99
100
 
101
+ YEPTRIS_API size_t yeptris_serialize_into_trusted(YeptrisDocument handle,
102
+ const yeptris_emit_options* opts, char* buf,
103
+ size_t cap) {
104
+ if (handle == NULL || buf == NULL || cap == 0) {
105
+ return 0;
106
+ }
107
+ yep_emitter em;
108
+ em.w.sc_dec = NULL;
109
+ em.w.sc_i = 0;
110
+ em.w.cap = 0;
111
+ em.w.grow = 0;
112
+ em.w.oom = 0;
113
+ em.w.bounded = 0;
114
+ em.doc = (const yeptris_document*)handle;
115
+ if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
116
+ return 0;
117
+ }
118
+ em.w.p = buf;
119
+ em.w.last = 0;
120
+ em.w.force_flow = 0;
121
+ em.w.in_flow = 0;
122
+ em.w.flow_item = 0;
123
+ em.w.unserializable = 0;
124
+ em.w.canonical = opts_canonical(opts);
125
+ em.w.explicit_doc_start = opts_doc_marker(opts);
126
+ em.w.json = 0;
127
+ em.w.json_compact = 0;
128
+ em.w.json_pretty = 0;
129
+ em.w.pretty_depth = 0;
130
+ em.w.best_width = opts_width(opts);
131
+ em.w.col = 0;
132
+ em.w.sv_input = ((const yeptris_document*)handle)->dom->input_base;
133
+ em.w.sv_arena = ((const yeptris_document*)handle)->dom->str;
134
+ em.w.sink = NULL;
135
+ em.w.watermark = 0;
136
+ em.w.sink_aborted = 0;
137
+ em.w.flushed = 0;
138
+ memset(&em.canon_names, 0, sizeof(em.canon_names));
139
+ if (em.w.canonical && !yep_nametab_init(&em.canon_names, yep_system_allocator())) {
140
+ return 0;
141
+ }
142
+ /* #352: ONE bounded wet pass — no memo, no dry run (the emit-split
143
+ * table's 2p/1p column priced the dry pass at ~0.55x). Routes derive
144
+ * on demand (the streaming route's bytes); an oversized write sets
145
+ * oom and the pass keeps counting, so the exact size falls out of
146
+ * the same walk and the retry writes identical bytes. */
147
+ em.w.cap = cap;
148
+ em.w.grow = 1;
149
+ em.w.bounded = 1;
150
+ size_t wrote = yep_emit_run(&em, 0);
151
+ em.w.bounded = 0;
152
+ int fit = !em.w.oom && !em.w.unserializable;
153
+ size_t ret = fit ? wrote : em.w.len;
154
+ if (em.canon_names.sys != NULL) {
155
+ yep_nametab_free(&em.canon_names);
156
+ }
157
+ if (!fit) {
158
+ return ret; /* ret >= cap: buf holds a partial document */
159
+ }
160
+ buf[wrote] = '\0';
161
+ return ret; /* ret < cap: the document is complete */
162
+ }
163
+
100
164
  YEPTRIS_API void yeptris_free(void* p) {
101
165
  free(p);
102
166
  }
@@ -112,6 +176,7 @@ YEPTRIS_API char* yeptris_serialize_ex(YeptrisDocument handle, const yeptris_emi
112
176
  em.w.cap = 0;
113
177
  em.w.grow = 0;
114
178
  em.w.oom = 0;
179
+ em.w.bounded = 0;
115
180
  em.doc = (const yeptris_document*)handle;
116
181
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
117
182
  return NULL;
@@ -199,6 +264,7 @@ YEPTRIS_API char* yeptris_serialize_json(YeptrisDocument handle, size_t* len) {
199
264
  em.w.cap = 0;
200
265
  em.w.grow = 0;
201
266
  em.w.oom = 0;
267
+ em.w.bounded = 0;
202
268
  em.doc = (const yeptris_document*)handle;
203
269
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
204
270
  return NULL;
@@ -227,8 +293,15 @@ YEPTRIS_API char* yeptris_serialize_json(YeptrisDocument handle, size_t* len) {
227
293
  if (em.w.canonical && !yep_nametab_init(&em.canon_names, yep_system_allocator())) {
228
294
  return NULL;
229
295
  }
230
- size_t need = yep_emit_run(&em, 1);
231
- char* out = malloc(need + 1);
296
+ /* #739: ONE wet pass into a growable buffer — the dry exact-sizing
297
+ * pass priced ~2x on the json lane (the #352 slice-2 pattern);
298
+ * the estimate starts at the input size (+64) and doubles on
299
+ * demand, shrink-to-fit at the end. */
300
+ size_t est = em.doc->dom->input_len + 64;
301
+ if (est < 256) {
302
+ est = 256;
303
+ }
304
+ char* out = (char*)malloc(est);
232
305
  if (out == NULL) {
233
306
  if (em.canon_names.sys != NULL) {
234
307
  yep_nametab_free(&em.canon_names);
@@ -236,7 +309,24 @@ YEPTRIS_API char* yeptris_serialize_json(YeptrisDocument handle, size_t* len) {
236
309
  return NULL;
237
310
  }
238
311
  em.w.p = out;
312
+ em.w.cap = est;
313
+ em.w.grow = 1;
239
314
  size_t wrote = yep_emit_run(&em, 0);
315
+ em.w.grow = 0;
316
+ out = em.w.p; /* wr_grow reallocs through w->p — the local is stale */
317
+ if (em.w.oom || em.w.unserializable) {
318
+ free(out);
319
+ if (em.canon_names.sys != NULL) {
320
+ yep_nametab_free(&em.canon_names);
321
+ }
322
+ return NULL;
323
+ }
324
+ if (em.w.cap - (wrote + 1) > em.w.cap / 8) {
325
+ char* tight = (char*)realloc(out, wrote + 1);
326
+ if (tight != NULL) {
327
+ out = tight;
328
+ }
329
+ }
240
330
  if (em.canon_names.sys != NULL) {
241
331
  yep_nametab_free(&em.canon_names);
242
332
  }
@@ -260,6 +350,7 @@ YEPTRIS_API char* yeptris_serialize_json_ex(YeptrisDocument handle, size_t* len,
260
350
  em.w.cap = 0;
261
351
  em.w.grow = 0;
262
352
  em.w.oom = 0;
353
+ em.w.bounded = 0;
263
354
  em.doc = (const yeptris_document*)handle;
264
355
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
265
356
  return NULL;
@@ -326,6 +417,7 @@ char* yep_serialize_json_compact(const yeptris_document* doc, size_t* len) {
326
417
  em.w.cap = 0;
327
418
  em.w.grow = 0;
328
419
  em.w.oom = 0;
420
+ em.w.bounded = 0;
329
421
  em.doc = doc;
330
422
  em.w.p = NULL;
331
423
  em.w.last = 0;
@@ -389,6 +481,7 @@ char* yep_serialize_json_pretty_ex(const yeptris_document* doc, size_t* len, con
389
481
  em.w.cap = 0;
390
482
  em.w.grow = 0;
391
483
  em.w.oom = 0;
484
+ em.w.bounded = 0;
392
485
  em.doc = doc;
393
486
  em.w.p = NULL;
394
487
  em.w.last = 0;
@@ -459,6 +552,7 @@ YEPTRIS_API size_t yeptris_serialize_stream(YeptrisDocument handle,
459
552
  em.w.cap = 0;
460
553
  em.w.grow = 0;
461
554
  em.w.oom = 0;
555
+ em.w.bounded = 0;
462
556
  em.doc = (const yeptris_document*)handle;
463
557
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
464
558
  return 0;
@@ -99,7 +99,11 @@ static void wr_put(yep_writer* w, const char* p, uint32_t n) {
99
99
  * first byte of the next heap block's header (the #182 flake:
100
100
  * corrupt deterministic, detected later, timing-varying) */
101
101
  if (w->grow && w->len + n + 1 > w->cap) {
102
- wr_grow(w, n);
102
+ if (w->bounded) {
103
+ w->oom = 1; /* keep counting: the trusted caller reads the size */
104
+ } else {
105
+ wr_grow(w, n);
106
+ }
103
107
  }
104
108
  if (w->oom) {
105
109
  w->len += n; /* keep counting: the caller fails on oom */
@@ -122,7 +126,11 @@ static void wr_byte(yep_writer* w, char c) {
122
126
  return;
123
127
  }
124
128
  if (w->grow && w->len + 2 > w->cap) { /* +1 write, +1 terminator */
125
- wr_grow(w, 1);
129
+ if (w->bounded) {
130
+ w->oom = 1; /* keep counting: the trusted caller reads the size */
131
+ } else {
132
+ wr_grow(w, 1);
133
+ }
126
134
  }
127
135
  if (w->oom) {
128
136
  w->len++;
@@ -346,37 +354,13 @@ static void emit_literal(yep_writer* w, const yep_dnode* n, int parent_col) {
346
354
  wr_indent(w, pad);
347
355
  }
348
356
  }
349
- /* The reader swallows the own break of a WHITESPACE-ONLY
350
- * more-indented last line ("|+\n:\n \t\n\n\n" reads trail=2), so
351
- * the spelling needs one extra break there -- written N reads N-1
352
- * (the nightly's round 32, crash-90c98918: pass1
353
- * "|+\n :\n \t\n\n" re-parsed one break short and the second
354
- * pass emitted clip). A regular last line reads 1:1. */
355
- uint32_t ls = body;
356
- while (ls > 0 && p[ls - 1] != '\n') {
357
- ls--;
358
- }
359
- int tab_last = 0;
360
- int ws_last = 1;
361
- for (uint32_t i = ls; i < body; i++) {
362
- if (p[i] == '\t') {
363
- tab_last = 1;
364
- } else if (p[i] != ' ') {
365
- ws_last = 0;
366
- break;
367
- }
368
- }
369
- /* the reader swallows the own break of a whitespace-only last line
370
- * that carries a TAB (the tab-indent law shifts the line's content
371
- * start): "|+\n:\n \t\n\n\n" reads trail=2 where a spaces-only
372
- * line reads 1:1 (the nightly's round 32, crash-90c98918: pass1
373
- * "|+\n :\n \t\n\n" re-parsed one break short and the second
374
- * pass emitted clip). One extra break spells the tab case — and
375
- * ONLY that case: a CONTENT last line that merely carries a tab
376
- * ("|+\n \t:\n..." — the round-33 composite, crash-306b67eb)
377
- * reads its own break 1:1, and the extra break made the value
378
- * GROW one break per pass. */
379
- for (uint32_t i = 0; i < trail + ((tab_last && ws_last) ? 1 : 0); i++) {
357
+ /* The keep spelling writes exactly the parsed trailing breaks:
358
+ * written N reads N. (The round-32 extra break for tab-carrying
359
+ * ws-only last lines compensated a READER bug -- the tab-content
360
+ * arm consumed its line's own break without counting it toward
361
+ * trailing -- and the reader counts it now; the compensation made
362
+ * every pass GROW one break.) */
363
+ for (uint32_t i = 0; i < trail; i++) {
380
364
  wr_byte(w, '\n');
381
365
  }
382
366
  }
@@ -688,22 +672,55 @@ static void wr_anchor_check(yep_writer* w, const yep_view* name) {
688
672
  }
689
673
  }
690
674
 
675
+ /* The anchor-name spell laws, shared by every anchor write path (the
676
+ * node lead AND the line-tail props: the round-45 bonus catch -- a
677
+ * PS-named anchor on a SEQ spelled raw through emit_props_tail and
678
+ * the re-parse died on the normalized break). Returns nonzero when
679
+ * the serialize must fail. */
680
+ static int anchor_name_unspellable(yep_writer* w, const yep_dnode* n) {
681
+ yep_view chk = wv(w, n->anchor);
682
+ if (chk.len == 0) {
683
+ return 0;
684
+ }
685
+ if (chk.p[chk.len - 1] == ':') {
686
+ /* a SCALAR keeps riding raw only when it spells INLINE
687
+ * ("k: &n: v" is byte-stable); a BLOCK-style spell puts the
688
+ * header where the separator should be ("&: |" -- the
689
+ * nightly's round 44, crash-07ad9ecc) -- there is no spell:
690
+ * fail the serialize */
691
+ if (n->kind != 0 || n->style == 4 || n->style == 5) {
692
+ w->unserializable = 1;
693
+ return 1;
694
+ }
695
+ }
696
+ /* a NORMALIZE-SUBJECT byte (LS/PS raw, NEL, or a hard break)
697
+ * re-reads as a folded break: pass2 sees a broken/empty name and
698
+ * dies. YAML has no quoted anchor names: fail the serialize. */
699
+ for (size_t ci = 0; ci < chk.len; ci++) {
700
+ unsigned char cb = (unsigned char)chk.p[ci];
701
+ if (cb == '\n' || cb == '\r') {
702
+ w->unserializable = 1;
703
+ return 1;
704
+ }
705
+ if (cb == 0xC2 && ci + 1 < chk.len && (unsigned char)chk.p[ci + 1] == 0x85) {
706
+ w->unserializable = 1; /* NEL */
707
+ return 1;
708
+ }
709
+ if (cb == 0xE2 && ci + 2 < chk.len && (unsigned char)chk.p[ci + 1] == 0x80 &&
710
+ ((unsigned char)chk.p[ci + 2] & 0xFE) == 0xA8) {
711
+ w->unserializable = 1; /* LS/PS */
712
+ return 1;
713
+ }
714
+ }
715
+ return 0;
716
+ }
717
+
691
718
  static void emit_anchor_ex(yep_emitter* em, const yep_dnode* n) {
692
719
  yep_writer* w = &em->w;
693
720
  if (n->anchor.len == 0) {
694
721
  return;
695
722
  }
696
- /* A name ending in ':' rides or separates context-dependently
697
- * (2SXE): spelled directly before a collection ("&: [[]]", the
698
- * nightly's round 31, crash-66a513ea; "k: &: [[1]]" fails its own
699
- * re-parse the same way) every spelling re-lexes differently --
700
- * the riding/yield law reads the colon as the key indicator at
701
- * the follower, so the tree changes or the parse dies. A scalar
702
- * node keeps riding raw ("k: &n: v" round-trips byte-stably).
703
- * There is no spell for the collection form: fail the serialize. */
704
- yep_view chk = wv(w, n->anchor);
705
- if (n->kind != 0 && chk.len > 0 && chk.p[chk.len - 1] == ':') {
706
- w->unserializable = 1;
723
+ if (anchor_name_unspellable(w, n)) {
707
724
  return;
708
725
  }
709
726
  wr_byte(w, '&');
@@ -875,6 +892,9 @@ static void emit_props_tail(yep_writer* w, const yep_dnode* n) {
875
892
  wr_tag_token(w, &tdec2);
876
893
  }
877
894
  if (n->anchor.len > 0) {
895
+ if (anchor_name_unspellable(w, n)) {
896
+ return;
897
+ }
878
898
  wr_byte(w, ' ');
879
899
  yep_view adec2 = wv(w, n->anchor);
880
900
  wr_anchor_check(w, &adec2);
@@ -892,6 +912,9 @@ static void emit_node(yep_emitter* em, uint32_t id, int parent_col, int as_key);
892
912
  * written). Conservative factor-4 estimate for every expandable
893
913
  * byte — overestimating only takes the (always-legal) explicit form. */
894
914
  static int key_written_over_limit(yep_writer* w, const yep_dnode* kn) {
915
+ if (w->json) {
916
+ return 0; /* JSON has no simple-key law: every key writes (#739) */
917
+ }
895
918
  uint64_t est = 2; /* the quotes */
896
919
  yep_view kv_ = wv(w, kn->value);
897
920
  const unsigned char* q = (const unsigned char*)kv_.p;
@@ -1069,7 +1092,11 @@ static void emit_block_map(yep_emitter* em, uint32_t id, int content_col) {
1069
1092
  w->len += total;
1070
1093
  } else {
1071
1094
  if (w->grow && w->len + total + 1 > w->cap) {
1072
- wr_grow(w, total);
1095
+ if (w->bounded) {
1096
+ w->oom = 1; /* keep counting: the trusted caller reads the size */
1097
+ } else {
1098
+ wr_grow(w, total);
1099
+ }
1073
1100
  }
1074
1101
  if (w->oom) {
1075
1102
  w->len += total; /* keep counting: the caller fails on oom */
@@ -57,6 +57,9 @@ typedef struct yep_writer {
57
57
  size_t cap;
58
58
  int grow;
59
59
  int oom;
60
+ int bounded; /* #352: caller-owned fixed buffer — an oversized write
61
+ sets oom and keeps counting (the needed size falls
62
+ out of the same pass); wr_grow is never called */
60
63
  int in_flow; /* scalars inside flow collections: plain must drop
61
64
  ',', '[', ']', '{', '}' — flow terminators */
62
65
  int flow_item; /* the NEXT scalar is a flow-SEQ item: a null must
@@ -918,7 +918,27 @@ static int e_block_scalar(yep_engine* e, yep_event* ev, int parent_col) {
918
918
  saw_content = 1;
919
919
  pending_breaks = 0;
920
920
  trailing_breaks = 0;
921
- e_line_done(e, li.end);
921
+ /* the line's OWN break counts toward trailing (the
922
+ * content-line arm and the spaces-past-indent arm
923
+ * scan it below): consuming it silently made keep
924
+ * spell one break short whenever a tab-carrying
925
+ * content line ENDED the block ("|+\n\n >\t\n
926
+ * \t \n" — libyaml keeps the final break, the
927
+ * nightly's round 37) */
928
+ {
929
+ size_t at = li.end;
930
+ size_t br = yep_scan_break_len(e->p, e->len, at);
931
+ if (br > 0) {
932
+ trailing_breaks = 1;
933
+ e->pos = at + br;
934
+ e->line++;
935
+ e->line_start = e->pos;
936
+ e->li_cache_line = 0;
937
+ e->shape_cache_line = 0;
938
+ } else {
939
+ e->pos = at;
940
+ }
941
+ }
922
942
  continue;
923
943
  }
924
944
  }
@@ -1187,10 +1207,18 @@ static yep_view e_resolve_tag_raw(yep_engine* e, yep_view tag) {
1187
1207
  if (tag.len == 1 && tag.p[0] == '!') {
1188
1208
  return tag;
1189
1209
  }
1190
- /* longest matching handle wins */
1210
+ /* longest matching handle wins; a redefined PRIMARY ('%TAG ! ...')
1211
+ * cannot capture a '!!'-led tag — the secondary handle resolves
1212
+ * through its own entry or the fixed tag:yaml.org,2002: default
1213
+ * (spec 6.8.2.2; libyaml: "%TAG ! !p/" + "!!i" is
1214
+ * tag:yaml.org,2002:i, the nightly's round 52, crash-763bb852) */
1215
+ int secondary = tag.len >= 2 && tag.p[0] == '!' && tag.p[1] == '!';
1191
1216
  int best = -1;
1192
1217
  uint32_t best_len = 0;
1193
1218
  for (int i = 0; i < e->tagmap_n; i++) {
1219
+ if (secondary && e->tagmap[i].handle.len == 1) {
1220
+ continue;
1221
+ }
1194
1222
  if (tag.len >= e->tagmap[i].handle.len && e->tagmap[i].handle.len > best_len &&
1195
1223
  memcmp(tag.p, e->tagmap[i].handle.p, e->tagmap[i].handle.len) == 0) {
1196
1224
  best = i;
@@ -2186,8 +2214,11 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2186
2214
  uint8_t pair_q; /* the pair opened on an explicit '?': its
2187
2215
  * JSON-style ':' separator is still ahead */
2188
2216
  uint8_t pair_wrap;
2189
- uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2190
- uint8_t key_coll; /* map: the nested collection opened in key position */
2217
+ uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2218
+ uint8_t q_empty_value; /* seq: the '?' empty-key pair — 1: opened,
2219
+ * 2: its value state (the entry comma was
2220
+ * consumed inside the pair, yeptris#741) */
2221
+ uint8_t key_coll; /* map: the nested collection opened in key position */
2191
2222
  int32_t buf_from;
2192
2223
  uint32_t entry_line;
2193
2224
  uint32_t entries;
@@ -2203,6 +2234,7 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2203
2234
  st[idx].pair_q = 0;
2204
2235
  st[idx].pair_wrap = 0;
2205
2236
  st[idx].q_coll = 0;
2237
+ st[idx].q_empty_value = 0;
2206
2238
  st[idx].buf_from = -1;
2207
2239
  st[idx].entry_line = 0;
2208
2240
  st[idx].entries = 0;
@@ -2210,20 +2242,21 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2210
2242
 
2211
2243
  static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_id) {
2212
2244
  struct {
2213
- uint8_t kind; /* 0 seq, 1 map */
2214
- uint8_t pending_key; /* map: 1 key seen awaiting ':' value; 2 key emitted w/o ':' */
2215
- uint8_t sep; /* a ',' was consumed and no entry followed yet */
2216
- uint8_t just_closed; /* a nested collection closed here (key candidate) */
2217
- uint8_t pair_open; /* seq: a buffered single-pair awaits its MAP_END */
2218
- uint8_t pair_value; /* seq: the open pair already has its value */
2219
- uint8_t pair_q; /* seq: the pair opened on an explicit '?':
2220
- * its JSON-style ':' separator is still ahead */
2221
- uint8_t pair_wrap; /* map frame pushed as a seq single-pair */
2222
- uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2223
- uint8_t key_coll; /* map: a nested collection opened in key position */
2224
- int32_t buf_from; /* seq: entry events buffered from this index */
2225
- uint32_t entry_line; /* seq: line where the buffered entry ended */
2226
- uint32_t entries; /* completed entries (for trailing-comma detection) */
2245
+ uint8_t kind; /* 0 seq, 1 map */
2246
+ uint8_t pending_key; /* map: 1 key seen awaiting ':' value; 2 key emitted w/o ':' */
2247
+ uint8_t sep; /* a ',' was consumed and no entry followed yet */
2248
+ uint8_t just_closed; /* a nested collection closed here (key candidate) */
2249
+ uint8_t pair_open; /* seq: a buffered single-pair awaits its MAP_END */
2250
+ uint8_t pair_value; /* seq: the open pair already has its value */
2251
+ uint8_t pair_q; /* seq: the pair opened on an explicit '?':
2252
+ * its JSON-style ':' separator is still ahead */
2253
+ uint8_t pair_wrap; /* map frame pushed as a seq single-pair */
2254
+ uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2255
+ uint8_t q_empty_value; /* seq: the '?' empty-key pair's state (#741) */
2256
+ uint8_t key_coll; /* map: a nested collection opened in key position */
2257
+ int32_t buf_from; /* seq: entry events buffered from this index */
2258
+ uint32_t entry_line; /* seq: line where the buffered entry ended */
2259
+ uint32_t entries; /* completed entries (for trailing-comma detection) */
2227
2260
  } st[YEP_MAX_DEPTH];
2228
2261
  int n = 0;
2229
2262
 
@@ -2263,6 +2296,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2263
2296
  }
2264
2297
  int keyish = (st[n - 1].kind == 1 && st[n - 1].pending_key == 0);
2265
2298
  int q_key = 0;
2299
+ int q_empty_key = 0; /* the '?' synthesized a null key (#741) */
2266
2300
  int rc;
2267
2301
  if (e_flow_ws(e) != 0) {
2268
2302
  return -1;
@@ -2292,6 +2326,10 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2292
2326
  !(e->p[n2] == ' ' || e->p[n2] == '\t' || e->p[n2] == '\n' || e->p[n2] == '\r' ||
2293
2327
  yep_ct_is((unsigned char)e->p[n2], YEP_CT_FLOW_IND))) {
2294
2328
  st[n - 1].pair_q = 0;
2329
+ /* yeptris#741: the consumed ':' is the pair's VALUE
2330
+ * indicator — the '?' empty-key pair's value state ends
2331
+ * here or the value node trips the entry guard */
2332
+ st[n - 1].q_empty_value = 0;
2295
2333
  e->pos++;
2296
2334
  continue;
2297
2335
  }
@@ -2319,6 +2357,15 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2319
2357
  * in a sequence a ':'-led token is a new plain, so it errors */
2320
2358
  return e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
2321
2359
  }
2360
+ if (st[n - 1].kind == 0 && st[n - 1].q_empty_value == 2 && e->pos < e->len &&
2361
+ e->p[e->pos] != ',' && e->p[e->pos] != ']' && e->p[e->pos] != '}' &&
2362
+ e->p[e->pos] != ':') {
2363
+ /* yeptris#741: a node in the '?' empty-key pair's value
2364
+ * state — ENTRY_MAPPING_VALUE wants VALUE/ENTRY/END only
2365
+ * ("[?, a]" is the libyaml "did not find expected ',' or
2366
+ * ']'" error) */
2367
+ return e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
2368
+ }
2322
2369
  if (e->pos < e->len && e->p[e->pos] == '?' &&
2323
2370
  (e->pos + 1 >= e->len || e->p[e->pos + 1] == ' ' || e->p[e->pos + 1] == '\t' ||
2324
2371
  e->p[e->pos + 1] == '\n' || e->p[e->pos + 1] == '\r' ||
@@ -2342,6 +2389,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2342
2389
  ev.col = e_col(e, e->pos) + 1;
2343
2390
  rc = 1;
2344
2391
  q_key = 1;
2392
+ q_empty_key = (st[n - 1].kind == 0);
2345
2393
  } else {
2346
2394
  rc = e_flow_node(e, &ev, keyish);
2347
2395
  q_key = 1;
@@ -2368,10 +2416,15 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2368
2416
  st[n - 1].q_coll = 1;
2369
2417
  }
2370
2418
  }
2371
- if (!q_key) {
2419
+ if (!q_key && st[n - 1].pair_open) {
2372
2420
  /* a VALUE collection completes the pending pair; a KEY
2373
2421
  * collection must not (its pair completes at ':' or the
2374
- * boundary with a null value) */
2422
+ * boundary with a null value). The gate is pair_open:
2423
+ * in a SEQUENCE a plain collection entry has no pair,
2424
+ * and the unguarded write planted a stale pair_value=1
2425
+ * that a LATER '?'-opened pair wrap inherited -- its
2426
+ * comma flush then skipped the implicit null
2427
+ * (crash-8106600e, "[... , ? 20:50 , ...]"). */
2375
2428
  st[n - 1].pair_value = 1;
2376
2429
  }
2377
2430
  unsigned char oc = (unsigned char)e->p[e->pos];
@@ -2550,6 +2603,9 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2550
2603
  if (emit_now(e, &ev) != 0) {
2551
2604
  return -2;
2552
2605
  }
2606
+ if (q_empty_key) {
2607
+ st[n - 1].q_empty_value = 1;
2608
+ }
2553
2609
  continue;
2554
2610
  }
2555
2611
  }
@@ -2584,6 +2640,18 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2584
2640
  unsigned char c = (unsigned char)e->p[e->pos];
2585
2641
  if (c == ',') {
2586
2642
  e->pos++;
2643
+ if (st[n - 1].kind == 0 && st[n - 1].pair_open && st[n - 1].pair_q &&
2644
+ !st[n - 1].pair_value && st[n - 1].q_empty_value == 1) {
2645
+ /* yeptris#741: the '?' empty-key pair's first comma is
2646
+ * the entry separator libyaml's ENTRY_MAPPING_KEY
2647
+ * consumed to reach its VALUE state — the pair stays
2648
+ * open and awaits ':' (value), ',' (empty value +
2649
+ * close), or ']' (empty value + close). Closing here
2650
+ * spilled the following ': x' into a second entry
2651
+ * ("[? ,: x , a]" read as two maps). */
2652
+ st[n - 1].q_empty_value = 2;
2653
+ continue;
2654
+ }
2587
2655
  if (st[n - 1].q_coll && st[n - 1].buf_from >= 0) {
2588
2656
  int32_t bf0 = st[n - 1].buf_from;
2589
2657
  st[n - 1].buf_from = -1;
@@ -2618,6 +2686,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2618
2686
  st[n - 1].pair_open = 0;
2619
2687
  st[n - 1].pair_value = 0;
2620
2688
  st[n - 1].pair_q = 0;
2689
+ st[n - 1].q_empty_value = 0;
2621
2690
  }
2622
2691
  if (st[n - 1].buf_from >= 0) {
2623
2692
  st[n - 1].buf_from = -1;
@@ -2751,6 +2820,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2751
2820
  }
2752
2821
  st[n - 1].pair_open = 0;
2753
2822
  st[n - 1].pair_value = 0;
2823
+ st[n - 1].q_empty_value = 0;
2754
2824
  }
2755
2825
  if (st[n - 1].buf_from >= 0) {
2756
2826
  st[n - 1].buf_from = -1;
@@ -2849,7 +2919,8 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2849
2919
  }
2850
2920
  }
2851
2921
  if (was_empty) {
2852
- yep_event kv; /* "[ : v" — the empty key */
2922
+ yep_event kv; /* "[ : v" — the empty key (CFD4,
2923
+ * ledgered suite-VALID) */
2853
2924
  e_event_init(&kv, YEP_EV_SCALAR);
2854
2925
  kv.implicit = 1;
2855
2926
  if (emit_now(e, &kv) != 0) {
@@ -2866,13 +2937,52 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2866
2937
  continue; /* value next */
2867
2938
  }
2868
2939
  if (st[n - 1].kind == 0 && n >= 1) {
2940
+ if (st[n - 1].pair_open && st[n - 1].q_empty_value == 2) {
2941
+ /* yeptris#741: the ':' after the '?' empty-key
2942
+ * pair's comma completes THE SAME pair (the pair's
2943
+ * VALUE indicator), not a new entry */
2944
+ size_t p2 = e->pos; /* the ':' byte is already consumed */
2945
+ while (p2 < e->len && (e->p[p2] == ' ' || e->p[p2] == '\t')) {
2946
+ p2++;
2947
+ }
2948
+ if (p2 < e->len && (e->p[p2] == ',' || e->p[p2] == ']' || e->p[p2] == '}')) {
2949
+ /* empty value at the closer: the pair closes
2950
+ * here; the closer dispatches as the separator */
2951
+ yep_event nv;
2952
+ e_event_init(&nv, YEP_EV_SCALAR);
2953
+ nv.implicit = 1;
2954
+ nv.style = YEP_STYLE_PLAIN;
2955
+ if (emit_now(e, &nv) != 0) {
2956
+ return -2;
2957
+ }
2958
+ yep_event mk;
2959
+ e_event_init(&mk, YEP_EV_MAP_END);
2960
+ if (emit_now(e, &mk) != 0) {
2961
+ return -2;
2962
+ }
2963
+ st[n - 1].pair_open = 0;
2964
+ st[n - 1].pair_value = 0;
2965
+ st[n - 1].pair_q = 0;
2966
+ st[n - 1].q_empty_value = 0;
2967
+ st[n - 1].sep = 1;
2968
+ st[n - 1].just_closed = 0;
2969
+ e->pos = p2;
2970
+ continue;
2971
+ }
2972
+ st[n - 1].q_empty_value = 0;
2973
+ e->pos++;
2974
+ continue;
2975
+ }
2869
2976
  if (st[n - 1].pair_open) {
2870
2977
  /* "[s: : v]" — ':' in the open pair's value slot
2871
2978
  * is not node content (libyaml: "did not find
2872
2979
  * expected node content") */
2873
2980
  return e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
2874
2981
  }
2875
- /* '[ : v' — single-pair mapping with an empty key */
2982
+ /* '[ : v' — single-pair mapping with an empty key
2983
+ * (CFD4, ledgered suite-VALID: the suite's tree parses
2984
+ * the bare-colon entry; libyaml's rejection is the
2985
+ * ledgered deviation) */
2876
2986
  if (n >= e->max_depth) {
2877
2987
  return e_fail(e, YEP_ERR_DEPTH, e->pos);
2878
2988
  }
@@ -4188,6 +4298,15 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4188
4298
  p[i + 3] == '\r'));
4189
4299
  if (rescue) {
4190
4300
  c->quote = 0;
4301
+ /* the marker is an unconditional document boundary (the
4302
+ * parser's own law) and the parser's post-cut reset wipes
4303
+ * the flow state; the walk must too, or a flow opened in
4304
+ * an EARLIER document re-lexes the next document's spaces
4305
+ * as token starts ("1,\"\n--- 2[\"\n--- T \"<LS>" --
4306
+ * whole kept the LS raw inside the phantom quote while
4307
+ * every chunked window folded it; the nightly's round 35,
4308
+ * crash-ef534bdf) */
4309
+ c->in_flow = 0;
4191
4310
  } else {
4192
4311
  /* NEL folds inside quotes too; LS/PS stay raw content — but
4193
4312
  * every break still arms the line-start state, or a col-0
@@ -4322,6 +4441,9 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4322
4441
  if (marker || (p[k] == '%' && ind == 0 && c->bs_content) ||
4323
4442
  (!marker && c->bs_indent != (uint32_t)-1 && ind <= (uint32_t)c->bs_indent)) {
4324
4443
  c->bs_open = 0; /* the fold ends: this line processes normally */
4444
+ if (marker) {
4445
+ c->in_flow = 0; /* markers are unconditional boundaries */
4446
+ }
4325
4447
  } else {
4326
4448
  size_t e = k;
4327
4449
  while (e < len && p[e] != '\n') {
@@ -4372,10 +4494,13 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4372
4494
  }
4373
4495
  /* the doc-inline twin ("--- |"): the node hangs off the document,
4374
4496
  * so only a later marker ends its fold (col-0 lines are content) */
4375
- if (line_start && c->in_flow == 0 && !c->bs_open && ch == '-' && i + 2 < len &&
4376
- p[i + 1] == '-' && p[i + 2] == '-' &&
4497
+ if (!c->bs_open && ch == '-' && i + 2 < len && p[i + 1] == '-' && p[i + 2] == '-' &&
4377
4498
  (i + 3 >= len || p[i + 3] == ' ' || p[i + 3] == '\t' || p[i + 3] == '\n' ||
4378
- p[i + 3] == '\r')) {
4499
+ p[i + 3] == '\r') &&
4500
+ (c->at_break || i == 0)) {
4501
+ c->in_flow = 0; /* the marker bounds the document even when the
4502
+ * walk still believes a flow is open (the same
4503
+ * round-35 law as the in-quote rescue) */
4379
4504
  size_t k = i + 3;
4380
4505
  while (k < len && (p[k] == ' ' || p[k] == '\t')) {
4381
4506
  k++;
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.39.1
4
+ version: 0.6.42.0
5
5
  platform: arm-linux-eabihf
6
6
  authors:
7
7
  - Ribose Inc.
@@ -38,6 +38,7 @@ files:
38
38
  - ext/yeptris_native/cbor_ruby.c
39
39
  - ext/yeptris_native/extconf.rb
40
40
  - ext/yeptris_native/json_ruby.c
41
+ - ext/yeptris_native/version.h
41
42
  - ext/yeptris_native/yeptris_native.c
42
43
  - lib/yeptris.rb
43
44
  - lib/yeptris/3.4/native.so