yeptris 0.6.39.1-arm-linux-eabihf → 0.6.41.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: eef0ce2de6b572cf3b06f6495b5c35bfcf7a4ff4a783ab7d60e7b77aece72a13
4
+ data.tar.gz: 6a0aedfc2d97b67eb0f42cdc539fcf1f8fc401845f57dd1060d9a84091af2418
5
5
  SHA512:
6
- metadata.gz: c9e5c875e40a7220c4a9183f3f000a39d169b1462e425dc37d4cc1389cc2496d52b9d53746ee4759e47d1e2cbc9eea0f4af5e3cd910e085d6d893d3082b971dd
7
- data.tar.gz: 331d2e087e1dc215eb0a5a3dc7e2f8a0e2ca9755ef49e201eed2c08a04c3931653850a26cfa7cf0af88ed99b02e14a14d6f6c9e4a018712d5ccdb234245d2f81
6
+ metadata.gz: 782c1f10129744b2f9a311e7b3169801e73b8fce37f9176a93679b49f17a5766e7b418505b5ab7cfc5534a85308f1f8de8f59687b3e8451e41a47bf1d03f6c6f
7
+ data.tar.gz: 55cb77f5f7c7b72b60239cfed3e7004de6fb79af7a1afe75af856d8c9df13d4635c9892e99ecf9eef71347d89b6679d870303ab609f442ba1b8046ae701c7f8b
@@ -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 41
15
+ #define YEPTRIS_VERSION_STRING "0.6.41"
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.41.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.41
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;
@@ -260,6 +326,7 @@ YEPTRIS_API char* yeptris_serialize_json_ex(YeptrisDocument handle, size_t* len,
260
326
  em.w.cap = 0;
261
327
  em.w.grow = 0;
262
328
  em.w.oom = 0;
329
+ em.w.bounded = 0;
263
330
  em.doc = (const yeptris_document*)handle;
264
331
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
265
332
  return NULL;
@@ -326,6 +393,7 @@ char* yep_serialize_json_compact(const yeptris_document* doc, size_t* len) {
326
393
  em.w.cap = 0;
327
394
  em.w.grow = 0;
328
395
  em.w.oom = 0;
396
+ em.w.bounded = 0;
329
397
  em.doc = doc;
330
398
  em.w.p = NULL;
331
399
  em.w.last = 0;
@@ -389,6 +457,7 @@ char* yep_serialize_json_pretty_ex(const yeptris_document* doc, size_t* len, con
389
457
  em.w.cap = 0;
390
458
  em.w.grow = 0;
391
459
  em.w.oom = 0;
460
+ em.w.bounded = 0;
392
461
  em.doc = doc;
393
462
  em.w.p = NULL;
394
463
  em.w.last = 0;
@@ -459,6 +528,7 @@ YEPTRIS_API size_t yeptris_serialize_stream(YeptrisDocument handle,
459
528
  em.w.cap = 0;
460
529
  em.w.grow = 0;
461
530
  em.w.oom = 0;
531
+ em.w.bounded = 0;
462
532
  em.doc = (const yeptris_document*)handle;
463
533
  if (yep_doc_dom((yeptris_document*)handle) == NULL) { /* #342 lazy */
464
534
  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);
@@ -1069,7 +1089,11 @@ static void emit_block_map(yep_emitter* em, uint32_t id, int content_col) {
1069
1089
  w->len += total;
1070
1090
  } else {
1071
1091
  if (w->grow && w->len + total + 1 > w->cap) {
1072
- wr_grow(w, total);
1092
+ if (w->bounded) {
1093
+ w->oom = 1; /* keep counting: the trusted caller reads the size */
1094
+ } else {
1095
+ wr_grow(w, total);
1096
+ }
1073
1097
  }
1074
1098
  if (w->oom) {
1075
1099
  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
  }
@@ -2186,8 +2206,11 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2186
2206
  uint8_t pair_q; /* the pair opened on an explicit '?': its
2187
2207
  * JSON-style ':' separator is still ahead */
2188
2208
  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 */
2209
+ uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2210
+ uint8_t q_empty_value; /* seq: the '?' empty-key pair — 1: opened,
2211
+ * 2: its value state (the entry comma was
2212
+ * consumed inside the pair, yeptris#741) */
2213
+ uint8_t key_coll; /* map: the nested collection opened in key position */
2191
2214
  int32_t buf_from;
2192
2215
  uint32_t entry_line;
2193
2216
  uint32_t entries;
@@ -2203,6 +2226,7 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2203
2226
  st[idx].pair_q = 0;
2204
2227
  st[idx].pair_wrap = 0;
2205
2228
  st[idx].q_coll = 0;
2229
+ st[idx].q_empty_value = 0;
2206
2230
  st[idx].buf_from = -1;
2207
2231
  st[idx].entry_line = 0;
2208
2232
  st[idx].entries = 0;
@@ -2210,20 +2234,21 @@ static void e_flow_frame_init(void* stv, int idx, int kind, int pending_key) {
2210
2234
 
2211
2235
  static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_id) {
2212
2236
  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) */
2237
+ uint8_t kind; /* 0 seq, 1 map */
2238
+ uint8_t pending_key; /* map: 1 key seen awaiting ':' value; 2 key emitted w/o ':' */
2239
+ uint8_t sep; /* a ',' was consumed and no entry followed yet */
2240
+ uint8_t just_closed; /* a nested collection closed here (key candidate) */
2241
+ uint8_t pair_open; /* seq: a buffered single-pair awaits its MAP_END */
2242
+ uint8_t pair_value; /* seq: the open pair already has its value */
2243
+ uint8_t pair_q; /* seq: the pair opened on an explicit '?':
2244
+ * its JSON-style ':' separator is still ahead */
2245
+ uint8_t pair_wrap; /* map frame pushed as a seq single-pair */
2246
+ uint8_t q_coll; /* seq: an explicit '?' key collection awaits completion */
2247
+ uint8_t q_empty_value; /* seq: the '?' empty-key pair's state (#741) */
2248
+ uint8_t key_coll; /* map: a nested collection opened in key position */
2249
+ int32_t buf_from; /* seq: entry events buffered from this index */
2250
+ uint32_t entry_line; /* seq: line where the buffered entry ended */
2251
+ uint32_t entries; /* completed entries (for trailing-comma detection) */
2227
2252
  } st[YEP_MAX_DEPTH];
2228
2253
  int n = 0;
2229
2254
 
@@ -2263,6 +2288,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2263
2288
  }
2264
2289
  int keyish = (st[n - 1].kind == 1 && st[n - 1].pending_key == 0);
2265
2290
  int q_key = 0;
2291
+ int q_empty_key = 0; /* the '?' synthesized a null key (#741) */
2266
2292
  int rc;
2267
2293
  if (e_flow_ws(e) != 0) {
2268
2294
  return -1;
@@ -2292,6 +2318,10 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2292
2318
  !(e->p[n2] == ' ' || e->p[n2] == '\t' || e->p[n2] == '\n' || e->p[n2] == '\r' ||
2293
2319
  yep_ct_is((unsigned char)e->p[n2], YEP_CT_FLOW_IND))) {
2294
2320
  st[n - 1].pair_q = 0;
2321
+ /* yeptris#741: the consumed ':' is the pair's VALUE
2322
+ * indicator — the '?' empty-key pair's value state ends
2323
+ * here or the value node trips the entry guard */
2324
+ st[n - 1].q_empty_value = 0;
2295
2325
  e->pos++;
2296
2326
  continue;
2297
2327
  }
@@ -2319,6 +2349,15 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2319
2349
  * in a sequence a ':'-led token is a new plain, so it errors */
2320
2350
  return e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
2321
2351
  }
2352
+ if (st[n - 1].kind == 0 && st[n - 1].q_empty_value == 2 && e->pos < e->len &&
2353
+ e->p[e->pos] != ',' && e->p[e->pos] != ']' && e->p[e->pos] != '}' &&
2354
+ e->p[e->pos] != ':') {
2355
+ /* yeptris#741: a node in the '?' empty-key pair's value
2356
+ * state — ENTRY_MAPPING_VALUE wants VALUE/ENTRY/END only
2357
+ * ("[?, a]" is the libyaml "did not find expected ',' or
2358
+ * ']'" error) */
2359
+ return e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
2360
+ }
2322
2361
  if (e->pos < e->len && e->p[e->pos] == '?' &&
2323
2362
  (e->pos + 1 >= e->len || e->p[e->pos + 1] == ' ' || e->p[e->pos + 1] == '\t' ||
2324
2363
  e->p[e->pos + 1] == '\n' || e->p[e->pos + 1] == '\r' ||
@@ -2342,6 +2381,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2342
2381
  ev.col = e_col(e, e->pos) + 1;
2343
2382
  rc = 1;
2344
2383
  q_key = 1;
2384
+ q_empty_key = (st[n - 1].kind == 0);
2345
2385
  } else {
2346
2386
  rc = e_flow_node(e, &ev, keyish);
2347
2387
  q_key = 1;
@@ -2368,10 +2408,15 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2368
2408
  st[n - 1].q_coll = 1;
2369
2409
  }
2370
2410
  }
2371
- if (!q_key) {
2411
+ if (!q_key && st[n - 1].pair_open) {
2372
2412
  /* a VALUE collection completes the pending pair; a KEY
2373
2413
  * collection must not (its pair completes at ':' or the
2374
- * boundary with a null value) */
2414
+ * boundary with a null value). The gate is pair_open:
2415
+ * in a SEQUENCE a plain collection entry has no pair,
2416
+ * and the unguarded write planted a stale pair_value=1
2417
+ * that a LATER '?'-opened pair wrap inherited -- its
2418
+ * comma flush then skipped the implicit null
2419
+ * (crash-8106600e, "[... , ? 20:50 , ...]"). */
2375
2420
  st[n - 1].pair_value = 1;
2376
2421
  }
2377
2422
  unsigned char oc = (unsigned char)e->p[e->pos];
@@ -2550,6 +2595,9 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2550
2595
  if (emit_now(e, &ev) != 0) {
2551
2596
  return -2;
2552
2597
  }
2598
+ if (q_empty_key) {
2599
+ st[n - 1].q_empty_value = 1;
2600
+ }
2553
2601
  continue;
2554
2602
  }
2555
2603
  }
@@ -2584,6 +2632,18 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2584
2632
  unsigned char c = (unsigned char)e->p[e->pos];
2585
2633
  if (c == ',') {
2586
2634
  e->pos++;
2635
+ if (st[n - 1].kind == 0 && st[n - 1].pair_open && st[n - 1].pair_q &&
2636
+ !st[n - 1].pair_value && st[n - 1].q_empty_value == 1) {
2637
+ /* yeptris#741: the '?' empty-key pair's first comma is
2638
+ * the entry separator libyaml's ENTRY_MAPPING_KEY
2639
+ * consumed to reach its VALUE state — the pair stays
2640
+ * open and awaits ':' (value), ',' (empty value +
2641
+ * close), or ']' (empty value + close). Closing here
2642
+ * spilled the following ': x' into a second entry
2643
+ * ("[? ,: x , a]" read as two maps). */
2644
+ st[n - 1].q_empty_value = 2;
2645
+ continue;
2646
+ }
2587
2647
  if (st[n - 1].q_coll && st[n - 1].buf_from >= 0) {
2588
2648
  int32_t bf0 = st[n - 1].buf_from;
2589
2649
  st[n - 1].buf_from = -1;
@@ -2618,6 +2678,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2618
2678
  st[n - 1].pair_open = 0;
2619
2679
  st[n - 1].pair_value = 0;
2620
2680
  st[n - 1].pair_q = 0;
2681
+ st[n - 1].q_empty_value = 0;
2621
2682
  }
2622
2683
  if (st[n - 1].buf_from >= 0) {
2623
2684
  st[n - 1].buf_from = -1;
@@ -2751,6 +2812,7 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2751
2812
  }
2752
2813
  st[n - 1].pair_open = 0;
2753
2814
  st[n - 1].pair_value = 0;
2815
+ st[n - 1].q_empty_value = 0;
2754
2816
  }
2755
2817
  if (st[n - 1].buf_from >= 0) {
2756
2818
  st[n - 1].buf_from = -1;
@@ -2849,7 +2911,8 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2849
2911
  }
2850
2912
  }
2851
2913
  if (was_empty) {
2852
- yep_event kv; /* "[ : v" — the empty key */
2914
+ yep_event kv; /* "[ : v" — the empty key (CFD4,
2915
+ * ledgered suite-VALID) */
2853
2916
  e_event_init(&kv, YEP_EV_SCALAR);
2854
2917
  kv.implicit = 1;
2855
2918
  if (emit_now(e, &kv) != 0) {
@@ -2866,13 +2929,52 @@ static int e_flow(yep_engine* e, yep_view anchor, yep_view tag, uint32_t anchor_
2866
2929
  continue; /* value next */
2867
2930
  }
2868
2931
  if (st[n - 1].kind == 0 && n >= 1) {
2932
+ if (st[n - 1].pair_open && st[n - 1].q_empty_value == 2) {
2933
+ /* yeptris#741: the ':' after the '?' empty-key
2934
+ * pair's comma completes THE SAME pair (the pair's
2935
+ * VALUE indicator), not a new entry */
2936
+ size_t p2 = e->pos; /* the ':' byte is already consumed */
2937
+ while (p2 < e->len && (e->p[p2] == ' ' || e->p[p2] == '\t')) {
2938
+ p2++;
2939
+ }
2940
+ if (p2 < e->len && (e->p[p2] == ',' || e->p[p2] == ']' || e->p[p2] == '}')) {
2941
+ /* empty value at the closer: the pair closes
2942
+ * here; the closer dispatches as the separator */
2943
+ yep_event nv;
2944
+ e_event_init(&nv, YEP_EV_SCALAR);
2945
+ nv.implicit = 1;
2946
+ nv.style = YEP_STYLE_PLAIN;
2947
+ if (emit_now(e, &nv) != 0) {
2948
+ return -2;
2949
+ }
2950
+ yep_event mk;
2951
+ e_event_init(&mk, YEP_EV_MAP_END);
2952
+ if (emit_now(e, &mk) != 0) {
2953
+ return -2;
2954
+ }
2955
+ st[n - 1].pair_open = 0;
2956
+ st[n - 1].pair_value = 0;
2957
+ st[n - 1].pair_q = 0;
2958
+ st[n - 1].q_empty_value = 0;
2959
+ st[n - 1].sep = 1;
2960
+ st[n - 1].just_closed = 0;
2961
+ e->pos = p2;
2962
+ continue;
2963
+ }
2964
+ st[n - 1].q_empty_value = 0;
2965
+ e->pos++;
2966
+ continue;
2967
+ }
2869
2968
  if (st[n - 1].pair_open) {
2870
2969
  /* "[s: : v]" — ':' in the open pair's value slot
2871
2970
  * is not node content (libyaml: "did not find
2872
2971
  * expected node content") */
2873
2972
  return e_fail(e, YEP_ERR_UNEXPECTED, e->pos);
2874
2973
  }
2875
- /* '[ : v' — single-pair mapping with an empty key */
2974
+ /* '[ : v' — single-pair mapping with an empty key
2975
+ * (CFD4, ledgered suite-VALID: the suite's tree parses
2976
+ * the bare-colon entry; libyaml's rejection is the
2977
+ * ledgered deviation) */
2876
2978
  if (n >= e->max_depth) {
2877
2979
  return e_fail(e, YEP_ERR_DEPTH, e->pos);
2878
2980
  }
@@ -4188,6 +4290,15 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4188
4290
  p[i + 3] == '\r'));
4189
4291
  if (rescue) {
4190
4292
  c->quote = 0;
4293
+ /* the marker is an unconditional document boundary (the
4294
+ * parser's own law) and the parser's post-cut reset wipes
4295
+ * the flow state; the walk must too, or a flow opened in
4296
+ * an EARLIER document re-lexes the next document's spaces
4297
+ * as token starts ("1,\"\n--- 2[\"\n--- T \"<LS>" --
4298
+ * whole kept the LS raw inside the phantom quote while
4299
+ * every chunked window folded it; the nightly's round 35,
4300
+ * crash-ef534bdf) */
4301
+ c->in_flow = 0;
4191
4302
  } else {
4192
4303
  /* NEL folds inside quotes too; LS/PS stay raw content — but
4193
4304
  * every break still arms the line-start state, or a col-0
@@ -4322,6 +4433,9 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4322
4433
  if (marker || (p[k] == '%' && ind == 0 && c->bs_content) ||
4323
4434
  (!marker && c->bs_indent != (uint32_t)-1 && ind <= (uint32_t)c->bs_indent)) {
4324
4435
  c->bs_open = 0; /* the fold ends: this line processes normally */
4436
+ if (marker) {
4437
+ c->in_flow = 0; /* markers are unconditional boundaries */
4438
+ }
4325
4439
  } else {
4326
4440
  size_t e = k;
4327
4441
  while (e < len && p[e] != '\n') {
@@ -4372,10 +4486,13 @@ static yep_norm_tok norm_step(yep_norm_ctx* c, const char* p, size_t len, size_t
4372
4486
  }
4373
4487
  /* the doc-inline twin ("--- |"): the node hangs off the document,
4374
4488
  * 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] == '-' &&
4489
+ if (!c->bs_open && ch == '-' && i + 2 < len && p[i + 1] == '-' && p[i + 2] == '-' &&
4377
4490
  (i + 3 >= len || p[i + 3] == ' ' || p[i + 3] == '\t' || p[i + 3] == '\n' ||
4378
- p[i + 3] == '\r')) {
4491
+ p[i + 3] == '\r') &&
4492
+ (c->at_break || i == 0)) {
4493
+ c->in_flow = 0; /* the marker bounds the document even when the
4494
+ * walk still believes a flow is open (the same
4495
+ * round-35 law as the in-quote rescue) */
4379
4496
  size_t k = i + 3;
4380
4497
  while (k < len && (p[k] == ' ' || p[k] == '\t')) {
4381
4498
  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.41.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