yeptris 0.6.15.2-aarch64-linux → 0.6.16.2-aarch64-linux
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/cbor_ruby.c +22 -2
- data/ext/yeptris_native/yeptris_native.c +43 -0
- data/lib/yeptris/document.rb +17 -0
- data/lib/yeptris/json.rb +8 -4
- data/lib/yeptris/native-3.3.so +0 -0
- data/lib/yeptris/psych/class_loader.rb +24 -0
- data/lib/yeptris/psych/scalar_scanner.rb +149 -0
- data/lib/yeptris/psych.rb +39 -7
- data/lib/yeptris/schema.rb +86 -0
- data/lib/yeptris.rb +1 -1
- data/libyeptris.so +0 -0
- data/vendor/libyeptris/CMakeLists.txt +1 -1
- data/vendor/libyeptris/src/yeptris/dom/dom.c +70 -0
- data/vendor/libyeptris/src/yeptris/dom/dom.h +10 -0
- data/vendor/libyeptris/src/yeptris/dom/mutate.c +2 -0
- data/vendor/libyeptris/src/yeptris/parse.c +11 -9
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9505473e51a840dbf7c50d8ed7ae56b7e3a9b3592e26787ba0a0b231320bfe86
|
|
4
|
+
data.tar.gz: 78fbfa73c88cc29e5376cde241e310b1b4d65b878e96198faf733640bc695454
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 88c4aaa0e2b48a7b5ce25da76e0035e0cd2d83f2901bde878c6114cdf046ade9c9e7ae74ff70ebc3fe403ea98a5dce86504de060cec35a5fb2e4e6a58a4c83b6
|
|
7
|
+
data.tar.gz: ed0b241f30ae93ea79f8d4deae9ca1f33dd14c124698484c79ea9905e42149564487bbad18234f704125185b0583f7a740c76817eaa2c522b28fa2f3dce20240
|
|
@@ -17,6 +17,14 @@
|
|
|
17
17
|
#include <stdlib.h>
|
|
18
18
|
#include <string.h>
|
|
19
19
|
|
|
20
|
+
static rb_encoding* cr_utf8_enc;
|
|
21
|
+
|
|
22
|
+
/* Map keys repeat across records in CBOR corpora; interning shares one
|
|
23
|
+
* VALUE per distinct key (the json_ruby.c on_key pattern, #157). */
|
|
24
|
+
static VALUE cr_key_str(const char* p, size_t len) {
|
|
25
|
+
return rb_enc_interned_str(p, (long)len, cr_utf8_enc);
|
|
26
|
+
}
|
|
27
|
+
|
|
20
28
|
#include "dom/dom.h"
|
|
21
29
|
#include "doc.h" /* the public YeptrisDocument wrapper: ->dom */
|
|
22
30
|
#include <yeptris/cbor.h>
|
|
@@ -91,8 +99,17 @@ static VALUE cr_walk(const yep_dom* d, uint32_t id) {
|
|
|
91
99
|
VALUE h = HASH_NEW_CAPA(pairs);
|
|
92
100
|
uint32_t c = n->first_child;
|
|
93
101
|
for (long i = 0; i < pairs && c != UINT32_MAX; i++) {
|
|
94
|
-
VALUE k
|
|
95
|
-
|
|
102
|
+
VALUE k;
|
|
103
|
+
const yep_dnode* kn = &d->nodes[c];
|
|
104
|
+
if (kn->kind == YEP_DOM_SCALAR && kn->tag_id == YEPTRIS_TAG_STR) {
|
|
105
|
+
uint32_t klen = 0;
|
|
106
|
+
const char* kp = cr_view(d, kn->value, &klen);
|
|
107
|
+
k = cr_key_str(kp, klen);
|
|
108
|
+
c = kn->next_sibling;
|
|
109
|
+
} else {
|
|
110
|
+
k = cr_walk(d, c);
|
|
111
|
+
c = d->nodes[c].next_sibling;
|
|
112
|
+
}
|
|
96
113
|
VALUE v = (c != UINT32_MAX) ? cr_walk(d, c) : Qnil;
|
|
97
114
|
if (c != UINT32_MAX) {
|
|
98
115
|
c = d->nodes[c].next_sibling;
|
|
@@ -107,6 +124,9 @@ static VALUE cr_walk(const yep_dom* d, uint32_t id) {
|
|
|
107
124
|
}
|
|
108
125
|
|
|
109
126
|
VALUE yep_rb_cbor_load(const char* p, size_t len, int strict) {
|
|
127
|
+
if (cr_utf8_enc == NULL) {
|
|
128
|
+
cr_utf8_enc = rb_utf8_encoding();
|
|
129
|
+
}
|
|
110
130
|
YeptrisStatus st = YEPTRIS_OK;
|
|
111
131
|
/* the DOM borrows the input buffer zero-copy; the walk ALLOCATES,
|
|
112
132
|
* and a GC compaction mid-walk moves the caller's String — the
|
|
@@ -423,8 +423,51 @@ static VALUE native_gc_mode_set(VALUE self, VALUE mode) {
|
|
|
423
423
|
return mode;
|
|
424
424
|
}
|
|
425
425
|
|
|
426
|
+
/* ABI-drift self-check (the #157-audit silent-nil class): the bundle
|
|
427
|
+
* is compiled against a specific yep_dom layout; engine drift makes
|
|
428
|
+
* the walkers read fields at stale offsets — cbor_load returned NIL
|
|
429
|
+
* on valid CBOR in the dev-checkout audit, with no error anywhere.
|
|
430
|
+
* Both walkers must round-trip a known document at load, or Init
|
|
431
|
+
* raises LoadError: lib/yeptris.rb's existing rescue falls back to
|
|
432
|
+
* the FFI ladder (loudly), and the stale bundle never answers with
|
|
433
|
+
* garbage. Runs inside rb_protect so a drifted walk raises instead of
|
|
434
|
+
* crashing the load. */
|
|
435
|
+
static VALUE native_self_check_body(VALUE unused) {
|
|
436
|
+
/* JSON: {"a"=>[1, 2.5, "x", true, nil]} */
|
|
437
|
+
const char* json = "{\"a\":[1,2.5,\"x\",true,null]}";
|
|
438
|
+
VALUE j = yep_rb_parse_json(json, strlen(json), 0);
|
|
439
|
+
if (j == Qundef || !RB_TYPE_P(j, T_HASH)) return Qfalse;
|
|
440
|
+
VALUE a = rb_hash_lookup(j, rb_str_new_cstr("a"));
|
|
441
|
+
if (!RB_TYPE_P(a, T_ARRAY) || RARRAY_LEN(a) != 5) return Qfalse;
|
|
442
|
+
if (!RB_INTEGER_TYPE_P(rb_ary_entry(a, 0))) return Qfalse;
|
|
443
|
+
if (!RB_FLOAT_TYPE_P(rb_ary_entry(a, 1))) return Qfalse;
|
|
444
|
+
if (!RB_TYPE_P(rb_ary_entry(a, 2), T_STRING)) return Qfalse;
|
|
445
|
+
if (rb_ary_entry(a, 3) != Qtrue || !NIL_P(rb_ary_entry(a, 4))) return Qfalse;
|
|
446
|
+
|
|
447
|
+
/* CBOR: A1 61 6B 01 = {"k" => 1} (canonical, hand-encoded) */
|
|
448
|
+
const char cbor[] = "\xA1\x61\x6B\x01";
|
|
449
|
+
VALUE c = yep_rb_cbor_load(cbor, sizeof(cbor) - 1, 0);
|
|
450
|
+
if (c == Qundef || !RB_TYPE_P(c, T_HASH)) return Qfalse;
|
|
451
|
+
VALUE one = rb_hash_lookup(c, rb_str_new_cstr("k"));
|
|
452
|
+
if (!RB_INTEGER_TYPE_P(one) || !rb_eql(one, INT2FIX(1))) return Qfalse;
|
|
453
|
+
return Qtrue;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
static void native_self_check(void) {
|
|
457
|
+
int state = 0;
|
|
458
|
+
VALUE ok = rb_protect(native_self_check_body, Qnil, &state);
|
|
459
|
+
if (state != 0 || ok != Qtrue) {
|
|
460
|
+
if (state != 0) rb_set_errinfo(Qnil);
|
|
461
|
+
rb_raise(rb_eLoadError,
|
|
462
|
+
"yeptris: native materializer failed its ABI self-check "
|
|
463
|
+
"(stale build against a drifted engine?) — refusing to "
|
|
464
|
+
"register; the FFI ladder will carry the load");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
426
468
|
RUBY_FUNC_EXPORTED void Init_native(void) {
|
|
427
469
|
utf8_enc = rb_utf8_encoding();
|
|
470
|
+
native_self_check();
|
|
428
471
|
VALUE mYep = rb_define_module("Yeptris");
|
|
429
472
|
VALUE mNat = rb_define_module_under(mYep, "Native");
|
|
430
473
|
rb_define_singleton_method(mNat, "load_json", native_load_json, -1);
|
data/lib/yeptris/document.rb
CHANGED
|
@@ -8,6 +8,11 @@ class Yeptris::Document
|
|
|
8
8
|
# explicit free path and the finalizer both flip the same flag.
|
|
9
9
|
Freed = Struct.new(:state) # :alive | :freed
|
|
10
10
|
|
|
11
|
+
# The raw C handle — the Yeptris::Node wrappers cache by its
|
|
12
|
+
# address; stream-children wrappers (Document.without_finalizer)
|
|
13
|
+
# carry it without registering a finalizer (#182).
|
|
14
|
+
attr_reader :c_ptr
|
|
15
|
+
|
|
11
16
|
# The schema this document was parsed with (a parse property).
|
|
12
17
|
attr_reader :parse_schema
|
|
13
18
|
|
|
@@ -80,6 +85,18 @@ class Yeptris::Document
|
|
|
80
85
|
new(c_ptr, Freed.new(:alive), schema)
|
|
81
86
|
end
|
|
82
87
|
|
|
88
|
+
# #182: the stream-children wrapper owns NOTHING (the parent
|
|
89
|
+
# Document wrapper is the sole freer of the C memory). A plain
|
|
90
|
+
# Document.new would register a finalizer on a c_ptr we don't own;
|
|
91
|
+
# a parse_stream on a multi-doc stream would free the same pointer
|
|
92
|
+
# three times at GC → SIGABRT. This factory builds the wrapper
|
|
93
|
+
# WITHOUT a finalizer; the wrapper's lifetime follows the owner.
|
|
94
|
+
def self.without_finalizer(c_ptr)
|
|
95
|
+
doc = new(c_ptr)
|
|
96
|
+
ObjectSpace.undefine_finalizer(doc)
|
|
97
|
+
doc
|
|
98
|
+
end
|
|
99
|
+
|
|
83
100
|
def ensure_alive!
|
|
84
101
|
raise Yeptris::FreedError, "document is freed" if @freed.state == :freed
|
|
85
102
|
end
|
data/lib/yeptris/json.rb
CHANGED
|
@@ -119,7 +119,7 @@ module Yeptris
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
def walk_tape(src, tape)
|
|
122
|
+
def walk_tape(src, tape, strict_dup = STRICT_DUPLICATE_KEYS)
|
|
123
123
|
n = tape[:count]
|
|
124
124
|
kinds = tape[:kinds].read_bytes(n).unpack("C*")
|
|
125
125
|
offs = tape[:offs].read_bytes(n * 4).unpack("V*")
|
|
@@ -139,7 +139,7 @@ module Yeptris
|
|
|
139
139
|
|
|
140
140
|
docs = [nil] # record 0 (DOC) pre-consumed — the root's slot
|
|
141
141
|
stack = []
|
|
142
|
-
key_sets =
|
|
142
|
+
key_sets = strict_dup ? [{}] : nil
|
|
143
143
|
pending_key = nil
|
|
144
144
|
i = 1
|
|
145
145
|
while i < n
|
|
@@ -152,8 +152,12 @@ module Yeptris
|
|
|
152
152
|
place(docs, stack, pending_key) { {} }
|
|
153
153
|
pending_key = nil
|
|
154
154
|
when T_CLOSE
|
|
155
|
+
# Key frames are pushed per MAP_OPEN — an ARRAY close must
|
|
156
|
+
# not pop the enclosing map's frame (issue found by ea's CI:
|
|
157
|
+
# {"a":[1],"b":[2],"c":[3]} exhausted the frames and the next
|
|
158
|
+
# map key hit key_sets.last.key? on nil under strict mode).
|
|
159
|
+
key_sets&.pop if stack.last.is_a?(Hash)
|
|
155
160
|
stack.pop
|
|
156
|
-
key_sets&.pop
|
|
157
161
|
when T_STR
|
|
158
162
|
# empty-string literals are frozen+shared since Ruby 3.4
|
|
159
163
|
# (and `+""` binds after the method chain anyway), so the
|
|
@@ -256,8 +260,8 @@ module Yeptris
|
|
|
256
260
|
place(docs, stack, pending_key) { {} }
|
|
257
261
|
pending_key = nil
|
|
258
262
|
when ValueML::CLOSE
|
|
263
|
+
key_sets&.pop if stack.last.is_a?(Hash)
|
|
259
264
|
stack.pop
|
|
260
|
-
key_sets&.pop
|
|
261
265
|
when ValueML::V_STR
|
|
262
266
|
text = arena.byteslice(offs[i], lens[i])
|
|
263
267
|
if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
|
data/lib/yeptris/native-3.3.so
CHANGED
|
Binary file
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Yeptris
|
|
6
|
+
module Psych
|
|
7
|
+
# The minimal ClassLoader face the ScalarScanner port drives
|
|
8
|
+
# (stdlib's loader resolves through the class hierarchy; the
|
|
9
|
+
# scanner only needs the date class and symbolize).
|
|
10
|
+
class ClassLoader
|
|
11
|
+
def date
|
|
12
|
+
Date
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def symbolize str
|
|
16
|
+
str.to_sym
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def load name
|
|
20
|
+
Object.const_get(name)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "date"
|
|
4
|
+
|
|
5
|
+
module Yeptris
|
|
6
|
+
module Psych
|
|
7
|
+
###
|
|
8
|
+
# Scan scalars for built in types — the stdlib Psych::ScalarScanner
|
|
9
|
+
# port (#179): same regexes, same precedence, same edge verdicts.
|
|
10
|
+
# The load path types through the C resolver; this class is the
|
|
11
|
+
# standalone API stdlib code (and Psych's own tests) drive directly.
|
|
12
|
+
class ScalarScanner
|
|
13
|
+
# Taken from http://yaml.org/type/timestamp.html
|
|
14
|
+
TIME = /^-?\d{4}-\d{1,2}-\d{1,2}(?:[Tt]|\s+)\d{1,2}:\d\d:\d\d(?:\.\d*)?(?:\s*(?:Z|[-+]\d{1,2}:?(?:\d\d)?))?$/
|
|
15
|
+
|
|
16
|
+
# Taken from http://yaml.org/type/float.html
|
|
17
|
+
# Base 60, [-+]inf and NaN are handled separately
|
|
18
|
+
FLOAT = /^(?:[-+]?([0-9][0-9_,]*)?\.[0-9]*([eE][-+][0-9]+)?(?# base 10))$/x
|
|
19
|
+
|
|
20
|
+
# Taken from http://yaml.org/type/int.html and modified to ensure at least one numerical symbol exists
|
|
21
|
+
INTEGER_STRICT = /^(?:[-+]?0b[_]*[0-1][0-1_]* (?# base 2)
|
|
22
|
+
|[-+]?0[_]*[0-7][0-7_]* (?# base 8)
|
|
23
|
+
|[-+]?(0|[1-9][0-9_]*) (?# base 10)
|
|
24
|
+
|[-+]?0x[_]*[0-9a-fA-F][0-9a-fA-F_]* (?# base 16))$/x
|
|
25
|
+
|
|
26
|
+
# Same as above, but allows commas.
|
|
27
|
+
# Not to YML spec, but kept for backwards compatibility
|
|
28
|
+
INTEGER_LEGACY = /^(?:[-+]?0b[_,]*[0-1][0-1_,]* (?# base 2)
|
|
29
|
+
|[-+]?0[_,]*[0-7][0-7_,]* (?# base 8)
|
|
30
|
+
|[-+]?(?:0|[1-9](?:[0-9]|,[0-9]|_[0-9])*) (?# base 10)
|
|
31
|
+
|[-+]?0x[_,]*[0-9a-fA-F][0-9a-fA-F_,]* (?# base 16))$/x
|
|
32
|
+
|
|
33
|
+
BOOLEAN_TRUE = /^(yes|true|on)$/i
|
|
34
|
+
BOOLEAN_FALSE = /^(no|false|off)$/i
|
|
35
|
+
|
|
36
|
+
attr_reader :class_loader
|
|
37
|
+
|
|
38
|
+
# Create a new scanner
|
|
39
|
+
def initialize class_loader = ClassLoader, strict_integer: false, parse_symbols: true
|
|
40
|
+
@symbol_cache = {}
|
|
41
|
+
@class_loader = class_loader
|
|
42
|
+
@strict_integer = strict_integer
|
|
43
|
+
@parse_symbols = parse_symbols
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Tokenize +string+ returning the Ruby object
|
|
47
|
+
def tokenize string
|
|
48
|
+
return nil if string.empty?
|
|
49
|
+
return @symbol_cache[string] if @symbol_cache.key?(string)
|
|
50
|
+
integer_regex = @strict_integer ? INTEGER_STRICT : INTEGER_LEGACY
|
|
51
|
+
# Check for a String type, being careful not to get caught by hash keys, hex values, and
|
|
52
|
+
# special floats (e.g., -.inf).
|
|
53
|
+
if string.match?(%r{^[^\d.:-]?[[:alpha:]_\s!@#$%\^&*(){}<>|/\\~;=]+}) || string.match?(/\n/)
|
|
54
|
+
return string if string.length > 5
|
|
55
|
+
|
|
56
|
+
if string.match?(/^[^ytonf~]/i)
|
|
57
|
+
string
|
|
58
|
+
elsif string == '~' || string.match?(/^null$/i)
|
|
59
|
+
nil
|
|
60
|
+
elsif string.match?(BOOLEAN_TRUE)
|
|
61
|
+
true
|
|
62
|
+
elsif string.match?(BOOLEAN_FALSE)
|
|
63
|
+
false
|
|
64
|
+
else
|
|
65
|
+
string
|
|
66
|
+
end
|
|
67
|
+
elsif string.match?(TIME)
|
|
68
|
+
begin
|
|
69
|
+
parse_time string
|
|
70
|
+
rescue ArgumentError
|
|
71
|
+
string
|
|
72
|
+
end
|
|
73
|
+
elsif string.match?(/^\d{4}-(?:1[012]|0\d|\d)-(?:[12]\d|3[01]|0\d|\d)$/)
|
|
74
|
+
begin
|
|
75
|
+
class_loader.date.strptime(string, '%F', Date::GREGORIAN)
|
|
76
|
+
rescue ArgumentError
|
|
77
|
+
string
|
|
78
|
+
end
|
|
79
|
+
elsif string.match?(/^\+?\.inf$/i)
|
|
80
|
+
Float::INFINITY
|
|
81
|
+
elsif string.match?(/^-\.inf$/i)
|
|
82
|
+
-Float::INFINITY
|
|
83
|
+
elsif string.match?(/^\.nan$/i)
|
|
84
|
+
Float::NAN
|
|
85
|
+
elsif @parse_symbols && string.match?(/^:./)
|
|
86
|
+
if string =~ /^:(["'])(.*)\1/
|
|
87
|
+
@symbol_cache[string] = class_loader.symbolize($2.sub(/^:/, ''))
|
|
88
|
+
else
|
|
89
|
+
@symbol_cache[string] = class_loader.symbolize(string.sub(/^:/, ''))
|
|
90
|
+
end
|
|
91
|
+
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}$/)
|
|
92
|
+
i = 0
|
|
93
|
+
string.split(':').each_with_index do |n, e|
|
|
94
|
+
i += (n.to_i * 60**(e - 2).abs)
|
|
95
|
+
end
|
|
96
|
+
i
|
|
97
|
+
elsif string.match?(/^[-+]?[0-9][0-9_]*(:[0-5]?[0-9]){1,2}\.[0-9_]*$/)
|
|
98
|
+
i = 0
|
|
99
|
+
string.split(':').each_with_index do |n, e|
|
|
100
|
+
i += (n.to_f * 60**(e - 2).abs)
|
|
101
|
+
end
|
|
102
|
+
i
|
|
103
|
+
elsif string.match?(FLOAT)
|
|
104
|
+
if string.match?(/\A[-+]?\.\Z/)
|
|
105
|
+
string
|
|
106
|
+
else
|
|
107
|
+
Float(string.delete(',_').gsub(/\.([Ee]|$)/, '\1'))
|
|
108
|
+
end
|
|
109
|
+
elsif string.match?(integer_regex)
|
|
110
|
+
parse_int string
|
|
111
|
+
else
|
|
112
|
+
string
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Parse and return an int from +string+
|
|
117
|
+
def parse_int string
|
|
118
|
+
Integer(string.delete(',_'))
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
###
|
|
122
|
+
# Parse and return a Time from +string+
|
|
123
|
+
def parse_time string
|
|
124
|
+
date, time = *(string.split(/[Tt]|\s+/, 2))
|
|
125
|
+
(yy, m, dd) = date.match(/^(-?\d{4})-(\d{1,2})-(\d{1,2})/).captures.map { |x| x.to_i }
|
|
126
|
+
md = time.match(/(\d+:\d+:\d+)(?:\.(\d*))?\s*(Z|[-+]\d+(:\d\d)?)?/)
|
|
127
|
+
|
|
128
|
+
(hh, mm, ss) = md[1].split(':').map { |x| x.to_i }
|
|
129
|
+
us = (md[2] ? Rational("0.#{md[2]}") : 0) * 1_000_000
|
|
130
|
+
|
|
131
|
+
time = Time.utc(yy, m, dd, hh, mm, ss, us)
|
|
132
|
+
|
|
133
|
+
return time if 'Z' == md[3]
|
|
134
|
+
return Time.at(time.to_i, us) unless md[3]
|
|
135
|
+
|
|
136
|
+
tz = md[3].match(/^([+\-]?\d{1,2})\:?(\d{1,2})?$/)[1..-1].compact.map { |digit| Integer(digit, 10) }
|
|
137
|
+
offset = tz.first * 3600
|
|
138
|
+
|
|
139
|
+
if offset < 0
|
|
140
|
+
offset -= ((tz[1] || 0) * 60)
|
|
141
|
+
else
|
|
142
|
+
offset += ((tz[1] || 0) * 60)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
Time.new(yy, m, dd, hh, mm, ss + us / 1_000_000r, offset)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
data/lib/yeptris/psych.rb
CHANGED
|
@@ -22,6 +22,9 @@ require "yeptris"
|
|
|
22
22
|
# document without materializing.
|
|
23
23
|
module Yeptris
|
|
24
24
|
module Psych
|
|
25
|
+
autoload :ClassLoader, "yeptris/psych/class_loader"
|
|
26
|
+
autoload :ScalarScanner, "yeptris/psych/scalar_scanner"
|
|
27
|
+
|
|
25
28
|
# The tag registries (Psych's class-level API, #95 bug 4):
|
|
26
29
|
# load_tags maps a serialized tag to the Class that revives it;
|
|
27
30
|
# dump_tags overrides the emitted tag for a Class. Consulted by
|
|
@@ -258,18 +261,28 @@ module Yeptris
|
|
|
258
261
|
::Yeptris::Psych.dump(obj, io, options)
|
|
259
262
|
end
|
|
260
263
|
|
|
261
|
-
def load_stream(yaml, **kwargs)
|
|
264
|
+
def load_stream(yaml, **kwargs, &block)
|
|
262
265
|
# materialize each document's root directly — the stream
|
|
263
266
|
# children share one C document, so their handles would all
|
|
264
|
-
# resolve to the first document's tree
|
|
267
|
+
# resolve to the first document's tree. stdlib's block form
|
|
268
|
+
# yields each loaded document's object (#179 round 4).
|
|
265
269
|
doc = Yeptris::Document.parse(yaml, schema: :compat_11)
|
|
266
270
|
return nil if doc.nil? # the legal empty stream
|
|
267
271
|
|
|
268
272
|
begin
|
|
269
|
-
(0...doc.document_count).map { |i| doc.root(i).to_ruby }
|
|
273
|
+
docs = (0...doc.document_count).map { |i| doc.root(i).to_ruby }
|
|
270
274
|
ensure
|
|
271
275
|
doc.free
|
|
272
276
|
end
|
|
277
|
+
docs.each { |d| block.call(d) } if block
|
|
278
|
+
docs
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# safe_load_stream: stdlib's surface — the stream form of
|
|
282
|
+
# safe_load. Our load_stream is already safe-by-default
|
|
283
|
+
# (Psych 5 semantics), so this is the yielding wrapper.
|
|
284
|
+
def safe_load_stream(yaml, **kwargs, &block)
|
|
285
|
+
load_stream(yaml, **kwargs, &block)
|
|
273
286
|
end
|
|
274
287
|
|
|
275
288
|
# The first document's node tree (no Ruby materialization).
|
|
@@ -291,13 +304,22 @@ module Yeptris
|
|
|
291
304
|
raise SyntaxError.from_parse_error(e)
|
|
292
305
|
end
|
|
293
306
|
|
|
294
|
-
def parse_stream(yaml)
|
|
307
|
+
def parse_stream(yaml, &block)
|
|
308
|
+
# stdlib yields each document to a block if one was given (and
|
|
309
|
+
# the stream it returns holds the same children either way).
|
|
310
|
+
# #182: the wrapper is the sole owner of the C memory — the
|
|
311
|
+
# stream merely REFERENCES it; the wrapper's own finalizer
|
|
312
|
+
# handles GC-free. (The block-yielding form is tracked under
|
|
313
|
+
# #179: the current implementation materializes eagerly; the
|
|
314
|
+
# block is only honored as a no-op convenience.)
|
|
295
315
|
doc = Yeptris::Document.parse(yaml, schema: :compat_11)
|
|
296
316
|
return nil if doc.nil? || doc.document_count.zero?
|
|
297
317
|
|
|
298
318
|
stream = Nodes::Stream.new
|
|
299
319
|
(0...doc.document_count).each do |i|
|
|
300
|
-
|
|
320
|
+
child = Nodes::Builder.document_stream_child(doc, i)
|
|
321
|
+
stream.children << child
|
|
322
|
+
block.call(child) if block # stdlib's yielding form (#179 round 4)
|
|
301
323
|
end
|
|
302
324
|
# ownership: the DOCUMENT wrapper is the sole owner — its own
|
|
303
325
|
# finalizer (pointer-only closure) frees the C memory; the
|
|
@@ -308,6 +330,8 @@ module Yeptris
|
|
|
308
330
|
rescue Yeptris::ParseError => e
|
|
309
331
|
raise SyntaxError.from_parse_error(e)
|
|
310
332
|
ensure
|
|
333
|
+
# the returned stream OWNS the doc's lifetime (its free delegates
|
|
334
|
+
# to the owner); only free here when the stream wasn't built
|
|
311
335
|
doc&.free if doc && !stream
|
|
312
336
|
end
|
|
313
337
|
|
|
@@ -440,6 +464,10 @@ module Yeptris
|
|
|
440
464
|
# tree stay valid while the tree is reachable; a GC finalizer
|
|
441
465
|
# releases the C memory when it is not.
|
|
442
466
|
class Document < Node
|
|
467
|
+
# The document's root as a Ruby object (the stream-yield face).
|
|
468
|
+
def to_ruby
|
|
469
|
+
children.first&.to_ruby
|
|
470
|
+
end
|
|
443
471
|
attr_reader :version, :tags
|
|
444
472
|
|
|
445
473
|
def initialize(version = [], tags = {})
|
|
@@ -522,8 +550,12 @@ module Yeptris
|
|
|
522
550
|
# stream owns the yeptris document)
|
|
523
551
|
def document_stream_child(doc, index)
|
|
524
552
|
root = doc.root(index)
|
|
525
|
-
|
|
526
|
-
|
|
553
|
+
# #182's root cause: three per-stream Document wrappers all
|
|
554
|
+
# capture the same c_ptr in their finalizers → three frees.
|
|
555
|
+
# The stream-children WRAPPER owns nothing (the owner wrapper
|
|
556
|
+
# is the real Document; this one just references it). Build
|
|
557
|
+
# a non-owning wrapper with no finalizer.
|
|
558
|
+
d = Document.send(:new, root&.document, false)
|
|
527
559
|
d.children << node(root) if root
|
|
528
560
|
d
|
|
529
561
|
end
|
data/lib/yeptris/schema.rb
CHANGED
|
@@ -66,6 +66,92 @@ module Yeptris
|
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
# #184 (lutaml-model KV path): zip Schema columns into record
|
|
70
|
+
# hashes ready for Serializable.instantiate. Mapping root → one
|
|
71
|
+
# hash; sequence-of-mappings → one hash per element; nested
|
|
72
|
+
# mappings → nested hashes. Returns Array<Hash> always. when_attribute
|
|
73
|
+
# / polymorphic stay out of scope (interpretive fallback).
|
|
74
|
+
def load_records(source, schema: :core_12, desc:, capacity: 64)
|
|
75
|
+
cols = load(source, schema: schema, desc: desc, capacity: capacity)
|
|
76
|
+
zip_records(desc, cols)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def zip_records(desc, cols)
|
|
80
|
+
root = desc[0] || {}
|
|
81
|
+
case root[:kind]
|
|
82
|
+
when :sequence
|
|
83
|
+
child_start = root[:child_index] || 1
|
|
84
|
+
child_count = root[:child_count] || 0
|
|
85
|
+
return [] if child_count.zero?
|
|
86
|
+
|
|
87
|
+
child = desc[child_start]
|
|
88
|
+
if child && child[:kind] == :mapping
|
|
89
|
+
field_start = child[:child_index] || (child_start + 1)
|
|
90
|
+
field_count = child[:child_count] || 0
|
|
91
|
+
fields = desc[field_start, field_count] || []
|
|
92
|
+
field_cols = cols[field_start, field_count] || []
|
|
93
|
+
nrows = field_cols.map(&:length).max || 0
|
|
94
|
+
Array.new(nrows) do |r|
|
|
95
|
+
h = {}
|
|
96
|
+
fields.each_with_index do |f, i|
|
|
97
|
+
next unless f[:wire_name]
|
|
98
|
+
col = field_cols[i] || []
|
|
99
|
+
# ABSENT keys are OMITTED, not nil-filled (lutaml-model's
|
|
100
|
+
# load-bearing edge: absent seeds defaults +
|
|
101
|
+
# using_default?; explicit nil is a present value with
|
|
102
|
+
# different render semantics). The C columns fill in
|
|
103
|
+
# document order — col.length > r means present.
|
|
104
|
+
next if col.length <= r
|
|
105
|
+
|
|
106
|
+
h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[r])
|
|
107
|
+
end
|
|
108
|
+
h
|
|
109
|
+
end
|
|
110
|
+
else
|
|
111
|
+
(cols[child_start] || []).map { |v| { value: v } }
|
|
112
|
+
end
|
|
113
|
+
when :mapping
|
|
114
|
+
field_start = root[:child_index] || 1
|
|
115
|
+
field_count = root[:child_count] || 0
|
|
116
|
+
fields = desc[field_start, field_count] || []
|
|
117
|
+
field_cols = cols[field_start, field_count] || []
|
|
118
|
+
h = {}
|
|
119
|
+
fields.each_with_index do |f, i|
|
|
120
|
+
next unless f[:wire_name]
|
|
121
|
+
col = field_cols[i] || []
|
|
122
|
+
next if col.empty? # absent: omitted, not nil-filled (#184)
|
|
123
|
+
|
|
124
|
+
h[f[:wire_name].to_sym] = materialize_field(f, desc, cols, col[0])
|
|
125
|
+
end
|
|
126
|
+
[h]
|
|
127
|
+
else
|
|
128
|
+
[{ value: cols[0]&.first }]
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
private_class_method :zip_records
|
|
132
|
+
|
|
133
|
+
def materialize_field(field, desc, cols, cell)
|
|
134
|
+
case field[:kind]
|
|
135
|
+
when :mapping
|
|
136
|
+
start = field[:child_index] || 0
|
|
137
|
+
count = field[:child_count] || 0
|
|
138
|
+
return nil if count.zero? || cell.nil?
|
|
139
|
+
|
|
140
|
+
nested = {}
|
|
141
|
+
desc[start, count]&.each_with_index do |nf, i|
|
|
142
|
+
next unless nf[:wire_name]
|
|
143
|
+
ncol = cols[start + i] || []
|
|
144
|
+
nested[nf[:wire_name].to_sym] = ncol.is_a?(Array) ? (ncol[0] rescue cell) : cell
|
|
145
|
+
end
|
|
146
|
+
nested
|
|
147
|
+
when :sequence
|
|
148
|
+
cell.is_a?(Array) ? cell : Array(cell)
|
|
149
|
+
else
|
|
150
|
+
cell
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
private_class_method :materialize_field
|
|
154
|
+
|
|
69
155
|
def compile(desc)
|
|
70
156
|
desc.map do |n|
|
|
71
157
|
{ kind: KIND.fetch(n.fetch(:kind)),
|
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.16.2".freeze
|
|
7
7
|
# The error hierarchy lives in THIS file (the parent namespace's
|
|
8
8
|
# own file): nested constants do not trigger a parent-constant
|
|
9
9
|
# autoload, and the law forbids internal requires — defining the
|
data/libyeptris.so
CHANGED
|
Binary file
|
|
@@ -241,6 +241,9 @@ void dom_link(yep_dom* d, uint32_t parent, uint32_t child) {
|
|
|
241
241
|
}
|
|
242
242
|
p->last_child = child;
|
|
243
243
|
p->count++;
|
|
244
|
+
/* every link changes SOME container's child list (#377's cache) */
|
|
245
|
+
d->child_cache_id = UINT32_MAX;
|
|
246
|
+
d->child_cache_len = 0;
|
|
244
247
|
}
|
|
245
248
|
|
|
246
249
|
/* ---- mutation side tables (64-2a) ---- */
|
|
@@ -485,6 +488,9 @@ yep_dom* yep_dom_create(const yep_allocator* sys) {
|
|
|
485
488
|
* document that decode-only consumers (CBOR load, serialize, free)
|
|
486
489
|
* never touch; yep_dom_handles() materializes it on first use */
|
|
487
490
|
d->handles = NULL;
|
|
491
|
+
d->child_cache = NULL;
|
|
492
|
+
d->child_cache_id = UINT32_MAX;
|
|
493
|
+
d->child_cache_len = 0;
|
|
488
494
|
if (yep_mutex_init(&d->midx.mu) != 0) {
|
|
489
495
|
yep_pool_destroy(pool);
|
|
490
496
|
yep_free(sys, d);
|
|
@@ -513,6 +519,69 @@ struct yep_hpool* yep_dom_handles(yep_dom* d) {
|
|
|
513
519
|
return h;
|
|
514
520
|
}
|
|
515
521
|
|
|
522
|
+
/* #377 (ruby #168): seq_at/map_at were O(i) sibling walks — the
|
|
523
|
+
* bindings' per-element loops made an 80k-row sequence quadratic
|
|
524
|
+
* (the relaton index: 238-328s where stdlib takes ~3s). The bulk
|
|
525
|
+
* drain (yeptris_node_children) fixed the bindings; THIS fixes the
|
|
526
|
+
* accessor: the last indexed container's child ids cache under the
|
|
527
|
+
* lazy-init mutex, O(1) after the first call. Invalidated by every
|
|
528
|
+
* child-list mutation (dom_invalidate_child_cache). */
|
|
529
|
+
void dom_invalidate_child_cache(yep_dom* d) {
|
|
530
|
+
if (d == NULL) {
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
d->child_cache_id = UINT32_MAX;
|
|
534
|
+
d->child_cache_len = 0;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/* Returns child id at index (UINT32_MAX when out of range); the
|
|
538
|
+
* container's child count rides *count_out. */
|
|
539
|
+
uint32_t dom_indexed_child(yep_dom* d, uint32_t cid, size_t index, uint32_t* count_out) {
|
|
540
|
+
const yep_dnode* n = yep_dom_node(d, cid);
|
|
541
|
+
if (n == NULL) {
|
|
542
|
+
*count_out = 0;
|
|
543
|
+
return UINT32_MAX;
|
|
544
|
+
}
|
|
545
|
+
*count_out = n->count;
|
|
546
|
+
if (d->child_cache_id == cid) {
|
|
547
|
+
if (index < d->child_cache_len) {
|
|
548
|
+
return d->child_cache[index];
|
|
549
|
+
}
|
|
550
|
+
return UINT32_MAX;
|
|
551
|
+
}
|
|
552
|
+
/* miss: build once under the lazy-init mutex (Threads.
|
|
553
|
+
* ReadOnlySharing — concurrent first calls race here exactly like
|
|
554
|
+
* the handle pool's) */
|
|
555
|
+
yep_mutex_lock(&d->midx.mu);
|
|
556
|
+
if (d->child_cache_id != cid) {
|
|
557
|
+
yep_free(d->sys, d->child_cache);
|
|
558
|
+
d->child_cache = NULL;
|
|
559
|
+
d->child_cache_len = 0;
|
|
560
|
+
if (n->count > 0) {
|
|
561
|
+
uint32_t* arr = yep_alloc(d->sys, n->count * sizeof(uint32_t));
|
|
562
|
+
if (arr != NULL) {
|
|
563
|
+
uint32_t k = 0;
|
|
564
|
+
for (uint32_t id = n->first_child; id != UINT32_MAX && k < n->count;) {
|
|
565
|
+
const yep_dnode* cn = yep_dom_node(d, id);
|
|
566
|
+
if (cn == NULL) {
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
arr[k++] = id;
|
|
570
|
+
id = cn->next_sibling;
|
|
571
|
+
}
|
|
572
|
+
d->child_cache = arr;
|
|
573
|
+
d->child_cache_len = k;
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
d->child_cache_id = cid;
|
|
577
|
+
}
|
|
578
|
+
yep_mutex_unlock(&d->midx.mu);
|
|
579
|
+
if (index < d->child_cache_len) {
|
|
580
|
+
return d->child_cache[index];
|
|
581
|
+
}
|
|
582
|
+
return UINT32_MAX;
|
|
583
|
+
}
|
|
584
|
+
|
|
516
585
|
void yep_dom_destroy(yep_dom* d) {
|
|
517
586
|
if (d == NULL) {
|
|
518
587
|
return;
|
|
@@ -523,6 +592,7 @@ void yep_dom_destroy(yep_dom* d) {
|
|
|
523
592
|
yep_free(d->sys, d->mut_att);
|
|
524
593
|
yep_free(d->sys, d->mut_depth);
|
|
525
594
|
yep_hpool_destroy(d->handles);
|
|
595
|
+
yep_free(d->sys, d->child_cache);
|
|
526
596
|
yep_pool_destroy(d->pool);
|
|
527
597
|
yep_free(d->sys, d);
|
|
528
598
|
}
|
|
@@ -114,6 +114,13 @@ typedef struct yep_dom {
|
|
|
114
114
|
* YeptrisNode wrappers here, so read-only document sharing across
|
|
115
115
|
* threads is safe; parse-path pools stay single-threaded */
|
|
116
116
|
struct yep_hpool* handles;
|
|
117
|
+
/* #377: the lazy indexed-children cache — child ids of the LAST
|
|
118
|
+
* container indexed via seq_at/map_at. Built once per container
|
|
119
|
+
* under the lazy-init mutex (read-only sharing), O(1) after;
|
|
120
|
+
* invalidated by every child-list mutation. NULL when idle. */
|
|
121
|
+
uint32_t* child_cache;
|
|
122
|
+
uint32_t child_cache_id; /* UINT32_MAX = empty */
|
|
123
|
+
uint32_t child_cache_len;
|
|
117
124
|
yep_dnode* nodes;
|
|
118
125
|
uint32_t ncount, ncap;
|
|
119
126
|
uint32_t* docs; /* document root node ids */
|
|
@@ -200,6 +207,9 @@ void dom_mut_set_depth(yep_dom* d, uint32_t id, uint16_t depth);
|
|
|
200
207
|
struct yep_hpool* yep_hpool_create(const yep_allocator* sys);
|
|
201
208
|
/* Lazy handle-pool acquisition (#157): NULL dom or OOM stays NULL. */
|
|
202
209
|
struct yep_hpool* yep_dom_handles(yep_dom* d);
|
|
210
|
+
/* #377: the lazy indexed-children cache (seq_at/map_at's O(1) leg) */
|
|
211
|
+
void dom_invalidate_child_cache(yep_dom* d);
|
|
212
|
+
uint32_t dom_indexed_child(yep_dom* d, uint32_t cid, size_t index, uint32_t* count_out);
|
|
203
213
|
void yep_hpool_destroy(struct yep_hpool* p);
|
|
204
214
|
void* yep_hpool_alloc(struct yep_hpool* p, size_t size, size_t align);
|
|
205
215
|
|
|
@@ -268,6 +268,8 @@ static int unlink_child(yep_dom* d, uint32_t parent, uint32_t child) {
|
|
|
268
268
|
p->last_child = c;
|
|
269
269
|
}
|
|
270
270
|
}
|
|
271
|
+
d->child_cache_id = UINT32_MAX; /* #377: the list changed */
|
|
272
|
+
d->child_cache_len = 0;
|
|
271
273
|
d->nodes[child].next_sibling = UINT32_MAX;
|
|
272
274
|
dom_mut_set_att(d, child, 0);
|
|
273
275
|
p->count--;
|
|
@@ -646,11 +646,12 @@ YEPTRIS_API YeptrisNode yeptris_node_seq_at(YeptrisNode handle, size_t index) {
|
|
|
646
646
|
if (n == NULL || n->kind != YEP_DOM_SEQUENCE || index >= n->count) {
|
|
647
647
|
return NULL;
|
|
648
648
|
}
|
|
649
|
-
|
|
650
|
-
uint32_t
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
649
|
+
yep_dom* dom = ((yeptris_node*)handle)->doc->dom;
|
|
650
|
+
uint32_t count = 0;
|
|
651
|
+
/* the node id IS its dnode index */
|
|
652
|
+
uint32_t id = dom_indexed_child(dom, ((yeptris_node*)handle)->id, index, &count); /* #377 */
|
|
653
|
+
if (id == UINT32_MAX) {
|
|
654
|
+
return NULL;
|
|
654
655
|
}
|
|
655
656
|
return wrap((yeptris_node*)handle, id);
|
|
656
657
|
}
|
|
@@ -685,10 +686,11 @@ YEPTRIS_API int yeptris_node_map_at(YeptrisNode handle, size_t index, YeptrisNod
|
|
|
685
686
|
return -1;
|
|
686
687
|
}
|
|
687
688
|
yeptris_node* h = (yeptris_node*)handle;
|
|
688
|
-
|
|
689
|
-
uint32_t
|
|
690
|
-
|
|
691
|
-
|
|
689
|
+
yep_dom* d = h->doc->dom;
|
|
690
|
+
uint32_t count = 0;
|
|
691
|
+
uint32_t child = dom_indexed_child(d, h->id, (size_t)index * 2, &count); /* #377 */
|
|
692
|
+
if (child == UINT32_MAX) {
|
|
693
|
+
return -1;
|
|
692
694
|
}
|
|
693
695
|
if (key != NULL) {
|
|
694
696
|
*key = wrap(h, child);
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: yeptris
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.16.2
|
|
5
5
|
platform: aarch64-linux
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-09-
|
|
11
|
+
date: 2026-09-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: ffi
|
|
@@ -50,11 +50,13 @@ files:
|
|
|
50
50
|
- lib/yeptris/native-3.3.so
|
|
51
51
|
- lib/yeptris/node.rb
|
|
52
52
|
- lib/yeptris/psych.rb
|
|
53
|
+
- lib/yeptris/psych/class_loader.rb
|
|
53
54
|
- lib/yeptris/psych/coder_shim.rb
|
|
54
55
|
- lib/yeptris/psych/drop_in.rb
|
|
55
56
|
- lib/yeptris/psych/encodable.rb
|
|
56
57
|
- lib/yeptris/psych/handler.rb
|
|
57
58
|
- lib/yeptris/psych/parser.rb
|
|
59
|
+
- lib/yeptris/psych/scalar_scanner.rb
|
|
58
60
|
- lib/yeptris/psych/visitors.rb
|
|
59
61
|
- lib/yeptris/schema.rb
|
|
60
62
|
- lib/yeptris/valueml.rb
|