yeptris 0.6.56.1-arm-linux-eabihf → 0.6.57.1-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: 92277802904cb56365eae27dfc3929fcbeceec3db850dbc95138127b3990f715
4
- data.tar.gz: cf8b22137e0eea5536a25bbc9fa702ead3a939fea02681256632ee2041fd173f
3
+ metadata.gz: 2ff669641748a920b861c211a4acdcd175c18f0be28b2d917fd81ddcbee85012
4
+ data.tar.gz: df7abbc39687b9d532cd8e49d64c8bb738102dea0b18bb486639933616b2f4de
5
5
  SHA512:
6
- metadata.gz: 5805c0315c67b0b3db2fc87575d39dc61a7c5f906a265bd580c22b6ec74bb349e9a7df8efbfaf8ff70f88505c6b58777ec6bf5598ece063c94b9cf3e5f5d4edd
7
- data.tar.gz: 467fc637747e2249e18ee22dda61a58ed475ebaede6f4196c6f9f4e3b905105f0813e4ad55e481ee280ba767c6f28e067bc859e896e2b5bc4938df8f587c3a87
6
+ metadata.gz: 621f9036c39cef1b661382ea245ee40ff1f9a1134199888475697b7e54f63f83887f3abecb4f370d23f45bc74d3f4ba110b47fca676f328bb045113764a5b620
7
+ data.tar.gz: acf1b5c5ff2025e0885d2b302074f9b327bde962ba943809bc361212d444f839c89916d1b09576d1a9d8a90f759279eef67740ebcdaa6fe585a3e331ec0855b0
@@ -11,8 +11,8 @@
11
11
 
12
12
  #define YEPTRIS_VERSION_MAJOR 0
13
13
  #define YEPTRIS_VERSION_MINOR 6
14
- #define YEPTRIS_VERSION_PATCH 56
15
- #define YEPTRIS_VERSION_STRING "0.6.56"
14
+ #define YEPTRIS_VERSION_PATCH 57
15
+ #define YEPTRIS_VERSION_STRING "0.6.57"
16
16
 
17
17
  #ifdef __cplusplus
18
18
  extern "C" {
@@ -437,6 +437,10 @@ module Yeptris
437
437
  when /\A!ruby\/object(?::(.+))?\z/
438
438
  klass = resolve_class(Regexp.last_match(1).to_s)
439
439
  revive_object(klass, node)
440
+ when /\A!ruby\/hash:(.+)\z/
441
+ revive_hash_class(resolve_class(Regexp.last_match(1)), node)
442
+ when /\A!ruby\/exception(?::(.*))?\z/
443
+ revive_exception(resolve_class(Regexp.last_match(1).to_s), node)
440
444
  else
441
445
  # Psych.load_tags first (the class registry, #95 bug 4);
442
446
  # anything else stays the plain mapping
@@ -452,9 +456,13 @@ module Yeptris
452
456
  # key's SHAPE decides (an explicit !!str '<<' is a literal key,
453
457
  # not a merge), the VALUE's node kind picks the arm, and every
454
458
  # merge is TypeError-guarded (a bad element keeps the whole
455
- # '<<' pair literal — merges are all-or-nothing).
456
- def hash_into(h, node)
457
- anchors[node.anchor] = h if node.anchor
459
+ # '<<' pair literal — merges are all-or-nothing). The anchor
460
+ # registrant is the receiver by default; the object revivals
461
+ # pass the revived OBJ (psych's revive(): the object itself is
462
+ # registered before the children walk, so an alias inside an
463
+ # ivar pointing back at the object resolves to it).
464
+ def hash_into(h, node, anchor: nil)
465
+ anchors[node.anchor] = (anchor || h) if node.anchor
458
466
  node.children.each_slice(2) do |k, v|
459
467
  key = visit(k)
460
468
  val = visit(v)
@@ -502,29 +510,124 @@ module Yeptris
502
510
  end
503
511
 
504
512
  def revive_object(klass, node)
505
- # bare !ruby/object (no class): Psych.dump(Object.new)'s form.
506
- # An Object has no declared state — allocation is the whole
507
- # revival (no reflection, no protocol needed).
508
- return ::Object.allocate if klass.nil?
513
+ obj = klass ? klass.allocate : ::Object.allocate
514
+ # psych's revive(): the object registers BEFORE the children
515
+ # walk — an alias inside the state pointing back at the
516
+ # object resolves to it (identity through ivars)
517
+ anchors[node.anchor] = obj if node.anchor
509
518
 
510
- raise ::Yeptris::DumpError, "#{klass} is not Yeptris::Psych::Encodable: implement init_with(coder)" unless klass <= ::Yeptris::Psych::Encodable
519
+ if klass
520
+ # The yeptris encoder's format (visit_encode_with): the
521
+ # Encodable protocol rides the __init__ marker, so the
522
+ # state passes back through init_with(coder) without
523
+ # library-side ivar reflection. The format decides on the
524
+ # RAW key before any value is visited (the old walk's
525
+ # shape; sibling values stay untouched when it fires).
526
+ init = nil
527
+ node.children.each_slice(2) do |k, v|
528
+ init = v if k.to_ruby.to_s == "__init__"
529
+ end
530
+ if init
531
+ unless klass <= ::Yeptris::Psych::Encodable
532
+ raise ::Yeptris::DumpError,
533
+ "#{klass} is not Yeptris::Psych::Encodable: implement init_with(coder)"
534
+ end
511
535
 
512
- obj = klass.allocate
513
- anchors[node.anchor] = obj if node.anchor
514
- # The class opts in via init_with(coder); the encoder wrote
515
- # `coder["__init__"] = the mapping` so we can pass it back
516
- # without library-side ivar reflection.
536
+ coder = ::Yeptris::Psych::CoderShim.new
537
+ if init.is_a?(::Yeptris::Psych::Nodes::Mapping)
538
+ init.children.each_slice(2) do |ck, cv|
539
+ coder[ck.to_ruby.to_s] = visit(cv)
540
+ end
541
+ end
542
+ obj.init_with(coder)
543
+ return obj
544
+ end
545
+ end
546
+
547
+ # stdlib psych's format (visit_Object + dump_ivars): the
548
+ # mapping's own top-level pairs ARE the object state —
549
+ # revives through init_with when the class defines it, ivar
550
+ # assignment otherwise. THE AUDIT FIX: the __init__-only
551
+ # walk silently dropped the ivars of every stdlib
552
+ # Psych.dump-produced document.
553
+ init_with_state(obj, hash_into({}, node, anchor: obj), node)
554
+ obj
555
+ end
556
+
557
+ # !ruby/hash:K (stdlib visit_Hash of a Hash subclass): the
558
+ # SUBCLASS allocates and the mapping's own pairs fill it
559
+ # (merge keys included, hash_into semantics). A name that
560
+ # resolves to a non-Hash raises stdlib resolve_subclass's
561
+ # ArgumentError; an unresolvable name stays a plain Hash (the
562
+ # resolve_class-returns-nil convention this binding already
563
+ # applies to !ruby/object and !ruby/struct — safe_load raises
564
+ # DisallowedClass for it first).
565
+ def revive_hash_class(klass, node)
566
+ if klass
567
+ unless klass <= ::Hash
568
+ raise ::ArgumentError, "Invalid tag: expected a subclass of Hash, got #{klass}"
569
+ end
570
+
571
+ h = klass.allocate
572
+ else
573
+ h = {}
574
+ end
575
+ anchors[node.anchor] = h if node.anchor
576
+ hash_into(h, node)
577
+ h
578
+ end
579
+
580
+ # !ruby/exception[:K] (stdlib visit_Exception + build_exception):
581
+ # the message rides the exception's own construction surface
582
+ # (Class#exception — psych's C build_exception writes the
583
+ # engine-internal mesg slot on an allocated instance, which
584
+ # Ruby-level ivar assignment cannot reach: message would stay
585
+ # the class name), backtrace is consumed if present, and every
586
+ # other pair lands as ivars (message/backtrace excepted — they
587
+ # name the exception's own surface, not object state). Bare
588
+ # !ruby/exception rides ::Exception (psych's class_loader
589
+ # exception fallback). A subclass whose initialize refuses a
590
+ # message argument constructs bare — same fallback psych's
591
+ # Ruby-side build_exception carried before the C move.
592
+ def revive_exception(klass, node)
593
+ h = {}
517
594
  node.children.each_slice(2) do |k, v|
518
- key = k.to_ruby.to_s
519
- next unless key == "__init__"
595
+ h[visit(k)] = visit(v)
596
+ end
597
+ mesg = h.delete("message")
598
+ e = build_exception(klass || ::Exception, mesg)
599
+ anchors[node.anchor] = e if node.anchor
600
+ e.set_backtrace(h.delete("backtrace")) if h.key?("backtrace")
601
+ init_with_state(e, h, node)
602
+ e
603
+ end
520
604
 
605
+ def build_exception(klass, mesg)
606
+ return klass.exception if mesg.nil?
607
+
608
+ begin
609
+ klass.exception(mesg)
610
+ rescue ::ArgumentError, ::TypeError
611
+ klass.exception
612
+ end
613
+ end
614
+
615
+ # psych's init_with: a class defining init_with takes the coder
616
+ # (the raw pair map, coder.tag = the mapping's tag); everything
617
+ # else — bare Object included — takes the pairs as ivars. Keys
618
+ # are @-normalized both ways: stdlib DUMPS the ivars @-stripped
619
+ # (yaml_tree's dump_ivars), older psych and hand-written docs
620
+ # spell them WITH the @ — both map to the same ivar.
621
+ def init_with_state(obj, h, node)
622
+ if obj.respond_to?(:init_with)
521
623
  coder = ::Yeptris::Psych::CoderShim.new
522
- if v.is_a?(::Yeptris::Psych::Nodes::Mapping)
523
- v.children.each_slice(2) do |ck, cv|
524
- coder[ck.to_ruby.to_s] = visit(cv)
525
- end
526
- end
624
+ coder.tag = node.tag
625
+ h.each { |k, v| coder[k] = v }
527
626
  obj.init_with(coder)
627
+ else
628
+ h.each do |k, v|
629
+ obj.instance_variable_set(:"@#{k.to_s.sub(/\A@/, "")}", v)
630
+ end
528
631
  end
529
632
  obj
530
633
  end
data/lib/yeptris/psych.rb CHANGED
@@ -208,11 +208,14 @@ module Yeptris
208
208
 
209
209
  def safe_load(yaml, permitted_classes: [::Date, ::Time], aliases: false,
210
210
  symbolize_names: false, **)
211
+ # ONE read at the boundary (read_input): IO-like input must be
212
+ # consumed once — the string rides parse AND the slice below
213
+ source = ::Yeptris.read_input(yaml)
211
214
  begin
212
- doc = Yeptris::Document.parse(yaml, schema: :compat_11)
215
+ doc = Yeptris::Document.parse(source, schema: :compat_11)
213
216
  return nil if doc.nil? # the legal empty stream
214
217
 
215
- res = force_utf8_scalars(walk_safe(doc.root(0), permitted_classes, aliases))
218
+ res = force_utf8_scalars(walk_safe(doc.root(0), permitted_classes, aliases, source))
216
219
  res = deep_symbolize_keys(res) if symbolize_names
217
220
  res
218
221
  rescue ::Yeptris::ParseError => e
@@ -290,23 +293,78 @@ module Yeptris
290
293
  ::Yeptris::Psych.dump(obj, io, options)
291
294
  end
292
295
 
293
- def load_stream(yaml, **kwargs, &block)
294
- # materialize each document's root directly — the stream
295
- # children share one C document, so their handles would all
296
- # resolve to the first document's tree. stdlib's block form
297
- # yields each loaded document's object (#179 round 4).
298
- doc = Yeptris::Document.parse(yaml, schema: :compat_11)
299
- return nil if doc.nil? # the legal empty stream
300
-
301
- begin
302
- docs = (0...doc.document_count).map { |i| doc.root(i).to_ruby }
303
- ensure
304
- doc.free
296
+ # stdlib's load_stream (yeptris#881): every document of the
297
+ # stream, in order — the block form yields each loaded document.
298
+ # The engine's stream parse drives the boundaries (--- / ... /
299
+ # implicit documents after a ...); empty documents are nil
300
+ # elements, exactly what stdlib yields.
301
+ #
302
+ # errors: :raise (default) re-raises a bad document as stdlib's
303
+ # family — Psych::SyntaxError (parse() translates the same way).
304
+ # errors: :collect never raises for a bad document: the stream
305
+ # re-walks at marker boundaries (Yeptris::YAML._stream_slices)
306
+ # through this face's own DOM materialization, and the bad
307
+ # document's position carries the SyntaxError marker.
308
+ def load_stream(yaml, errors: :raise, **kwargs, &block)
309
+ unless %i[raise collect].include?(errors)
310
+ raise ArgumentError, "errors: must be :raise or :collect (got #{errors.inspect})"
305
311
  end
312
+
313
+ # once, up front: the collect fallback re-parses this text and
314
+ # an IO input would already be at EOF by then
315
+ yaml = ::Yeptris.read_input(yaml).to_s
316
+ docs =
317
+ begin
318
+ # materialize each document's root directly — the stream
319
+ # children share one C document, so their handles would all
320
+ # resolve to the first document's tree. stdlib's block form
321
+ # yields each loaded document's object (#179 round 4).
322
+ doc = ::Yeptris::Document.parse(yaml, schema: :compat_11)
323
+ if doc.nil?
324
+ # the legal empty stream: stdlib answers a stream object
325
+ # with no children; the array face answers []
326
+ []
327
+ else
328
+ begin
329
+ (0...doc.document_count).map { |i| doc.root(i).to_ruby }
330
+ ensure
331
+ doc.free
332
+ end
333
+ end
334
+ rescue ::Yeptris::ParseError => e
335
+ # stdlib's family for a bad document (parse() translates
336
+ # the same way); in collect mode the bad document's
337
+ # position carries the SyntaxError marker instead
338
+ raise SyntaxError.from_parse_error(e) if errors != :collect
339
+
340
+ _collect_stream(yaml)
341
+ end
306
342
  docs.each { |d| block.call(d) } if block
307
343
  docs
308
344
  end
309
345
 
346
+ # The errors: :collect fallback: parse the marker-boundary
347
+ # slices through this face's own DOM materialization; a failed
348
+ # slice's position carries the SyntaxError marker.
349
+ def _collect_stream(yaml)
350
+ results = []
351
+ ::Yeptris::YAML._stream_slices(yaml).each do |slice|
352
+ begin
353
+ doc = ::Yeptris::Document.parse(slice, schema: :compat_11)
354
+ next if doc.nil?
355
+
356
+ begin
357
+ (0...doc.document_count).each { |i| results << doc.root(i).to_ruby }
358
+ ensure
359
+ doc.free
360
+ end
361
+ rescue ::Yeptris::ParseError => e
362
+ results << SyntaxError.from_parse_error(e)
363
+ end
364
+ end
365
+ results
366
+ end
367
+
310
368
  # safe_load_stream: stdlib's surface — the stream form of
311
369
  # safe_load. Our load_stream is already safe-by-default
312
370
  # (Psych 5 semantics), so this is the yielding wrapper.
@@ -406,7 +464,25 @@ module Yeptris
406
464
  # class outside the permitted set raises too (issue #69: a
407
465
  # compat_11 date must not become a Date unless Date is
408
466
  # permitted — Psych::DisallowedClass semantics).
409
- def walk_safe(root, permitted, aliases_enabled)
467
+ #
468
+ # The single-pass slice (#862): the gate walk IS the cost —
469
+ # per-node FFI reads. Three provable facts retire it for clean
470
+ # documents: the parser cannot build an alias without a `*`
471
+ # byte in the source, cannot build an explicit tag without a
472
+ # `!` byte, and the marshal builder REFUSES timestamps and
473
+ # merge keys (its success proves none exist — every
474
+ # timestamp-typed shape is refused, verified per shape). So
475
+ # `marshal_fast` doubles as the probe AND the build in one C
476
+ # call: on a gate-free source its success proves check() could
477
+ # not have raised, and the walk disappears; a refusal or a gate
478
+ # byte falls back to the checked walk, which is byte-for-byte
479
+ # the old path.
480
+ def walk_safe(root, permitted, aliases_enabled, source = nil)
481
+ if root && !source.nil? &&
482
+ !source.include?("*") && !source.include?("!")
483
+ fast = root.marshal_fast
484
+ return fast unless fast.nil?
485
+ end
410
486
  check(root, permitted, aliases_enabled) if root
411
487
  root.to_ruby
412
488
  end
data/lib/yeptris/yaml.rb CHANGED
@@ -44,11 +44,100 @@ module Yeptris
44
44
  Psych.safe_load(yaml.to_s, permitted_classes: permitted_classes, aliases: aliases)
45
45
  end
46
46
 
47
- # Every document in the stream, in order.
48
- def load_stream(yaml, schema: :compat_11)
47
+ # Every document in the stream, in order (the multi-document stream
48
+ # API, yeptris#881 — Psych's load_stream parity). Document boundary
49
+ # detection follows the engine's stream parse: explicit --- / ...
50
+ # markers, implicit documents after a ... terminator, and empty
51
+ # documents — nil elements, exactly what Psych yields for them.
52
+ #
53
+ # errors: :raise (default) raises Yeptris::ParseError on the first
54
+ # bad document — psych's behavior (a stream parse cannot resume
55
+ # past it). errors: :collect never raises for a bad document: the
56
+ # text re-walks at marker boundaries (see _stream_slices) and the
57
+ # bad document's position carries a Yeptris::ParseError marker
58
+ # (skip with results.grep(Yeptris::ParseError)). A stream that
59
+ # parses whole collects to the same plain document array as :raise.
60
+ #
61
+ # With a block, every element — documents and, under :collect,
62
+ # error markers — is yielded in order; the array is returned.
63
+ def load_stream(yaml, schema: :compat_11, errors: :raise, &block)
64
+ unless %i[raise collect].include?(errors)
65
+ raise ArgumentError, "errors: must be :raise or :collect (got #{errors.inspect})"
66
+ end
67
+
49
68
  yaml = Yeptris.read_input(yaml)
50
69
  yaml = yaml.to_s
51
- _drain_all(yaml, schema)
70
+ docs =
71
+ begin
72
+ docs = _drain_all(yaml, schema)
73
+ # The value drain emits a nil record even for a stream with
74
+ # ZERO documents ("", a blank line, a comment) — stdlib's
75
+ # load_stream answers zero documents there, and the corner
76
+ # must not mask a real empty document ("---\n" IS [nil]).
77
+ # Only the [nil] shape needs the discriminating DOM count,
78
+ # so the good path pays nothing.
79
+ if docs == [nil]
80
+ doc = Document.parse(yaml, schema: schema)
81
+ docs = [] if doc.nil? || doc.document_count.zero?
82
+ doc&.free
83
+ end
84
+ docs
85
+ rescue ParseError
86
+ raise if errors != :collect
87
+
88
+ _collect_stream(yaml, schema)
89
+ end
90
+ docs.each { |d| block.call(d) } if block
91
+ docs
92
+ end
93
+
94
+ # A column-0 --- / ... marker line. A real document boundary in
95
+ # every valid YAML stream (block/scalar content can never carry one
96
+ # unindented), so the scan is exact — it never splits inside a
97
+ # document. Marker-less boundaries (implicit documents after a
98
+ # ...) stay INSIDE their slice, and each slice parses through the
99
+ # full stream face, so nothing is lost there either. Slices keep
100
+ # their marker lines so every slice parses standalone. Used only by
101
+ # the errors: :collect fallback — a stream that parses whole never
102
+ # splits (the fallback's lossy cases are the whole reason #881
103
+ # exists; the good path must stay exact).
104
+ STREAM_MARKER = /\A(?:---|\.\.\.)(?:[ \t]|\r?\n|\z)/
105
+
106
+ # The marker-delimited slices of a stream. A slice that began at a
107
+ # marker line always survives (a trailing bare `---` is an empty
108
+ # document — nil, like psych yields); only a comments/blank-only
109
+ # leading prefix (no document) drops.
110
+ def _stream_slices(yaml)
111
+ slices = []
112
+ current = +""
113
+ markered = false
114
+ yaml.each_line do |line|
115
+ if line.match?(STREAM_MARKER)
116
+ slices << current if markered || !current.strip.empty?
117
+ current = line.dup
118
+ markered = true
119
+ else
120
+ current << line
121
+ end
122
+ end
123
+ slices << current if markered || !current.strip.empty?
124
+ slices
125
+ end
126
+
127
+ # The errors: :collect fallback: parse marker-boundary slices
128
+ # through the full stream face; a failed slice's position carries
129
+ # the ParseError. Documents before the bad one still load — the
130
+ # per-document rescue-and-skip contract, centralized.
131
+ def _collect_stream(yaml, schema)
132
+ results = []
133
+ _stream_slices(yaml).each do |slice|
134
+ begin
135
+ results.concat(_drain_all(slice, schema))
136
+ rescue ParseError => e
137
+ results << e
138
+ end
139
+ end
140
+ results
52
141
  end
53
142
 
54
143
  # The Marshal fast path when the loaded libyeptris has it (>= 0.1.11
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.56.1".freeze
6
+ VERSION = "0.6.57.1".freeze
7
7
 
8
8
  # Informational notices are OPT-IN (yeptris-ruby#217): consumers
9
9
  # embed yeptris inside CLIs that assert clean stderr, and a
data/libyeptris.so CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.56
6
+ VERSION 0.6.57
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -141,6 +141,13 @@ YEPTRIS_API YeptrisStatus yeptris_recorder_feed(YeptrisRecorder rec, const char*
141
141
  * the next feed; reset at every feed entry (drain per chunk). */
142
142
  YEPTRIS_API const YeptrisEventRecord* yeptris_recorder_records(YeptrisRecorder rec, size_t* count);
143
143
  YEPTRIS_API const char* yeptris_recorder_arena(YeptrisRecorder rec, size_t* len);
144
+
145
+ /* Status of the last feed with its 1-based error position (0 = the
146
+ * failure carries no position, e.g. a capture allocation failure).
147
+ * Mirror of yeptris_pull_status / yeptris_iterparse_status. */
148
+ YEPTRIS_API YeptrisStatus yeptris_recorder_status(const YeptrisRecorder rec, uint32_t* line,
149
+ uint32_t* col);
150
+
144
151
  YEPTRIS_API void yeptris_recorder_free(YeptrisRecorder rec);
145
152
 
146
153
  /* ---- iterparse -------------------------------------------------------- */
@@ -26,16 +26,35 @@ static int cap_arena(const yep_dom* d, uint32_t need) {
26
26
  return d->input_len == 0 || (size_t)(d->str_len + need) <= d->input_len * 128 + (1u << 20);
27
27
  }
28
28
 
29
+ /* Doubling with the uint32 ceiling (the audit's wrap finding): the old
30
+ * `while (cap < target) cap *= 2` wrapped to 0 and span forever once a
31
+ * cap crossed 2^31 with a target above it. A cap that cannot double
32
+ * inside uint32 — or a target past the 32-bit space at all — is an
33
+ * alloc failure, never a wrapped capacity. */
34
+ static int dom_cap_next(uint32_t cap, size_t target, uint32_t* out) {
35
+ if (target > UINT32_MAX) {
36
+ return 0;
37
+ }
38
+ while (cap < target) {
39
+ if (cap > UINT32_MAX / 2) {
40
+ return 0;
41
+ }
42
+ cap *= 2;
43
+ }
44
+ *out = cap;
45
+ return 1;
46
+ }
47
+
29
48
  int dom_grow_nodes(yep_dom* d, uint32_t need) {
30
- if (d->ncount + need <= d->ncap) {
49
+ if ((size_t)d->ncount + need <= d->ncap) {
31
50
  return 1;
32
51
  }
33
52
  if (!cap_nodes(d)) {
34
53
  return 0;
35
54
  }
36
- uint32_t ncap = d->ncap ? d->ncap * 2 : 64;
37
- while (ncap < d->ncount + need) {
38
- ncap *= 2;
55
+ uint32_t ncap;
56
+ if (!dom_cap_next(d->ncap ? d->ncap : 64, (size_t)d->ncount + need, &ncap)) {
57
+ return 0;
39
58
  }
40
59
  yep_dnode* nd = yep_pool_alloc(d->pool, (size_t)ncap * sizeof(yep_dnode), 16);
41
60
  if (nd == NULL) {
@@ -50,15 +69,15 @@ int dom_grow_nodes(yep_dom* d, uint32_t need) {
50
69
  }
51
70
 
52
71
  int dom_grow_docs(yep_dom* d, uint32_t need) {
53
- if (d->dcount + need <= d->dcap) {
72
+ if ((size_t)d->dcount + need <= d->dcap) {
54
73
  return 1;
55
74
  }
56
75
  if (d->input_len != 0 && (size_t)d->dcount > d->input_len / 2 + 16) {
57
76
  return 0;
58
77
  }
59
- uint32_t ncap = d->dcap ? d->dcap * 2 : 16;
60
- while (ncap < d->dcount + need) {
61
- ncap *= 2;
78
+ uint32_t ncap;
79
+ if (!dom_cap_next(d->dcap ? d->dcap : 16, (size_t)d->dcount + need, &ncap)) {
80
+ return 0;
62
81
  }
63
82
  uint32_t* nd = yep_pool_alloc(d->pool, (size_t)ncap * sizeof(uint32_t), 16);
64
83
  if (nd == NULL) {
@@ -75,15 +94,15 @@ int dom_grow_docs(yep_dom* d, uint32_t need) {
75
94
  /* Doubles the arena until cap bytes fit; 0 on OOM. Offsets stored in
76
95
  * nodes stay valid across the move. */
77
96
  static int str_grow(yep_dom* d, uint32_t need) {
78
- if (d->str_len + need <= d->str_cap) {
97
+ if ((size_t)d->str_len + need <= d->str_cap) {
79
98
  return 1;
80
99
  }
81
100
  if (!cap_arena(d, need)) {
82
101
  return 0;
83
102
  }
84
- uint32_t cap = d->str_cap ? d->str_cap : 256;
85
- while (cap < d->str_len + need) {
86
- cap *= 2;
103
+ uint32_t cap;
104
+ if (!dom_cap_next(d->str_cap ? d->str_cap : 256, (size_t)d->str_len + need, &cap)) {
105
+ return 0;
87
106
  }
88
107
  char* ns = yep_alloc(d->sys, cap);
89
108
  if (ns == NULL) {
@@ -100,8 +119,15 @@ static int str_grow(yep_dom* d, uint32_t need) {
100
119
 
101
120
  yep_sview yep_dom_str_put(yep_dom* d, const char* p, uint32_t len) {
102
121
  yep_sview sv = {YEP_SV_INPUT, 0}; /* empty: arena offset 0 */
103
- if (len == 0 || !str_grow(d, len)) {
104
- return sv; /* len 0 or OOM: empty view */
122
+ if (len == 0) {
123
+ return sv;
124
+ }
125
+ if (!str_grow(d, len)) {
126
+ /* OOM: an empty view here would silently corrupt the document
127
+ * (the value reads as ""). Record it; dom_place/dom_open_node
128
+ * fail the parse with the memory status. */
129
+ d->oom = 1;
130
+ return sv;
105
131
  }
106
132
  memcpy(d->str + d->str_len, p, len);
107
133
  sv.off = YEP_SV_INPUT | d->str_len;
@@ -172,6 +198,11 @@ uint32_t dom_open_node(yep_dom* d, uint8_t kind, const yep_view* tag, const yep_
172
198
  n->kind = kind;
173
199
  n->tag = dom_str_in(d, tag, 0);
174
200
  n->anchor = dom_str_in(d, anchor, anchor_borrowed);
201
+ if (d->oom) {
202
+ /* the tag/anchor arena write failed: refuse the node (the slot
203
+ * is uncommitted — the next open re-initializes it) */
204
+ return UINT32_MAX;
205
+ }
175
206
  n->style = style;
176
207
  n->implicit = implicit;
177
208
  n->flow = flow;
@@ -213,8 +244,8 @@ static int dom_anchor_set(yep_dom* d, uint32_t ordinal, uint32_t node) {
213
244
  cap = sized;
214
245
  }
215
246
  }
216
- while (cap < ordinal) {
217
- cap *= 2;
247
+ if (!dom_cap_next(cap, ordinal, &cap)) {
248
+ return -1; /* the wrap-checked doubling (audit round 5) */
218
249
  }
219
250
  uint32_t* na = yep_alloc(d->sys, cap * sizeof(*na));
220
251
  if (na == NULL) {
@@ -271,9 +302,9 @@ static int dom_mut_grow(yep_dom* d, uint32_t need) {
271
302
  if (need <= d->mut_tbl_cap) {
272
303
  return 1;
273
304
  }
274
- uint32_t cap = d->mut_tbl_cap ? d->mut_tbl_cap : 64;
275
- while (cap < need) {
276
- cap *= 2;
305
+ uint32_t cap;
306
+ if (!dom_cap_next(d->mut_tbl_cap ? d->mut_tbl_cap : 64, need, &cap)) {
307
+ return 0; /* the wrap-checked doubling (audit round 5) */
277
308
  }
278
309
  uint8_t* na = yep_alloc(d->sys, (size_t)cap / 8 + 1);
279
310
  uint16_t* nd = yep_alloc(d->sys, (size_t)cap * sizeof(uint16_t));
@@ -335,8 +366,13 @@ void dom_mut_set_depth(yep_dom* d, uint32_t id, uint16_t depth) {
335
366
  }
336
367
 
337
368
  /* Places a completed node: value for a pending key, child of the top
338
- * collection, or document root. */
369
+ * collection, or document root. Every builder path ends here, so it is
370
+ * also the OOM choke: an arena write that failed (d->oom) must fail
371
+ * the node, never leave a silently-empty value in the tree. */
339
372
  int dom_place(yep_dom* d, uint32_t id) {
373
+ if (d->oom) {
374
+ return -1;
375
+ }
340
376
  if (d->depth == 0) {
341
377
  if (!dom_grow_docs(d, 1)) {
342
378
  return -1;
@@ -119,6 +119,11 @@ typedef struct yep_dom {
119
119
  * OFFSETS into it, so growth never dangles (dom_str_put) */
120
120
  char* str;
121
121
  uint32_t str_len, str_cap;
122
+ /* an arena write failed (str_put under OOM): the value would have
123
+ * silently become the empty view — doc corruption. Sticky; every
124
+ * builder path checks it at dom_place/dom_open_node and fails the
125
+ * parse with the memory status. */
126
+ int oom;
122
127
  const char* input_base; /* borrowed parse input (offset base) */
123
128
  /* handle arena: thread-safe (atomic bump) — query APIs allocate
124
129
  * YeptrisNode wrappers here, so read-only document sharing across
@@ -11,12 +11,20 @@
11
11
 
12
12
  #include "../../include/yeptris/events.h"
13
13
  #include "../common/simd_text.h"
14
+ #include "../encoding/encoding.h"
15
+ #include "../memory/allocator.h"
14
16
  #include "../parse/engine.h"
15
17
  #include "capture.h"
16
18
 
17
19
  typedef struct {
18
20
  yep_rec_store* store;
19
21
  int doc_ends; /* DOCUMENT_END events seen this run */
22
+ int oom; /* the capture failed: yep_rec_on_event returned
23
+ * nonzero — the engine's rc (-2) is shared with the
24
+ * normal document-abort, so without this bit an OOM
25
+ * was indistinguishable from doc-complete and the
26
+ * final event vanished silently (the audit's
27
+ * finding) */
20
28
  } yep_iter_sink;
21
29
 
22
30
  static int iter_on_event(void* ctx, const yep_event* ev) {
@@ -31,6 +39,7 @@ static int iter_on_event(void* ctx, const yep_event* ev) {
31
39
  }
32
40
  int rc = yep_rec_on_event(s->store, ev);
33
41
  if (rc != 0) {
42
+ s->oom = 1;
34
43
  return rc;
35
44
  }
36
45
  if (s->doc_ends >= 1) {
@@ -44,6 +53,11 @@ struct yeptris_iterparse {
44
53
  size_t len;
45
54
  size_t cursor;
46
55
  int done;
56
+ int gated; /* the encoding front-end ran once: after it, buf
57
+ * indexes the space every engine run's pos lives
58
+ * in (the resume arithmetic requires it) */
59
+ unsigned char* owned; /* UTF-16/32 transcode output (buf moved) */
60
+ char* norm; /* NEL/LS/PS-normalized copy (buf moved) */
47
61
  int emitted_stream_start;
48
62
  int pending_stream_end; /* deliver a final lone STREAM_END slice */
49
63
  YeptrisStatus status;
@@ -76,6 +90,55 @@ YEPTRIS_API const YeptrisEvent* yeptris_iterparse_next(YeptrisIterparse it, size
76
90
  return NULL;
77
91
  }
78
92
  for (;;) {
93
+ /* the encoding front-end, ONCE (the yep_engine_run law): the
94
+ * resume arithmetic below adds the engine's pos to it->cursor,
95
+ * and pos lives in the buffer space the RUN parsed — never the
96
+ * raw bytes. The front-end's consumed prefix broke that twice:
97
+ * the BOM it strips (pos cannot see it, so a BOM'd stream
98
+ * re-parsed the previous document's tail), and the UTF-16/32
99
+ * transcode (pos counts UTF-8 bytes, cursor counts raw ones —
100
+ * slices corrupted). The NEL/LS/PS break normalizer inside the
101
+ * run shifts pos the same way, so the gate owns both: encode
102
+ * to UTF-8 and normalize here, move buf, and the spaces agree. */
103
+ if (!it->gated) {
104
+ it->gated = 1;
105
+ const yep_allocator* sys = yep_system_allocator();
106
+ yep_encoding enc = YEP_ENC_UNKNOWN;
107
+ size_t bom = yep_bom_sniff((const unsigned char*)it->buf, it->len, &enc);
108
+ if (enc == YEP_ENC_UTF16LE || enc == YEP_ENC_UTF16BE || enc == YEP_ENC_UTF32LE ||
109
+ enc == YEP_ENC_UTF32BE) {
110
+ size_t tlen = 0;
111
+ size_t terr = 0;
112
+ int rc = yep_transcode_to_utf8(sys, enc, (const unsigned char*)it->buf + bom,
113
+ it->len - bom, &it->owned, &tlen, &terr);
114
+ if (rc != 0) {
115
+ it->done = 1;
116
+ it->status = YEPTRIS_ERROR_ENCODING;
117
+ it->err_line = 1;
118
+ it->err_col = 1;
119
+ return NULL;
120
+ }
121
+ /* the copy IS the post-gate buffer: it opens at the
122
+ * first content byte (the mark never entered it), so
123
+ * cursor restarts at zero — keeping `bom` here skipped
124
+ * the copy's first unit and corrupted the first slice */
125
+ it->buf = (const char*)it->owned;
126
+ it->len = tlen;
127
+ it->cursor = 0;
128
+ } else {
129
+ it->cursor = bom; /* the mark is the front-end's prefix */
130
+ }
131
+ size_t nlen = 0;
132
+ int kept_raw = 0;
133
+ char* norm = yep_engine_normalize_breaks(sys, it->buf + it->cursor,
134
+ it->len - it->cursor, &nlen, &kept_raw);
135
+ if (norm != NULL) {
136
+ it->norm = norm;
137
+ it->buf = norm;
138
+ it->len = nlen;
139
+ it->cursor = 0; /* the copy starts at its own zero */
140
+ }
141
+ }
79
142
  if (it->done && it->pending_stream_end && it->next >= it->store.n) {
80
143
  it->pending_stream_end = 0;
81
144
  if (!it->emitted_stream_start) {
@@ -133,7 +196,7 @@ YEPTRIS_API const YeptrisEvent* yeptris_iterparse_next(YeptrisIterparse it, size
133
196
  * document's start on the resumed run */
134
197
  yep_rec_reset(&it->store);
135
198
  it->next = 0;
136
- yep_iter_sink s = {&it->store, 0};
199
+ yep_iter_sink s = {&it->store, 0, 0};
137
200
  yep_engine* eng = yep_engine_create(yep_system_allocator());
138
201
  if (eng == NULL) {
139
202
  it->done = 1;
@@ -153,15 +216,39 @@ YEPTRIS_API const YeptrisEvent* yeptris_iterparse_next(YeptrisIterparse it, size
153
216
  yep_engine_prepare(eng, &pst);
154
217
  int rc = yep_engine_run(eng, it->buf + it->cursor, it->len - it->cursor, &sink);
155
218
  size_t pos = yep_engine_pos(eng);
156
- const yep_error* err = yep_engine_error(eng);
157
- if (rc == -1 && err != NULL) {
158
- it->err_line = err->line;
159
- it->err_col = err->col;
219
+ /* the error slot lives inside the engine and destroy munmaps it
220
+ * (glibc serves the ~260KB engine with mmap): copy the verdict
221
+ * out BEFORE the free */
222
+ yep_err_code err_code = YEP_ERR_NONE;
223
+ uint32_t err_line = 0, err_col = 0;
224
+ {
225
+ const yep_error* err = yep_engine_error(eng);
226
+ if (err != NULL) {
227
+ err_code = err->code;
228
+ err_line = err->line;
229
+ err_col = err->col;
230
+ }
160
231
  }
161
232
  yep_engine_destroy(eng);
162
- if (rc == -1) {
233
+ /* A failed run with a SET channel is a parse-class error and the
234
+ * CODE picks the public status; a failed run with a CLEAN
235
+ * channel is the capture sink dying — memory, never a parse
236
+ * error, and never a silent stream end (the old code fell into
237
+ * the doc-complete arm on rc == -2 and the final event
238
+ * vanished). The abort (rc == -2 without s.oom) stays the
239
+ * normal per-document completion. */
240
+ int failed = rc == -1 || s.oom;
241
+ if (failed) {
163
242
  it->done = 1;
164
- it->status = err != NULL ? YEPTRIS_ERROR_PARSE : YEPTRIS_ERROR_MEMORY;
243
+ if (err_code != YEP_ERR_NONE) {
244
+ it->err_line = err_line;
245
+ it->err_col = err_col;
246
+ it->status = err_code == YEP_ERR_MEMORY ? YEPTRIS_ERROR_MEMORY
247
+ : err_code == YEP_ERR_ENCODING ? YEPTRIS_ERROR_ENCODING
248
+ : YEPTRIS_ERROR_PARSE;
249
+ } else {
250
+ it->status = YEPTRIS_ERROR_MEMORY;
251
+ }
165
252
  if (it->store.n == 0) {
166
253
  if (!it->emitted_stream_start) {
167
254
  /* the engine emitted +STR before failing: match it */
@@ -202,6 +289,9 @@ YEPTRIS_API void yeptris_iterparse_free(YeptrisIterparse it) {
202
289
  if (it == NULL) {
203
290
  return;
204
291
  }
292
+ const yep_allocator* sys = yep_system_allocator();
293
+ yep_free(sys, it->owned);
294
+ yep_free(sys, it->norm);
205
295
  yep_rec_free(&it->store);
206
296
  free(it->mat);
207
297
  free(it);
@@ -44,13 +44,33 @@ YEPTRIS_API YeptrisPullParser yeptris_pull_new(const char* buf, size_t len) {
44
44
  yep_text_active()->scan_stats(buf, len, &pst);
45
45
  yep_engine_prepare(eng, &pst);
46
46
  int rc = yep_engine_run(eng, buf, len, &sink);
47
- const yep_error* err = yep_engine_error(eng);
48
- if (err != NULL && rc != 0) {
49
- p->err_line = err->line;
50
- p->err_col = err->col;
47
+ /* the error slot lives INSIDE the engine, and the engine is a
48
+ * ~260KB allocation glibc serves with mmap — destroy munmaps it,
49
+ * so the slot's verdict must be copied out BEFORE destroy (the
50
+ * round-5 CI segfaulted reading err->code after the free). */
51
+ yep_err_code err_code = YEP_ERR_NONE;
52
+ uint32_t err_line = 0, err_col = 0;
53
+ {
54
+ const yep_error* err = yep_engine_error(eng);
55
+ if (err != NULL) {
56
+ err_code = err->code;
57
+ err_line = err->line;
58
+ err_col = err->col;
59
+ }
51
60
  }
52
61
  yep_engine_destroy(eng);
53
- p->status = rc == 0 ? YEPTRIS_OK : YEPTRIS_ERROR_PARSE;
62
+ /* the error CODE decides the public status — the parse route's law
63
+ * (the recorder twin carries the full comment). A clean channel
64
+ * under a failed run is the capture sink failing: memory. */
65
+ if (rc != 0 && err_code != YEP_ERR_NONE) {
66
+ p->err_line = err_line;
67
+ p->err_col = err_col;
68
+ p->status = err_code == YEP_ERR_MEMORY ? YEPTRIS_ERROR_MEMORY
69
+ : err_code == YEP_ERR_ENCODING ? YEPTRIS_ERROR_ENCODING
70
+ : YEPTRIS_ERROR_PARSE;
71
+ } else {
72
+ p->status = rc == 0 ? YEPTRIS_OK : YEPTRIS_ERROR_MEMORY;
73
+ }
54
74
  return p;
55
75
  }
56
76
 
@@ -61,6 +61,12 @@ static int push_on_event(void* ctx, const yep_event* ev) {
61
61
  e.flow = ev->flow;
62
62
  e.line = ev->line;
63
63
  e.col = ev->col;
64
+ /* the cross-model identity law (#179, test_event_marks): push,
65
+ * pull and the recorder emit identical mark data — dropping the
66
+ * end marks here made the push route's events half-marked where
67
+ * pull/recorder carried all four fields */
68
+ e.end_line = ev->end_line;
69
+ e.end_col = ev->end_col;
64
70
  e.value = ev->value.len > 0 ? (const char*)ev->value.p : NULL;
65
71
  e.value_len = ev->value.len;
66
72
  e.anchor = ev->anchor.len > 0 ? (const char*)ev->anchor.p : NULL;
@@ -95,10 +101,26 @@ YEPTRIS_API YeptrisStatus yeptris_push_parse(const char* buf, size_t len, yeptri
95
101
  yep_text_active()->scan_stats(buf, len, &pst);
96
102
  yep_engine_prepare(eng, &pst);
97
103
  int rc = yep_engine_run(eng, buf, len, &sink);
104
+ /* the error slot lives inside the engine and destroy munmaps it
105
+ * (glibc serves the ~260KB engine with mmap): copy the verdict out
106
+ * BEFORE the free — reading err->code after destroy segfaulted the
107
+ * round-5 CI */
108
+ yep_err_code err_code = YEP_ERR_NONE;
109
+ {
110
+ const yep_error* err = yep_engine_error(eng);
111
+ if (err != NULL) {
112
+ err_code = err->code;
113
+ }
114
+ }
98
115
  yep_engine_destroy(eng);
99
116
  if (rc == 0) {
100
117
  return YEPTRIS_OK;
101
118
  }
102
- /* -2: the callback aborted — reported as a parse stop, not a bug */
119
+ /* the engine's own memory failures report MEMORY (the parse route's
120
+ * law); the callback abort keeps its documented contract — a parse
121
+ * stop, clean channel */
122
+ if (err_code == YEP_ERR_MEMORY) {
123
+ return YEPTRIS_ERROR_MEMORY;
124
+ }
103
125
  return YEPTRIS_ERROR_PARSE;
104
126
  }
@@ -37,6 +37,23 @@ YEPTRIS_API YeptrisRecorder yeptris_recorder_new_ex(YeptrisSchema schema) {
37
37
  return r;
38
38
  }
39
39
 
40
+ /* The engine's error code decides the public status — the parse
41
+ * route's law (parse.c maps YEP_ERR_* to the pinned codes). A clean
42
+ * channel means the SINK failed: the capture ran out of memory, the
43
+ * bytes never were a parse error. The MEMORY arm: the engine's own
44
+ * allocation failures (e_fail(YEP_ERR_MEMORY)) used to read as a parse
45
+ * error here — yep_engine_error always returns the slot, so the old
46
+ * `err != NULL` discriminator could never fire (the audit's finding). */
47
+ static YeptrisStatus rec_status_from_err(const yep_error* err) {
48
+ if (err == NULL || err->code == YEP_ERR_NONE || err->code == YEP_ERR_MEMORY) {
49
+ return YEPTRIS_ERROR_MEMORY;
50
+ }
51
+ if (err->code == YEP_ERR_ENCODING) {
52
+ return YEPTRIS_ERROR_ENCODING;
53
+ }
54
+ return YEPTRIS_ERROR_PARSE;
55
+ }
56
+
40
57
  YEPTRIS_API YeptrisStatus yeptris_recorder_feed(YeptrisRecorder rec, const char* chunk, size_t len,
41
58
  int final) {
42
59
  if (rec == NULL || (chunk == NULL && len != 0)) {
@@ -71,15 +88,11 @@ YEPTRIS_API YeptrisStatus yeptris_recorder_feed(YeptrisRecorder rec, const char*
71
88
  int rc = yep_engine_step(rec->eng, chunk, len, final, &sink);
72
89
  if (rc != 0) {
73
90
  const yep_error* err = yep_engine_error(rec->eng);
74
- if (err != NULL) {
91
+ if (err != NULL && err->code != YEP_ERR_NONE) {
75
92
  rec->err_line = err->line;
76
93
  rec->err_col = err->col;
77
- rec->status = YEPTRIS_ERROR_PARSE;
78
- } else {
79
- /* a clean error channel means the SINK failed: the capture
80
- * ran out of memory, the bytes never were a parse error */
81
- rec->status = YEPTRIS_ERROR_MEMORY;
82
94
  }
95
+ rec->status = rec_status_from_err(err);
83
96
  rec->final_fed = 1; /* a failed feed kills the stream */
84
97
  return rec->status;
85
98
  }
@@ -107,6 +120,20 @@ YEPTRIS_API const char* yeptris_recorder_arena(YeptrisRecorder rec, size_t* len)
107
120
  return rec->store.arena;
108
121
  }
109
122
 
123
+ YEPTRIS_API YeptrisStatus yeptris_recorder_status(const YeptrisRecorder rec, uint32_t* line,
124
+ uint32_t* col) {
125
+ if (rec == NULL) {
126
+ return YEPTRIS_ERROR_ARG;
127
+ }
128
+ if (line != NULL) {
129
+ *line = rec->err_line;
130
+ }
131
+ if (col != NULL) {
132
+ *col = rec->err_col;
133
+ }
134
+ return rec->status;
135
+ }
136
+
110
137
  YEPTRIS_API void yeptris_recorder_free(YeptrisRecorder rec) {
111
138
  if (rec == NULL) {
112
139
  return;
@@ -98,7 +98,15 @@ void* yep_arena_alloc(yep_arena* arena, size_t size, size_t align) {
98
98
  uintptr_t base = (uintptr_t)b + sizeof(yep_arena_block);
99
99
  uintptr_t start = yep_round_up(base + b->used, align);
100
100
 
101
- if (start + size > base + b->size) {
101
+ /* the fit test in subtraction form (the audit's finding): the old
102
+ * `start + size > base + b->size` wraps when size rides past the
103
+ * pointer space and the request "fits". The limit-first clause
104
+ * covers the round_up overshoot — start can sit up to align-1
105
+ * bytes past the limit, where `limit - start` would underflow — so
106
+ * both comparisons are wrap-free without leaning on the
107
+ * hostile-size guard above. */
108
+ uintptr_t limit = base + b->size;
109
+ if (start >= limit || size > limit - start) {
102
110
  /* Amortized doubling: each new block is at least twice the last,
103
111
  * unless the request itself is larger. */
104
112
  size_t payload = arena->last_block_size * 2;
@@ -64,7 +64,11 @@ void* yep_pool_alloc(yep_pool* pool, size_t size, size_t align) {
64
64
  uintptr_t base = (uintptr_t)b + sizeof(yep_block);
65
65
  uintptr_t start = yep_round_up(base + b->used, align);
66
66
 
67
- if (start + size > base + b->size) {
67
+ /* the fit test in subtraction form — the arena twin carries the
68
+ * full comment (wrap-free comparisons, round_up overshoot covered
69
+ * by the limit-first clause) */
70
+ uintptr_t limit = base + b->size;
71
+ if (start >= limit || size > limit - start) {
68
72
  /* Dedicated-or-default new block, sized so the aligned request
69
73
  * always fits (malloc returns at least 16-byte-aligned memory, so
70
74
  * the correction is < align). */
@@ -4798,8 +4798,40 @@ typedef struct yep_stepstate {
4798
4798
  * state the whole-stream walk holds at the ---
4799
4799
  * cut, so per-document normalization is
4800
4800
  * byte-identical to the one-buffer walk */
4801
+ int gated; /* the encoding front-end ran: BOM decisions
4802
+ * belong to the stream's first bytes, never to
4803
+ * a mid-stream chunk (a later U+FEFF is
4804
+ * content). While 0 the head may still be a
4805
+ * BOM prefix, and scanned is 0 by the same
4806
+ * law -- every hold returns before step_scan. */
4801
4807
  } yep_stepstate;
4802
4808
 
4809
+ /* True when buf[0..len) could still be the PREFIX of a BOM (the rest
4810
+ * arrives with the next chunk): the sniff needs its full width, so a
4811
+ * 1-3-byte head holds the gate instead of reading a mark's bytes as
4812
+ * content. A buffer long enough for the sniff to decide never gets
4813
+ * here. */
4814
+ static int step_bom_prefix(const char* buf, size_t len) {
4815
+ const unsigned char* p = (const unsigned char*)buf;
4816
+ switch (p[0]) {
4817
+ case 0xEF:
4818
+ return len == 1 || (len == 2 && p[1] == 0xBB);
4819
+ case 0xFF:
4820
+ case 0xFE:
4821
+ return len == 1;
4822
+ case 0x00:
4823
+ if (len == 1) {
4824
+ return 1;
4825
+ }
4826
+ if (p[1] != 0x00) {
4827
+ return 0;
4828
+ }
4829
+ return len == 2 || p[2] == 0xFE;
4830
+ default:
4831
+ return 0;
4832
+ }
4833
+ }
4834
+
4803
4835
  static int engine_run_impl(yep_engine* e, const char* buf, size_t len, const yep_sink* sink,
4804
4836
  int emit_start, int emit_end);
4805
4837
 
@@ -5344,6 +5376,33 @@ int yep_engine_step(yep_engine* e, const char* chunk, size_t len, int final, con
5344
5376
  memcpy(st->buf + st->len, chunk, len);
5345
5377
  st->len += len;
5346
5378
  }
5379
+ /* the encoding front-end the one-shot owns (yep_engine_run): the
5380
+ * stepping route (and the recorder riding it) had NONE, so UTF-16
5381
+ * bytes fed through it parsed garbage silently. The gate runs
5382
+ * once, on the stream's first bytes: the UTF-8 BOM strips (a lone
5383
+ * one is an empty stream, not a U+FEFF scalar -- the one-shot's
5384
+ * round-122 law), a UTF-16/32 BOM rejects (chunk-wise transcoding
5385
+ * cannot carry state across feeds: a surrogate pair split by the
5386
+ * chunk boundary has nowhere to live), and an undecided prefix
5387
+ * holds until enough bytes decide it. */
5388
+ if (!st->gated && st->len > 0) {
5389
+ yep_encoding enc = YEP_ENC_UNKNOWN;
5390
+ size_t bom = yep_bom_sniff((const unsigned char*)st->buf, st->len, &enc);
5391
+ if (enc == YEP_ENC_UTF8) {
5392
+ memmove(st->buf, st->buf + bom, st->len - bom);
5393
+ st->len -= bom;
5394
+ st->gated = 1;
5395
+ } else if (enc == YEP_ENC_UTF16LE || enc == YEP_ENC_UTF16BE || enc == YEP_ENC_UTF32LE ||
5396
+ enc == YEP_ENC_UTF32BE) {
5397
+ yep_error_set(&e->err, YEP_ERR_ENCODING, 1, 1, bom,
5398
+ "the chunked route cannot decode UTF-16/32 input");
5399
+ return -1;
5400
+ } else if (st->len >= 4 || !step_bom_prefix(st->buf, st->len)) {
5401
+ st->gated = 1;
5402
+ } else if (!final) {
5403
+ return 0; /* the head may still complete a BOM: hold */
5404
+ }
5405
+ }
5347
5406
  if (!final) {
5348
5407
  size_t cut = step_scan(st);
5349
5408
  if (cut == 0 || cut >= st->len) {
@@ -437,7 +437,12 @@ engine_enter:
437
437
  }
438
438
  if (rc != 0) {
439
439
  const yep_error* ee = yep_engine_error(eng);
440
- yep_err_code code = ee ? ee->code : YEP_ERR_UNEXPECTED;
440
+ /* a CLEAN channel under a failed run means the SINK failed — a
441
+ * builder ran out of memory (the DOM's oom law), the bytes
442
+ * never were a parse error. Mapping it to PARSE let
443
+ * yep_dom_str_put's swallowed OOM surface as a syntax error
444
+ * (the audit's finding: silent doc corruption reads worse). */
445
+ yep_err_code code = (ee && ee->code != YEP_ERR_NONE) ? ee->code : YEP_ERR_MEMORY;
441
446
  if (ee) {
442
447
  yep_error* tls = yep_error_tls();
443
448
  *tls = *ee;
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.56.1
4
+ version: 0.6.57.1
5
5
  platform: arm-linux-eabihf
6
6
  authors:
7
7
  - Ribose Inc.