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 +4 -4
- data/ext/yeptris_native/version.h +2 -2
- data/lib/yeptris/psych/visitors.rb +123 -20
- data/lib/yeptris/psych.rb +91 -15
- data/lib/yeptris/yaml.rb +92 -3
- data/lib/yeptris.rb +1 -1
- data/libyeptris.so +0 -0
- data/vendor/libyeptris/CMakeLists.txt +1 -1
- data/vendor/libyeptris/src/include/yeptris/events.h +7 -0
- data/vendor/libyeptris/src/yeptris/dom/dom.c +56 -20
- data/vendor/libyeptris/src/yeptris/dom/dom.h +5 -0
- data/vendor/libyeptris/src/yeptris/events/iterparse.c +97 -7
- data/vendor/libyeptris/src/yeptris/events/pull.c +25 -5
- data/vendor/libyeptris/src/yeptris/events/push.c +23 -1
- data/vendor/libyeptris/src/yeptris/events/recorder.c +33 -6
- data/vendor/libyeptris/src/yeptris/memory/arena.c +9 -1
- data/vendor/libyeptris/src/yeptris/memory/pool.c +5 -1
- data/vendor/libyeptris/src/yeptris/parse/engine.c +59 -0
- data/vendor/libyeptris/src/yeptris/parse.c +6 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2ff669641748a920b861c211a4acdcd175c18f0be28b2d917fd81ddcbee85012
|
|
4
|
+
data.tar.gz: df7abbc39687b9d532cd8e49d64c8bb738102dea0b18bb486639933616b2f4de
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
15
|
-
#define YEPTRIS_VERSION_STRING "0.6.
|
|
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
|
-
|
|
457
|
-
|
|
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
|
-
|
|
506
|
-
#
|
|
507
|
-
#
|
|
508
|
-
|
|
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
|
-
|
|
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
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
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
|
-
|
|
523
|
-
|
|
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(
|
|
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
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
@@ -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
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
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
|
|
104
|
-
return sv;
|
|
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
|
-
|
|
217
|
-
|
|
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
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
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
|
-
/*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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;
|