yeptris 0.6.32.1-arm-linux-eabihf → 0.6.33.2-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: 8a9333da6cc1b324ef56312f4f72c815d82fe2707c72ea06ee10dba97af0b3ae
4
- data.tar.gz: 5e17a70af42215e78a0f2307349cb56cc3f14f8ce492d73f4a49fd361867a53b
3
+ metadata.gz: 42eb1c4ae3d3ca06b6ec3eb1a1c468eb76f10330ea4bd94b8517151fe41b1fa5
4
+ data.tar.gz: 7a4f59ab5e830e18047d8e20689a12e45dbcf8be65d9ebffe1ef9e2925861b73
5
5
  SHA512:
6
- metadata.gz: c312b9bf2f386719f51bdd34f94fdd7fa3957b9ccc7a49a60225db79ae6f4475a33369e2c44aafb92629fb00b4bc3f831c5df4ae878e78365f52e09533c00d87
7
- data.tar.gz: 8951d5b8009fc9bf65853fef2a54df2bc46d97c3eb48f0a60aff0800019e9bf750726128682b93850a5764531732564c0d76f77dd84099822881e7d2edac1d83
6
+ metadata.gz: bf593a29d457d07357597161bb5223127e6a5b744cfb94065308ac8e8e773a53b432d8ff53e1465aa0052216db2b39bb9d2fd2153f302f78f44b61226cd13565
7
+ data.tar.gz: 949537f2f78205e80b72992ed1f4f39f29cca4e04c2a2bfc476ec3af7e5b5fcdfbb9b21eb9c9cc9f33d0b1c7ef86a72a041ec7b5eb4e186eb4ebaec22517d35c
data/README.adoc CHANGED
@@ -5,6 +5,15 @@ https://github.com/leptris/yeptris[libyeptris] — the YAML counterpart
5
5
  of libleptris. Psych-compatible semantics, one shared library, zero
6
6
  compilation at install.
7
7
 
8
+ Part of the yeptris program: *the best (multiple dimensions)
9
+ YAML/JSON parser/builder ever* — conformance, performance,
10
+ safety/hardening, API surfaces, transport correctness, and trust,
11
+ held simultaneously, in C, Ruby, Rust, and JS. This binding is the
12
+ most complete surface: the Psych drop-in namespace, JSON.parse
13
+ parity with the strict duplicate-key contract, CBOR, and ValueML,
14
+ tracking every libyeptris release in lockstep. The achievement
15
+ program lives in the engine's PLAN.md ("The best-ever program").
16
+
8
17
  == Install (development)
9
18
 
10
19
  The gem loads a shared `libyeptris` — from `YEPTRIS_LIB_PATH`, a
@@ -173,9 +173,19 @@ static VALUE jr_object(jr* j) {
173
173
  }
174
174
 
175
175
  static VALUE jr_object_body(jr* j, VALUE h_pre) {
176
+ /* The pairs staging buffer MUST live in GC-scanned memory: pairs
177
+ * beyond the current one exist only there once the key/val locals
178
+ * are reused, so a malloc'd buffer makes them invisible to the
179
+ * collector — it can free them mid-parse and recycle their slots,
180
+ * and rb_hash_bulk_insert then inserts garbage (yeptris-ruby#279:
181
+ * the arm64-darwin SIGSEGV whose fault address was document text).
182
+ * The stack array IS scanned, so the bulk fast path is bounded at
183
+ * 31 pairs; beyond that the buffered pairs flush with one
184
+ * bulk_insert and the rest of the object rides rb_hash_aset. */
176
185
  VALUE pairs[64];
177
186
  VALUE* pv = pairs;
178
187
  size_t pcap = 64, pn = 0, heap_cap = 0;
188
+ int aset_rest = 0;
179
189
  for (;;) {
180
190
  jr_ws(j);
181
191
  if (j->i >= j->len || j->p[j->i] != '"') { j->err = -2; goto out; }
@@ -186,7 +196,7 @@ static VALUE jr_object_body(jr* j, VALUE h_pre) {
186
196
  j->i++;
187
197
  VALUE val = jr_value(j);
188
198
  if (j->err) goto out;
189
- if (yep_ins_mode == YEP_INS_ASET || j->strict_dup) {
199
+ if (yep_ins_mode == YEP_INS_ASET || j->strict_dup || aset_rest) {
190
200
  VALUE h = h_pre == Qundef ? (h_pre = HASH_NEW_CAPA(8)) : h_pre;
191
201
  if (j->strict_dup && !NIL_P(rb_hash_aref(h, key))) {
192
202
  j->err = -3;
@@ -196,15 +206,21 @@ static VALUE jr_object_body(jr* j, VALUE h_pre) {
196
206
  rb_hash_aset(h, key, val);
197
207
  } else {
198
208
  if (pn + 2 > pcap) {
199
- size_t ncap = pcap * 2;
200
- VALUE* nv = malloc(ncap * sizeof(VALUE));
201
- if (!nv) { j->err = -1; goto out; }
202
- memcpy(nv, pv, pn * sizeof(VALUE));
203
- if (pv != pairs) { free(pv); heap_cap = 1; }
204
- pv = nv; pcap = ncap;
209
+ /* buffer full: flush what is staged (no heap staging —
210
+ * see the #279 note above); THIS pair and the rest of
211
+ * the object ride aset — no continue here, the
212
+ * delimiter scan below must still run */
213
+ VALUE h = (h_pre == Qundef) ? (h_pre = HASH_NEW_CAPA((long)(pn / 2) + 8)) : h_pre;
214
+ rb_hash_bulk_insert((long)pn, (const VALUE*)pv, h);
215
+ pn = 0;
216
+ aset_rest = 1;
217
+ }
218
+ if (aset_rest) {
219
+ rb_hash_aset(h_pre, key, val);
220
+ } else {
221
+ pv[pn++] = key;
222
+ pv[pn++] = val;
205
223
  }
206
- pv[pn++] = key;
207
- pv[pn++] = val;
208
224
  }
209
225
  jr_ws(j);
210
226
  if (j->i >= j->len) { j->err = -2; goto out; }
@@ -351,7 +367,17 @@ double yep_rb_scan_time(const char* p, size_t len, int n) {
351
367
  VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
352
368
  jr j;
353
369
  memset(&j, 0, sizeof(j));
354
- j.p = p; j.len = len; j.enc = rb_utf8_encoding();
370
+ /* The parse holds j.p across unbounded allocations, and the GC
371
+ * strategy knobs leave collection ENABLED during the window
372
+ * (arm64's default is none). A compaction moving the source
373
+ * string would dangle every j->p read after the move
374
+ * (yeptris-ruby#279's heap-layout-dependent SIGSEGV class), so
375
+ * the parse runs on a private copy: one memcpy of the input,
376
+ * then no Ruby object backs the cursor. */
377
+ char* own = malloc(len ? len : 1);
378
+ if (own == NULL) rb_raise(rb_eNoMemError, "yeptris native json");
379
+ memcpy(own, p, len);
380
+ j.p = own; j.len = len; j.enc = rb_utf8_encoding();
355
381
  j.strict_dup = strict_dup;
356
382
  VALUE already = Qtrue;
357
383
  if (yep_gc_mode != YEP_GC_NONE) already = rb_gc_disable();
@@ -359,6 +385,7 @@ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
359
385
  jr_ws(&j);
360
386
  if (j.i != len && j.err == 0) j.err = -2;
361
387
  free(j.scratch);
388
+ free(own);
362
389
  if (already == Qfalse) rb_gc_enable();
363
390
  if (yep_gc_mode == YEP_GC_START && j.err == 0) rb_gc_start();
364
391
  if (j.err == -1) rb_raise(rb_eNoMemError, "yeptris native json");
@@ -366,6 +393,11 @@ VALUE yep_rb_parse_json(const char* p, size_t len, int strict_dup) {
366
393
  rb_raise(rb_path2class("Yeptris::ParseError"), "duplicate key \"%s\" in JSON object",
367
394
  RSTRING_PTR(j.dup_key));
368
395
  }
369
- if (j.err != 0) rb_raise(rb_path2class("Yeptris::ParseError"), "native json parse failed");
396
+ if (j.err != 0) {
397
+ /* the tape engine's wording ("not strict JSON at byte N") so
398
+ * consumers can pin one error shape across both routes
399
+ * (yeptris-ruby#279's adjacent note) */
400
+ rb_raise(rb_path2class("Yeptris::ParseError"), "not strict JSON at byte %zu", j.i);
401
+ }
370
402
  return v;
371
403
  }
Binary file
data/lib/yeptris/ffi.rb CHANGED
@@ -228,12 +228,17 @@ module Yeptris
228
228
  %i[pointer size_t int int pointer pointer], :int
229
229
  attach_function :yeptris_marshal_node,
230
230
  %i[yeptris_node pointer pointer], :int
231
+ attach_function :yeptris_marshal_node_ex,
232
+ %i[yeptris_node uint32 pointer pointer], :int
231
233
  attach_function :yeptris_marshal_free, [:pointer], :void
232
234
  # The bulk child drain (#168's quadratic): O(n) iteration for
233
235
  # #each/#each_pair. Engines without it fall back to the per-index
234
236
  # walk (correct, quadratic).
235
237
  CHILDREN_DRAIN = !(@missing ||= []).include?(:yeptris_node_children)
236
238
  MARSHAL = !(@missing ||= []).include?(:yeptris_marshal_node)
239
+ # the strict-dup marshal entry (engine 0.6.34+, yeptris-ruby#278):
240
+ # gates the JSON fallback's C route in STRICT mode
241
+ MARSHAL_STRICT_DUP = !(@missing ||= []).include?(:yeptris_marshal_node_ex)
237
242
 
238
243
  # bulk build (TODO.impl/15 phase D): one call raises a document
239
244
  BUILD_SCALAR = 1
data/lib/yeptris/json.rb CHANGED
@@ -9,10 +9,13 @@ module Yeptris
9
9
  # keeps the Psych contract for every input (JSON-shaped included),
10
10
  # so the two never drift into each other's semantics.
11
11
  #
12
- # Engines, fastest first (both exact — the parity spec runs against
12
+ # Engines, fastest first (all exact — the parity spec runs against
13
13
  # whichever is loaded):
14
14
  # 1. the native materializer (fused C scan → VALUE; opt-in build)
15
- # 2. the record drain + strict conversion walk (always available)
15
+ # 2. the marshal engine (#278): strict-JSON parse + Marshal 4.8
16
+ # emission + ONE core Marshal.load — all C, no per-value Ruby
17
+ # (engine 0.6.34+; the strict flag follows the resolved json gem)
18
+ # 3. the record drain + strict conversion walk (always available)
16
19
  module JSON
17
20
  class Error < ::Yeptris::Error; end
18
21
  class ParseError < Error; end
@@ -39,9 +42,46 @@ module Yeptris
39
42
  raise ParseError, e.message
40
43
  end
41
44
  end
45
+ # yeptris-ruby#278: with no Ruby-version-matched native build the
46
+ # pure-Ruby walk measured 0.11x vs JSON.parse on real documents —
47
+ # the marshal engine keeps the fallback C-side end to end.
48
+ return marshal_engine(source, 0) if ::Yeptris::FFI::MARSHAL_STRICT_DUP
42
49
  tape_engine(source)
43
50
  end
44
51
 
52
+ # The marshal engine (#278): yeptris_parse_json gates the strict
53
+ # grammar, yeptris_marshal_node_ex writes the Ruby Marshal 4.8
54
+ # stream (STRICT_DUP_KEYS raising on json 3's default contract),
55
+ # and ONE core Marshal.load materializes the graph. Returns nil
56
+ # when the engine cannot serve the route (caller keeps walking);
57
+ # every parse/dup failure raises ParseError.
58
+ def marshal_engine(source, flags)
59
+ begin
60
+ gate = ::Yeptris::Document.parse_json(source)
61
+ rescue ::Yeptris::ParseError => e
62
+ raise ParseError, e.message
63
+ end
64
+ begin
65
+ out_p = ::FFI::MemoryPointer.new(:pointer)
66
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
67
+ st = ::Yeptris::FFI.yeptris_marshal_node_ex(
68
+ ::Yeptris::FFI.yeptris_document_root(gate.c_ptr, 0), flags, out_p, olen_p
69
+ )
70
+ if st != ::Yeptris::FFI::OK
71
+ raise ParseError, ::Yeptris::FFI.last_error_message if st == ::Yeptris::FFI::ERROR_PARSE
72
+
73
+ return nil # UNSUPPORTED: merge keys et al. cannot occur in JSON
74
+ end
75
+ buf = out_p.read_pointer
76
+ data = buf.read_bytes(::Yeptris::FFI.read_c_size_t(olen_p))
77
+ data.force_encoding(Encoding::ASCII_8BIT)
78
+ ::Yeptris::FFI.yeptris_marshal_free(buf)
79
+ Marshal.load(data)
80
+ ensure
81
+ gate.free
82
+ end
83
+ end
84
+
45
85
  # The tape engine (issue #81): ONE call validates and materializes
46
86
  # the record stream; the walk reads four bulk columns and slices
47
87
  # strings straight out of the caller's source — no gate document,
@@ -212,6 +252,12 @@ module Yeptris
212
252
  raise ParseError, e.message
213
253
  end
214
254
  begin
255
+ if ::Yeptris::FFI::MARSHAL_STRICT_DUP
256
+ routed = marshal_engine(
257
+ source, STRICT_DUPLICATE_KEYS ? ::Yeptris::FFI::MARSHAL_STRICT_DUP_KEYS : 0
258
+ )
259
+ return routed unless routed.nil?
260
+ end
215
261
  cols = ::Yeptris::FFI::ValueColumns.new
216
262
  st = ::Yeptris::FFI.yeptris_value_drain_columns(
217
263
  source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
data/lib/yeptris/node.rb CHANGED
@@ -50,7 +50,7 @@ class Yeptris::Node
50
50
 
51
51
  # Scalar content / alias name (UTF-8 String), nil for collections.
52
52
  def value
53
- len = ::FFI::MemoryPointer.new(:size_t)
53
+ len = len_buffer
54
54
  ptr = alive { Yeptris::FFI.yeptris_node_value(@c_ptr, len) }
55
55
  return nil if ptr.null?
56
56
 
@@ -69,9 +69,18 @@ class Yeptris::Node
69
69
  STYLES[alive { Yeptris::FFI.yeptris_node_style(@c_ptr) }]
70
70
  end
71
71
 
72
+ # The out-length receiver: ONE buffer per thread, reused. A fresh
73
+ # FFI::MemoryPointer per accessor call was an allocation storm that
74
+ # stalled the Windows heap under the #182 churn loop (the #504
75
+ # family — the hang's stack sits in FFI::MemoryPointer#initialize,
76
+ # node.rb anchor/tag reads inside Integer#times with GC.start).
77
+ def len_buffer
78
+ Thread.current[:yeptris_len_buf] ||= ::FFI::MemoryPointer.new(:size_t)
79
+ end
80
+
72
81
  # Explicit tag URI when present, else nil.
73
82
  def tag
74
- len = ::FFI::MemoryPointer.new(:size_t)
83
+ len = len_buffer
75
84
  ptr = alive { Yeptris::FFI.yeptris_node_tag(@c_ptr, len) }
76
85
  return nil if ptr.null?
77
86
 
@@ -79,7 +88,7 @@ class Yeptris::Node
79
88
  end
80
89
 
81
90
  def anchor
82
- len = ::FFI::MemoryPointer.new(:size_t)
91
+ len = len_buffer
83
92
  ptr = alive { Yeptris::FFI.yeptris_node_anchor(@c_ptr, len) }
84
93
  return nil if ptr.null?
85
94
 
@@ -216,7 +216,7 @@ module Yeptris
216
216
  # explicit !!str tag (single-quoted) so it does not
217
217
  # re-load as a merge
218
218
  key_node = @tree.new_scalar("<<", :single_quoted)
219
- key_node.set_tag("!!str")
219
+ key_node.set_tag("tag:yaml.org,2002:str")
220
220
  m.map_add_node(key_node, visit(v))
221
221
  else
222
222
  m.map_add(key_text(k), visit(v))
data/lib/yeptris/yaml.rb CHANGED
@@ -364,7 +364,7 @@ module Yeptris
364
364
  # stdlib yaml_tree: a literal chevron key carries the
365
365
  # explicit !!str tag so it does not re-load as a merge
366
366
  key_node = doc.new_scalar("<<", :single_quoted)
367
- key_node.set_tag("!!str")
367
+ key_node.set_tag("tag:yaml.org,2002:str")
368
368
  m.map_add_node(key_node, build(doc, v, seen))
369
369
  else
370
370
  m.map_add(key_text(k), build(doc, v, seen))
data/lib/yeptris.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module Yeptris
4
4
  # The gem's version lives in the parent namespace's file — the last
5
5
  # internal require (yeptris/version) retired with it.
6
- VERSION = "0.6.32.1".freeze
6
+ VERSION = "0.6.33.2".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
data/libyeptris.so CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.32
6
+ VERSION 0.6.33
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -98,15 +98,27 @@ if(YEPTRIS_ENABLE_UBSAN)
98
98
  message(STATUS "UndefinedBehaviorSanitizer: ENABLED (fatal)")
99
99
  endif()
100
100
 
101
- # libFuzzer implies ASAN (libleptris TODO 40 pattern).
101
+ # libFuzzer implies ASAN+UBSan (the teptris fuzz.yml pattern: the
102
+ # memory-safety contracts ride the SAME binary as the fuzzer, so every
103
+ # nightly round reports UB at the crash site instead of libFuzzer's
104
+ # "combine with AddressSanitizer" note).
102
105
  if(YEPTRIS_ENABLE_FUZZING)
103
106
  # instrument everything, but link the fuzzer MAIN only into the
104
107
  # harnesses: a global -fsanitize=fuzzer link option demands
105
108
  # LLVMFuzzerTestOneInput from every executable (the CLI and the
106
109
  # test binaries died on it once CXX also pinned to clang++)
107
- add_compile_options(-fsanitize=fuzzer-no-link,address -fno-omit-frame-pointer -g)
108
- add_link_options(-fsanitize=address)
109
- message(STATUS "libFuzzer: ENABLED (implies ASAN)")
110
+ if(APPLE)
111
+ # Apple ld64 cannot link UBSan'd static archives
112
+ # ("invalid r_symbolnum"): ASan-only locally
113
+ add_compile_options(-fsanitize=fuzzer-no-link,address -fno-omit-frame-pointer -g)
114
+ add_link_options(-fsanitize=address)
115
+ message(STATUS "libFuzzer: ENABLED (implies ASAN; UBSan needs linux ld)")
116
+ else()
117
+ add_compile_options(-fsanitize=fuzzer-no-link,address,undefined
118
+ -fno-sanitize-recover=undefined -fno-omit-frame-pointer -g)
119
+ add_link_options(-fsanitize=address,undefined)
120
+ message(STATUS "libFuzzer: ENABLED (implies ASAN+UBSan)")
121
+ endif()
110
122
  endif()
111
123
 
112
124
  # -------------------------------------------------------------------- LTO --
@@ -392,6 +392,27 @@ int yep_dom_on_event(void* ctx, const yep_event* ev) {
392
392
  if (d->depth == 0) {
393
393
  return -1;
394
394
  }
395
+ if (ev->type == YEP_EV_MAP_END && d->map_pending_key[d->depth - 1]) {
396
+ /* an odd child reached the mapping: the unpaired node —
397
+ * possibly an ANCHORED one — used to be dropped here, its
398
+ * anchor lost, and a later alias then emitted a dangling
399
+ * reference the re-parse rejected (the nightly's round
400
+ * 102). Pair it with a null: "k:" with nothing is a null
401
+ * value in every other position. */
402
+ yep_event nv;
403
+ memset(&nv, 0, sizeof(nv));
404
+ nv.type = YEP_EV_SCALAR;
405
+ nv.style = YEP_STYLE_PLAIN;
406
+ nv.implicit = 1;
407
+ uint32_t nid = dom_new_node(d, &nv, YEP_DOM_SCALAR);
408
+ if (nid == UINT32_MAX) {
409
+ return -1;
410
+ }
411
+ uint32_t top = d->stack[d->depth - 1];
412
+ dom_link(d, top, d->pending_key_id[d->depth - 1]);
413
+ dom_link(d, top, nid);
414
+ d->map_pending_key[d->depth - 1] = 0;
415
+ }
395
416
  d->depth--;
396
417
  return 0;
397
418
 
@@ -729,6 +750,7 @@ static int jb_value(jbuilder* b) {
729
750
  return 0;
730
751
  }
731
752
  for (;;) {
753
+ size_t member_start = b->i;
732
754
  int rc = jb_value(b);
733
755
  if (rc != 0) {
734
756
  return rc;
@@ -738,6 +760,14 @@ static int jb_value(jbuilder* b) {
738
760
  /* a value must be followed by , or } — the map's key
739
761
  * slot is pending until the ':' value completes */
740
762
  if (b->i < b->len && b->p[b->i] == ':' && d->map_pending_key[d->depth - 1] != 0) {
763
+ /* libyaml's simple-key law rides the JSON-class
764
+ * route too: a key whose colon distance crosses
765
+ * 1024 characters defers to the engine's clean
766
+ * error (run 38143486175) */
767
+ if (b->i - member_start > 1024 &&
768
+ yep_simple_key_chars(b->p + member_start, b->i - member_start) > 1024) {
769
+ return -2;
770
+ }
741
771
  b->i++;
742
772
  rc = jb_value(b);
743
773
  if (rc != 0) {
@@ -820,8 +850,11 @@ int dom_on_flow_build(void* ctx, const char* p, size_t open, size_t len, uint32_
820
850
  yep_json_walk w;
821
851
  yep_json_tok t;
822
852
  yep_json_walk_init(&w, p, len, open, max_depth);
823
- w.strict = d->flow_strict; /* the strict-JSON route rides the same
824
- fused walk (TODO.restructure/81) */
853
+ w.strict = 1; /* the same byte-for-byte JSON gate as the engine's
854
+ * pass 1: a lenient walk split number/literal keys
855
+ * across the fast/slow paths (whole {9->1} vs chunked
856
+ * {"9:1"->null}); flow_strict keeps owning the TAG
857
+ * route only (TODO.restructure/81) */
825
858
  for (;;) {
826
859
  yep_jw_status st = yep_json_walk_next(&w, &t);
827
860
  if (st == YEP_JW_REJECT) {
@@ -976,6 +1009,23 @@ int dom_from_tape(yep_dom* d, const yeptris_json_tape* t) {
976
1009
  if (d->depth == 0) {
977
1010
  return -1;
978
1011
  }
1012
+ if (d->nodes[d->stack[d->depth - 1]].kind == YEP_DOM_MAPPING &&
1013
+ d->map_pending_key[d->depth - 1]) {
1014
+ /* an odd child reached the mapping (see the event-path
1015
+ * MAP_END): pair it with a null instead of dropping it */
1016
+ yep_event nv;
1017
+ memset(&nv, 0, sizeof(nv));
1018
+ nv.type = YEP_EV_SCALAR;
1019
+ nv.style = YEP_STYLE_PLAIN;
1020
+ nv.implicit = 1;
1021
+ uint32_t nid = dom_new_node(d, &nv, YEP_DOM_SCALAR);
1022
+ if (nid == UINT32_MAX) {
1023
+ return -1;
1024
+ }
1025
+ dom_link(d, d->stack[d->depth - 1], d->pending_key_id[d->depth - 1]);
1026
+ dom_link(d, d->stack[d->depth - 1], nid);
1027
+ d->map_pending_key[d->depth - 1] = 0;
1028
+ }
979
1029
  d->depth--;
980
1030
  break;
981
1031
  case YEP_T_NULL: