yeptris 0.6.15.1 → 0.6.15.2

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: 0033737c829c7beb3ae83ca816033386b4fe23b53968f2b0ff40a74c97aac469
4
- data.tar.gz: c27accdae4e4807500d4edbfa39228b003c70e5380de3f259bd5315dc9a2dc6f
3
+ metadata.gz: b1326388a6f59887d9b8f541ec6f4c835071ca05f4821d429a82b8acc1a990e2
4
+ data.tar.gz: 521f5abbcebb9611a0c67699b1952e72d3790d228fe606148f095d622f5b4387
5
5
  SHA512:
6
- metadata.gz: ca7d477d959df73f231e443f15ceef0403774129e2df0af49d94e2f926d24c380cbacf6e92d88e9488715c028de0533e93fa82ca039ee129dd13b90f4b4badd0
7
- data.tar.gz: 54b35dfe9c5399157ffbbb1e33b058afbe523e36222771d000ff697ccaccd059d5b049a3383ac2fbad61086d4548d3bc23343f600311c4c8438535d2a87cc2a8
6
+ metadata.gz: 7b6e36f35a32d480af8635023bbaf173194e8295e34851ae53ec8121cdf872e9d71c4877f70cc94d1ee62d0d22e4f74e868edc7746b2fbbd9d66af47f31daad0
7
+ data.tar.gz: 21a442ef4c99c57f46b837af822db3d98434c476b5a0b7c56d52f03bbfac0c8849be28b2a7fd2109a95b15a80ea6eda9116c3631ff5c6faff2d134e5b2a861fc
@@ -20,7 +20,7 @@ class Yeptris::Document
20
20
  # always yields the same Ruby object (identity for aliases/eql?),
21
21
  # cleared at free — no stale entries, no GC-race weak maps.
22
22
  @wrapper_cache = {}
23
- ObjectSpace.define_finalizer(self, self.class.finalize(c_ptr, freed)) unless c_ptr.null?
23
+ ObjectSpace.define_finalizer(self, self.class.finalize(c_ptr)) unless c_ptr.null?
24
24
  end
25
25
 
26
26
  def self.parse(yaml, schema: :core_12, max_depth: 0)
@@ -88,6 +88,9 @@ class Yeptris::Document
88
88
  return if @freed.state == :freed
89
89
 
90
90
  @freed.state = :freed
91
+ # stop the GC finalizer BEFORE the C free — it captures the same
92
+ # pointer, and its firing after this free is the double free
93
+ ObjectSpace.undefine_finalizer(self)
91
94
  @wrapper_cache.clear
92
95
  Yeptris::FFI.yeptris_document_free(@c_ptr)
93
96
  end
@@ -221,12 +224,15 @@ class Yeptris::Document
221
224
  self
222
225
  end
223
226
 
224
- # GC safety net: an explicit #free already ran is fine; a miss here
225
- # frees C memory that would otherwise leak.
226
- def self.finalize(c_ptr, freed)
227
- proc do
228
- Yeptris::FFI.yeptris_document_free(c_ptr) if freed.state == :alive
229
- freed.state = :freed
230
- end
227
+ # GC safety net (#182): the proc captures ONLY the raw pointer no
228
+ # Ruby objects, no wrapper, no freed-flag struct. A finalizer that
229
+ # closes over the owning wrapper races that wrapper's own sweep
230
+ # (the classic use-after-free: freed?/free against half-collected
231
+ # ivars, then a garbage C pointer into yeptris_document_free →
232
+ # SIGABRT). Explicit #free undefines this finalizer BEFORE the C
233
+ # free, so the double free cannot happen; finalizers run on the
234
+ # main thread, so there is no cross-thread window.
235
+ def self.finalize(c_ptr)
236
+ proc { Yeptris::FFI.yeptris_document_free(c_ptr) }
231
237
  end
232
238
  end
@@ -115,7 +115,11 @@ module Yeptris
115
115
  end
116
116
 
117
117
  def parse_timestamp(v)
118
- return Date.parse(v) unless v.match?(/[Tt ]\d/)
118
+ # stdlib scalar_scanner: date-only strings parse with
119
+ # strptime('%F', Date::GREGORIAN) — the PROLEPTIC calendar —
120
+ # not Date.parse's default ITALY reform (a 1582 cycle fails
121
+ # the ported test_julian_date otherwise)
122
+ return ::Date.strptime(v, "%F", ::Date::GREGORIAN) unless v.match?(/[Tt ]\d/)
119
123
 
120
124
  # normalize the YAML 1.1 space forms onto iso8601 for
121
125
  # xmlschema: "2001-12-14 21:59:43.10 -05:00" ->
data/lib/yeptris/node.rb CHANGED
@@ -394,6 +394,15 @@ class Yeptris::Node
394
394
  def scalar_to_ruby
395
395
  return symbolize if symbol? && tag_id == :str
396
396
 
397
+ # stdlib deserialize's tagged-scalar arms: the class tag carries
398
+ # DateTime (dumped tagged — a plain timestamp re-loads as Time)
399
+ if tag == "!ruby/object:DateTime"
400
+ ts = value.to_s.sub(/ (\d)/, 'T\\1').sub(/ ([+-]\d)/, '\\1')
401
+ t = ::Time.xmlschema(ts)
402
+ return ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec,
403
+ Rational(t.utc_offset, 86_400)) + Rational(t.subsec, 86_400)
404
+ end
405
+
397
406
  case tag_id
398
407
  when :null then nil
399
408
  when :bool then to_bool
@@ -66,11 +66,34 @@ module Yeptris
66
66
  end
67
67
  end
68
68
 
69
+ # stdlib register()s every object — scalars included — so a
70
+ # repeated scalar dumps &name/*name (test_float_references,
71
+ # test_alias_with_time). Containers already route through
72
+ # anchor_for; this is the scalar arm of the same machinery.
73
+ def anchored_scalar(obj, text: nil, tag: nil)
74
+ state, name = anchor_for(obj)
75
+ return alias_of(obj, name) if state == :alias
76
+
77
+ n = scalar(text || obj, tag: tag)
78
+ n.set_anchor(name) if name
79
+ remember(obj, n)
80
+ n
81
+ end
82
+
69
83
  def visit(obj)
70
84
  case obj
85
+ # strings/floats/ints: stdlib yaml_tree does NOT anchor them
86
+ # (its float test is load-side only) — and CBOR encode
87
+ # rejects the anchors the cbor profile feeds it
71
88
  when nil, true, false, ::Integer, ::Float, ::String then scalar(obj)
72
89
  when ::Symbol then @tree.new_scalar(":#{obj}", :plain) # psych emits symbols bare, never through visit_String's quoting rules
73
- when ::Date, ::Time then scalar(obj) # timestamp text, never ivars
90
+ # stdlib visit_DateTime: the class tag carries the type (a
91
+ # plain timestamp re-loads as Time) — "!ruby/object:DateTime
92
+ # 2017-04-13 12:00:00.500000000 +09:00", to_s text, one line
93
+ when ::DateTime
94
+ anchored_scalar(obj, text: obj.strftime("%F %H:%M:%S.%9N %:z"),
95
+ tag: "!ruby/object:DateTime")
96
+ when ::Date, ::Time then anchored_scalar(obj) # timestamp text, never ivars
74
97
  when ::Hash then visit_hash(obj)
75
98
  when ::Array then visit_array(obj)
76
99
  when ::Struct then visit_struct(obj)
@@ -101,7 +124,7 @@ module Yeptris
101
124
  if @refs[oid] < 2
102
125
  return [:none, nil]
103
126
  end
104
- name = "o#{@counter += 1}"
127
+ name = (@counter += 1).to_s # stdlib: &1, *1
105
128
  @names[oid] = name
106
129
  [:new, name]
107
130
  end
@@ -156,7 +179,7 @@ module Yeptris
156
179
  # neutral surface alone was pinned)
157
180
  ::Yeptris::YAML::BulkBuilder.time_text(obj)
158
181
  elsif obj.is_a?(::Date)
159
- obj.iso8601 # canonical calendar form (DateTime rides here too)
182
+ obj.iso8601 # canonical calendar form
160
183
  elsif obj.nil?
161
184
  "" # libyaml's null rendering rides bare (issue #290)
162
185
  elsif obj.is_a?(::Float)
@@ -165,7 +188,7 @@ module Yeptris
165
188
  obj.to_s
166
189
  end
167
190
  n =
168
- if obj.is_a?(::String)
191
+ if obj.is_a?(::String) && !tag
169
192
  ::Yeptris::YAML::Builder.build_string(@tree, obj)
170
193
  else
171
194
  @tree.new_scalar(text, :plain)
@@ -220,7 +243,7 @@ module Yeptris
220
243
  m = @tree.new_mapping
221
244
  remember(strct, m)
222
245
  m.set_anchor(name) if name
223
- m.set_tag(strct.class.name ? "!ruby/struct:#{strct.class.name}" : "!ruby/struct")
246
+ m.set_tag(strct.class.name.to_s.empty? ? "!ruby/struct" : "!ruby/struct:#{strct.class.name}")
224
247
  strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
225
248
  m
226
249
  end
@@ -326,6 +349,9 @@ module Yeptris
326
349
  when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
327
350
  else node&.to_ruby
328
351
  end
352
+ # stdlib register(): an anchored node's result is THE object
353
+ # for every alias to that anchor (scalars included)
354
+ anchors[node.anchor] = result if node.respond_to?(:anchor) && node.anchor
329
355
  # the domain-types post-pass (stdlib ToRuby#accept's tail):
330
356
  # registered blocks transform the revived result per node
331
357
  if node && !::Yeptris::Psych.domain_types.empty?
data/lib/yeptris/psych.rb CHANGED
@@ -299,10 +299,11 @@ module Yeptris
299
299
  (0...doc.document_count).each do |i|
300
300
  stream.children << Nodes::Builder.document_stream_child(doc, i)
301
301
  end
302
+ # ownership: the DOCUMENT wrapper is the sole owner — its own
303
+ # finalizer (pointer-only closure) frees the C memory; the
304
+ # stream merely references it. A tree-side finalizer closing
305
+ # over the wrapper raced the wrapper's sweep (#182's abort).
302
306
  stream.owner = doc
303
- ObjectSpace.define_finalizer(
304
- stream, proc { doc.free unless doc.freed? }
305
- )
306
307
  stream
307
308
  rescue Yeptris::ParseError => e
308
309
  raise SyntaxError.from_parse_error(e)
@@ -327,6 +328,11 @@ module Yeptris
327
328
  # data anyway
328
329
  out =
329
330
  case obj
331
+ # DateTime < Date in ruby's hierarchy — the fast scalar
332
+ # path would emit it tag-less iso8601 (re-loading as Time);
333
+ # the visitor carries stdlib's !ruby/object:DateTime tag
334
+ when ::DateTime
335
+ Visitors::YAMLTree.new.push(obj).finish
330
336
  when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
331
337
  Yeptris::YAML.dump(obj, header: true)
332
338
  else
@@ -442,12 +448,13 @@ module Yeptris
442
448
  @tags = tags
443
449
  end
444
450
 
445
- # @api private — transfer of C ownership to this tree
451
+ # @api private — attach the document: the tree REFERENCES it
452
+ # (keeping the C memory alive while the tree is reachable);
453
+ # the wrapper's own finalizer is the sole freer (#182: a
454
+ # tree-side finalizer closing over the wrapper raced the
455
+ # wrapper's sweep — SIGABRT under document churn)
446
456
  def own(yeptris_doc)
447
457
  @owner = yeptris_doc
448
- ObjectSpace.define_finalizer(
449
- self, proc { yeptris_doc.free unless yeptris_doc.freed? }
450
- )
451
458
  self
452
459
  end
453
460
 
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.15.1".freeze
6
+ VERSION = "0.6.15.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
@@ -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
- const yep_dom* dom = ((yeptris_node*)handle)->doc->dom;
650
- uint32_t id = n->first_child;
651
- for (size_t i = 0; i < index && id != UINT32_MAX; i++) {
652
- const yep_dnode* cur = yep_dom_node(dom, id);
653
- id = cur ? cur->next_sibling : UINT32_MAX;
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
- const yep_dom* d = h->doc->dom;
689
- uint32_t child = n->first_child;
690
- for (size_t i = 0; i < index * 2; i++) {
691
- child = d->nodes[child].next_sibling; /* pairs are key,value,… */
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,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yeptris
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.15.1
4
+ version: 0.6.15.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.