yeptris 0.6.16.1 → 0.6.17.1

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: 36b4d1444bde48fba47b0fd89851f5ebe8cd7a666c7e3faa8652caf4bb37a60f
4
- data.tar.gz: 7f113394e8f82d502425610637641716af695a835ba01f6ce5e611c5073c6d55
3
+ metadata.gz: e317e3324f6bd24065dec096878270922234973be2957c7c850fdb59dea214c6
4
+ data.tar.gz: e98c83679d4228d3b35ee70b527f3d10f916308090f35b17585aaa9679292726
5
5
  SHA512:
6
- metadata.gz: cec260d2c0b2c10d152336d0809eca8d9d15c4fc8e52ddac94fa01b8850c3f59cebb74f0ddd78b48f751ce9fb6a1f74e6f9646d81a237144645810949c93b7a0
7
- data.tar.gz: efa7dbc2609e25b0d25bb0d93c7d877223808d88343299e7dfd60ed9442eb31806347a4aa1f255452bfc219af2a5d4c34b66f5d50346d839de59ee256dafe0da
6
+ metadata.gz: 4861baa93673811f488bdb34f33826dd9a204eabda55f8b0a2064a2eee1e021e36c49bba8ee597d0ac832c5368bcf572d42adfee257325f46e334ad4e3e7d846
7
+ data.tar.gz: '08cd8dc7729a1a3eaac787d85e6fdea0bb9001e1bd449e2c31dc57ebc9c2d327ab505f62d6dc647f99dad1d356a3fab3efc6ac3e1c5429e70589581e5114d741'
@@ -25,102 +25,144 @@ static VALUE cr_key_str(const char* p, size_t len) {
25
25
  return rb_enc_interned_str(p, (long)len, cr_utf8_enc);
26
26
  }
27
27
 
28
+ #include "cbor/sink.h"
28
29
  #include "dom/dom.h"
29
30
  #include "doc.h" /* the public YeptrisDocument wrapper: ->dom */
30
31
  #include <yeptris/cbor.h>
31
32
  #include <yeptris/dom.h>
32
33
  #include <yeptris/resolve.h>
33
34
 
34
- static const char* cr_view(const yep_dom* d, yep_sview sv, uint32_t* len) {
35
- *len = sv.len;
36
- if (sv.len == 0) {
37
- return "";
38
- }
39
- return (sv.off & YEP_SV_INPUT) ? (d->str + (sv.off & YEP_SV_OFF)) : (d->input_base + sv.off);
35
+ /* ---- the VALUE sink (#157): bytes -> VALUE in ONE pass ----
36
+ *
37
+ * The decode grammar drives; these callbacks build the same VALUEs
38
+ * the old DOM + cr_walk pair produced (verified by the binding's
39
+ * differential suite): nil/true/false by tag, ints via LL2NUM (the
40
+ * INT64_MIN text parity wart kept), floats via DBL2NUM (no
41
+ * text round trip), strings copied utf8 (byte strings today ride the
42
+ * same utf8 String), map keys interned (values never are).
43
+ *
44
+ * GC: the container stack is a Ruby array (every VALUE reachable);
45
+ * the input buffer is read directly, so decode runs with GC disabled
46
+ * (the caller's String may move otherwise — the #160 bus error). */
47
+
48
+ struct rv_ctx {
49
+ VALUE stack; /* [.., container, (pending-key)?] */
50
+ VALUE root; /* Qnil until the top item closes */
51
+ };
52
+
53
+ static VALUE rv_top(const struct rv_ctx* c) {
54
+ long n = RARRAY_LEN(c->stack);
55
+ return n > 0 ? rb_ary_entry(c->stack, n - 1) : Qnil;
40
56
  }
41
57
 
42
- static VALUE cr_scalar(const yep_dom* d, const yep_dnode* n) {
43
- uint32_t len = 0;
44
- const char* p = cr_view(d, n->value, &len);
45
- switch (n->tag_id) {
46
- case YEPTRIS_TAG_NULL:
47
- return Qnil;
48
- case YEPTRIS_TAG_BOOL:
49
- if (len == 1) { /* Psych: "y"/"n" stay Strings */
50
- return rb_utf8_str_new(p, len);
51
- }
52
- return (len == 4 && memcmp(p, "true", 4) == 0) ? Qtrue : Qfalse;
53
- case YEPTRIS_TAG_INT: {
54
- char buf[32];
55
- size_t cp = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
56
- memcpy(buf, p, cp);
57
- buf[cp] = '\0';
58
- char* end = NULL;
59
- long long v = strtoll(buf, &end, 10);
60
- if (end != buf && *end == '\0' && end == buf + len && v > INT64_MIN) {
61
- return LL2NUM(v);
62
- }
63
- return rb_utf8_str_new(p, len); /* int_or_string's fallback */
58
+ static VALUE rv_attach(struct rv_ctx* c, VALUE v) {
59
+ VALUE top = rv_top(c);
60
+ if (RB_TYPE_P(top, T_ARRAY)) {
61
+ rb_ary_push(top, v);
62
+ return v;
64
63
  }
65
- case YEPTRIS_TAG_FLOAT: {
66
- char buf[64];
67
- size_t cp = len < sizeof(buf) - 1 ? len : sizeof(buf) - 1;
68
- memcpy(buf, p, cp);
69
- buf[cp] = '\0';
70
- char* end = NULL;
71
- double v = strtod(buf, &end);
72
- if (end != buf && end != buf + len) {
73
- return rb_utf8_str_new(p, len); /* float_or_string's fallback */
74
- }
75
- return DBL2NUM(v);
64
+ if (RB_TYPE_P(top, T_HASH)) {
65
+ rb_ary_push(c->stack, v); /* becomes the pending key */
66
+ return v;
76
67
  }
77
- default:
78
- return rb_utf8_str_new(p, len);
68
+ /* a pending key: pop it, land v in the map */
69
+ VALUE k = rb_ary_pop(c->stack);
70
+ VALUE map = rv_top(c);
71
+ if (RB_TYPE_P(map, T_HASH)) {
72
+ rb_hash_aset(map, k, v);
73
+ return v;
79
74
  }
75
+ /* the stack emptied: v is the root */
76
+ c->root = v;
77
+ return v;
80
78
  }
81
79
 
82
- static VALUE cr_walk(const yep_dom* d, uint32_t id) {
83
- const yep_dnode* n = &d->nodes[id];
84
- switch (n->kind) {
85
- case YEP_DOM_SCALAR:
86
- return cr_scalar(d, n);
87
- case YEP_DOM_SEQUENCE: {
88
- long cnt = (long)n->count;
89
- VALUE a = rb_ary_new_capa(cnt);
90
- uint32_t c = n->first_child;
91
- for (long i = 0; i < cnt && c != UINT32_MAX; i++) {
92
- rb_ary_push(a, cr_walk(d, c));
93
- c = d->nodes[c].next_sibling;
94
- }
95
- return a;
80
+ static int rv_text(void* ctx, const char* s, uint32_t n, uint8_t tag_id, int is_key,
81
+ const char* tag, uint32_t tag_len) {
82
+ (void)tag;
83
+ (void)tag_len;
84
+ struct rv_ctx* c = ctx;
85
+ VALUE v;
86
+ switch (tag_id) {
87
+ case YEPTRIS_TAG_NULL:
88
+ v = Qnil;
89
+ break;
90
+ case YEPTRIS_TAG_BOOL:
91
+ v = (n == 4 && memcmp(s, "true", 4) == 0) ? Qtrue : Qfalse;
92
+ break;
93
+ default: /* rendered text (diagnostics, simple(N), key ints) */
94
+ v = rb_utf8_str_new(s, (long)n);
95
+ break;
96
96
  }
97
- case YEP_DOM_MAPPING: {
98
- long pairs = (long)(n->count / 2);
99
- VALUE h = HASH_NEW_CAPA(pairs);
100
- uint32_t c = n->first_child;
101
- for (long i = 0; i < pairs && c != UINT32_MAX; i++) {
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
- }
113
- VALUE v = (c != UINT32_MAX) ? cr_walk(d, c) : Qnil;
114
- if (c != UINT32_MAX) {
115
- c = d->nodes[c].next_sibling;
116
- }
117
- rb_hash_aset(h, k, v);
118
- }
119
- return h;
97
+ (void)is_key;
98
+ rv_attach(c, v);
99
+ return 1;
100
+ }
101
+
102
+ static int rv_int(void* ctx, int negative, uint64_t mag, int is_key, const char* tag,
103
+ uint32_t tag_len) {
104
+ (void)tag;
105
+ (void)tag_len;
106
+ struct rv_ctx* c = ctx;
107
+ VALUE v;
108
+ if (negative && mag == (uint64_t)1 << 63) {
109
+ /* parity: the DOM path's strtoll guard renders INT64_MIN a
110
+ * String today; the differential suite pins it */
111
+ v = rb_utf8_str_new("-9223372036854775808", 20);
112
+ } else {
113
+ v = LL2NUM(negative ? -(int64_t)mag : (int64_t)mag);
120
114
  }
121
- default: /* ALIAS: CBOR decode never produces one */
122
- return Qnil;
115
+ rv_attach(c, v);
116
+ return 1;
117
+ }
118
+
119
+ static int rv_float(void* ctx, double dv, int is_key, const char* tag, uint32_t tag_len) {
120
+ (void)is_key;
121
+ (void)tag;
122
+ (void)tag_len;
123
+ rv_attach(ctx, DBL2NUM(dv));
124
+ return 1;
125
+ }
126
+
127
+ static int rv_str(void* ctx, const unsigned char* p, uint32_t n, int borrowed, int is_key,
128
+ const char* tag, uint32_t tag_len) {
129
+ (void)borrowed;
130
+ (void)tag;
131
+ (void)tag_len;
132
+ struct rv_ctx* c = ctx;
133
+ VALUE v = is_key ? rb_enc_interned_str((const char*)p, (long)n, cr_utf8_enc)
134
+ : rb_utf8_str_new((const char*)p, (long)n);
135
+ rv_attach(c, v);
136
+ return 1;
137
+ }
138
+
139
+ static int rv_bytes(void* ctx, const unsigned char* p, uint32_t n, int borrowed, int is_key,
140
+ const char* tag, uint32_t tag_len) {
141
+ /* parity: byte strings ride the same utf8 String today */
142
+ return rv_str(ctx, p, n, borrowed, is_key, tag, tag_len);
143
+ }
144
+
145
+ static int rv_open(void* ctx, int is_map, uint64_t cap, const char* tag, uint32_t tag_len) {
146
+ (void)tag;
147
+ (void)tag_len;
148
+ struct rv_ctx* c = ctx;
149
+ VALUE v;
150
+ if (is_map) {
151
+ v = (cap != UINT64_MAX && cap / 2 <= 4096) ? HASH_NEW_CAPA((long)(cap / 2))
152
+ : rb_hash_new();
153
+ } else {
154
+ v = (cap != UINT64_MAX && cap <= 8192) ? rb_ary_new_capa((long)cap) : rb_ary_new();
123
155
  }
156
+ rb_ary_push(c->stack, v);
157
+ return 1;
158
+ }
159
+
160
+ static int rv_close(void* ctx, int is_map) {
161
+ (void)is_map;
162
+ struct rv_ctx* c = ctx;
163
+ VALUE v = rb_ary_pop(c->stack);
164
+ rv_attach(c, v);
165
+ return 1;
124
166
  }
125
167
 
126
168
  VALUE yep_rb_cbor_load(const char* p, size_t len, int strict) {
@@ -128,27 +170,24 @@ VALUE yep_rb_cbor_load(const char* p, size_t len, int strict) {
128
170
  cr_utf8_enc = rb_utf8_encoding();
129
171
  }
130
172
  YeptrisStatus st = YEPTRIS_OK;
131
- /* the DOM borrows the input buffer zero-copy; the walk ALLOCATES,
132
- * and a GC compaction mid-walk moves the caller's String — the
133
- * borrowed base dangles (the #160 CI bus error on 3.2/linux).
134
- * Same discipline as the JSON walk: GC off across decode+walk */
173
+ /* the decoder reads the caller's buffer directly; GC off across
174
+ * the pass (a compaction would move the String under p — the
175
+ * #160 bus error) */
135
176
  VALUE gc_on = rb_gc_disable();
136
- /* YeptrisDocument is a WRAPPER (doc.h) — ->dom is the tree; a raw
137
- * yep_dom* cast read the wrapper's fields as the dom (the CI
138
- * segfaults: docs at the wrong offset) */
139
- yeptris_document* doc = (yeptris_document*)yeptris_cbor_decode(
140
- p, len, strict ? YEPTRIS_CBOR_STRICT : 0, &st);
141
- yep_dom* d = (doc != NULL) ? doc->dom : NULL;
142
- if (d == NULL) {
143
- if (RTEST(gc_on)) {
144
- rb_gc_enable();
145
- }
146
- return Qundef;
147
- }
148
- VALUE v = (d->dcount > 0 && d->docs[0] != UINT32_MAX) ? cr_walk(d, d->docs[0]) : Qnil;
149
- yeptris_document_free((YeptrisDocument)doc);
177
+ struct rv_ctx ctx;
178
+ ctx.stack = rb_ary_new();
179
+ ctx.root = Qnil;
180
+ static const yep_cbor_sink RV_SINK = {
181
+ NULL, rv_text, rv_int, rv_float, rv_str, rv_bytes, rv_open, rv_close,
182
+ };
183
+ yep_cbor_sink sink = RV_SINK;
184
+ sink.ctx = &ctx;
185
+ st = yep_cbor_decode_gen((const unsigned char*)p, len, strict, NULL, &sink);
150
186
  if (RTEST(gc_on)) {
151
187
  rb_gc_enable();
152
188
  }
153
- return v;
189
+ if (st != YEPTRIS_OK) {
190
+ return Qundef;
191
+ }
192
+ return ctx.root;
154
193
  }
@@ -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/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 = STRICT_DUPLICATE_KEYS ? [{}] : nil
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)
@@ -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).
@@ -299,13 +312,14 @@ module Yeptris
299
312
  # handles GC-free. (The block-yielding form is tracked under
300
313
  # #179: the current implementation materializes eagerly; the
301
314
  # block is only honored as a no-op convenience.)
302
- _ = block # reserved for the future yielding form (#179)
303
315
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
304
316
  return nil if doc.nil? || doc.document_count.zero?
305
317
 
306
318
  stream = Nodes::Stream.new
307
319
  (0...doc.document_count).each do |i|
308
- stream.children << Nodes::Builder.document_stream_child(doc, i)
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)
309
323
  end
310
324
  # ownership: the DOCUMENT wrapper is the sole owner — its own
311
325
  # finalizer (pointer-only closure) frees the C memory; the
@@ -450,6 +464,10 @@ module Yeptris
450
464
  # tree stay valid while the tree is reachable; a GC finalizer
451
465
  # releases the C memory when it is not.
452
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
453
471
  attr_reader :version, :tags
454
472
 
455
473
  def initialize(version = [], tags = {})
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.16.1".freeze
6
+ VERSION = "0.6.17.1".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
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.16
6
+ VERSION 0.6.17
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -65,7 +65,9 @@ enum {
65
65
  (default: last, as at parse) */
66
66
  };
67
67
 
68
- /* One plan node. 32 bytes, ABI-pinned. */
68
+ /* One plan node. ABI-pinned: a pointer plus 16 fixed-width bytes —
69
+ * 24 on LP64, 20 on 32-bit (the hosts' FFI mirrors size :pointer
70
+ * per-process, so the layout contract is the POINTER plus the tail). */
69
71
  typedef struct yeptris_desc_node {
70
72
  const char* wire_name; /* mapping key to match (NULL: the root) */
71
73
  uint8_t kind; /* YEP_SK_* */
@@ -77,7 +79,7 @@ typedef struct yeptris_desc_node {
77
79
  uint32_t reserved; /* must be 0 (ABI headroom) */
78
80
  } yeptris_desc_node;
79
81
 
80
- YEPTRIS_CT_ASSERT(sizeof(yeptris_desc_node) == 24);
82
+ YEPTRIS_CT_ASSERT(sizeof(yeptris_desc_node) == sizeof(const char*) + 16);
81
83
 
82
84
  /* One column's output. Caller allocates the payload array (sized by
83
85
  * expectation); the call fills it and sets count. No per-value FFI: