yeptris 0.6.14.1-aarch64-linux → 0.6.15.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1037c0cb4941d26071d8f9318f3440081b1537974923aac94a77dc4373dbc24e
4
- data.tar.gz: 103b51efe91818653c2f6161b888f124283dac39122550964108db991ec4838f
3
+ metadata.gz: 419470a0e038bec65880a3d764da46f072d7993e5aca9de71825a8428d5b2f98
4
+ data.tar.gz: 19cf714c7f80abc07d8729420700ef2c08bf1a140de03c3e11a8bb6ff177a4f4
5
5
  SHA512:
6
- metadata.gz: c444b61b4492a684de9d5146efd71e12fe2bd2b392ef2ab9e37760e3b118c5f61a2665b0007b2deac9e9bc552144f5b7e1a49fe6f6e772310812a5e3296c1052
7
- data.tar.gz: 12bd118139e3ee5cc8e97b18ef1c50b836d4e308517b0d0568ffd230bd192ac518626aafeb8d66acbac3b2d8c80db3296f180f0ec72dad2f9b75889d51692417
6
+ metadata.gz: 9d11c53e5305c76f780f431d73be9c62228e250ab164c4fd636429ad4fca4e3705b1d46b0e9cf7abed5d8a0662d0347a2399de0ce3a16f5eec987d05d0fb053f
7
+ data.tar.gz: 5c24e95902b4ae44595710274ede3efbd605885d8cbc0747fc0965fcb8a2addb32ea8f18648606fef823d09a6f106d777d175f4e8ed0f1c8e74810f22d6cbee6
@@ -20,24 +20,29 @@ 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)
27
27
  yaml = Yeptris.read_input(yaml)
28
28
  yaml = yaml.to_s
29
+ status = ::FFI::MemoryPointer.new(:int)
29
30
  doc =
30
31
  if schema == :core_12 && max_depth.zero?
31
- Yeptris::FFI.yeptris_parse(yaml, yaml.bytesize, nil)
32
+ Yeptris::FFI.yeptris_parse(yaml, yaml.bytesize, status)
32
33
  else
33
34
  opts = Yeptris::FFI::ParseOptions.new
34
35
  opts[:schema] = schema == :compat_11 ? Yeptris::FFI::SCHEMA_11_COMPAT : Yeptris::FFI::SCHEMA_12_CORE
35
36
  opts[:max_depth] = max_depth
36
- Yeptris::FFI.yeptris_parse_ex(yaml, yaml.bytesize, opts, nil)
37
+ Yeptris::FFI.yeptris_parse_ex(yaml, yaml.bytesize, opts, status)
37
38
  end
38
39
  if doc.null?
39
- # the status out-param is skipped: the thread-local error
40
- # channel carries the failure detail (measurable on small docs)
40
+ # NULL + OK is the legal empty stream (no documents): nil, like
41
+ # stdlib. The status out-param is the discriminator the TLS
42
+ # error message cannot serve (a stale error from an earlier
43
+ # parse persists on the thread).
44
+ return nil if status.read_int.zero?
45
+
41
46
  raise Yeptris::ParseError,
42
47
  "parse failed: #{Yeptris::FFI.last_error_message}"
43
48
  end
@@ -83,6 +88,9 @@ class Yeptris::Document
83
88
  return if @freed.state == :freed
84
89
 
85
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)
86
94
  @wrapper_cache.clear
87
95
  Yeptris::FFI.yeptris_document_free(@c_ptr)
88
96
  end
@@ -216,12 +224,15 @@ class Yeptris::Document
216
224
  self
217
225
  end
218
226
 
219
- # GC safety net: an explicit #free already ran is fine; a miss here
220
- # frees C memory that would otherwise leak.
221
- def self.finalize(c_ptr, freed)
222
- proc do
223
- Yeptris::FFI.yeptris_document_free(c_ptr) if freed.state == :alive
224
- freed.state = :freed
225
- 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) }
226
237
  end
227
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
@@ -276,6 +276,31 @@ class Yeptris::Node
276
276
  alive { Yeptris::FFI.yeptris_node_id(@c_ptr) }
277
277
  end
278
278
 
279
+ # The marshal fast path (TODO.restructure/21; #178): one C call
280
+ # turns this subtree into Marshal 4.8 bytes — Marshal.load builds
281
+ # the Ruby objects in C, no per-node FFI. nil when the document
282
+ # carries constructs the format cannot express (merge keys,
283
+ # timestamps, tagged revivals): the caller walks instead. Raises
284
+ # ParseError on engine errors (not on the bail).
285
+ def marshal_fast
286
+ return nil unless Yeptris::FFI::MARSHAL
287
+
288
+ out_p = ::FFI::MemoryPointer.new(:pointer)
289
+ olen_p = ::FFI::MemoryPointer.new(:size_t)
290
+ st = alive { Yeptris::FFI.yeptris_marshal_node(@c_ptr, out_p, olen_p) }
291
+ if st == Yeptris::FFI::ERROR_UNSUPPORTED
292
+ nil
293
+ elsif st != Yeptris::FFI::OK
294
+ raise Yeptris::ParseError, Yeptris::FFI.last_error_message
295
+ else
296
+ bytes = out_p.read_pointer.read_bytes(olen_p.read_uint64)
297
+ bytes.force_encoding(Encoding::ASCII_8BIT)
298
+ ::Marshal.load(bytes)
299
+ end
300
+ ensure
301
+ Yeptris::FFI.yeptris_marshal_free(out_p.read_pointer) if out_p
302
+ end
303
+
279
304
  def to_ruby(memo = nil)
280
305
  # readonly documents memoize per node: a second materialization
281
306
  # returns the SAME object (the readonly cache is the ground truth
@@ -293,25 +318,7 @@ class Yeptris::Node
293
318
  # Falls back to the per-node FFI walk on constructs the format
294
319
  # cannot express (merge keys, timestamps) and on older builds.
295
320
  if Yeptris::FFI::MARSHAL && memo.nil?
296
- result =
297
- begin
298
- out_p = ::FFI::MemoryPointer.new(:pointer)
299
- olen_p = ::FFI::MemoryPointer.new(:size_t)
300
- st = Yeptris::FFI.yeptris_marshal_node(@c_ptr, out_p, olen_p)
301
- if st == Yeptris::FFI::ERROR_UNSUPPORTED
302
- nil
303
- elsif st != Yeptris::FFI::OK
304
- raise Yeptris::ParseError, Yeptris::FFI.last_error_message
305
- else
306
- buf = out_p.read_pointer
307
- len = olen_p.read_uint64
308
- bytes = buf.read_bytes(len)
309
- bytes.force_encoding(Encoding::ASCII_8BIT)
310
- ::Marshal.load(bytes)
311
- end
312
- ensure
313
- Yeptris::FFI.yeptris_marshal_free(out_p.read_pointer) if out_p
314
- end
321
+ result = marshal_fast
315
322
  unless result.nil?
316
323
  @document.readonly_memo[node_id] = result if @document.readonly?
317
324
  return result
@@ -332,7 +339,32 @@ class Yeptris::Node
332
339
  h = {}
333
340
  memo[node_id] = h
334
341
  each_pair do |k, v|
335
- h[k.to_ruby_key] = v.to_ruby(memo)
342
+ key = k.to_ruby_key
343
+ val = v.to_ruby(memo)
344
+ if key == "<<" && k.tag != "tag:yaml.org,2002:str"
345
+ # stdlib revive_hash's merge branch: the VALUE's node kind
346
+ # picks the arm; every merge is TypeError-guarded (a bad
347
+ # element keeps the whole '<<' pair literal)
348
+ if v.kind == :alias || v.mapping?
349
+ begin
350
+ h.merge!(val)
351
+ rescue ::TypeError
352
+ h[key] = val
353
+ end
354
+ elsif v.sequence?
355
+ begin
356
+ merged = {}
357
+ val.reverse_each { |e| merged.merge!(e) }
358
+ h.merge!(merged)
359
+ rescue ::TypeError
360
+ h[key] = val
361
+ end
362
+ else
363
+ h[key] = val
364
+ end
365
+ else
366
+ h[key] = val
367
+ end
336
368
  end
337
369
  h
338
370
  when :sequence
@@ -342,7 +374,11 @@ class Yeptris::Node
342
374
  a
343
375
  when :alias
344
376
  t = alias_target
345
- t.nil? ? nil : t.to_ruby(memo)
377
+ if t.nil?
378
+ raise ::Yeptris::Psych::AnchorNotDefined,
379
+ "Unknown anchor: #{anchor}"
380
+ end
381
+ t.to_ruby(memo)
346
382
  else
347
383
  scalar_to_ruby
348
384
  end
@@ -358,6 +394,15 @@ class Yeptris::Node
358
394
  def scalar_to_ruby
359
395
  return symbolize if symbol? && tag_id == :str
360
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
+
361
406
  case tag_id
362
407
  when :null then nil
363
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)
@@ -188,6 +211,13 @@ module Yeptris
188
211
  key_node = @tree.new_scalar("", :single_quoted)
189
212
  key_node.set_tag("!")
190
213
  m.map_add_node(key_node, visit(v))
214
+ elsif k.is_a?(::String) && k == "<<"
215
+ # stdlib yaml_tree: a literal chevron key carries the
216
+ # explicit !!str tag (single-quoted) so it does not
217
+ # re-load as a merge
218
+ key_node = @tree.new_scalar("<<", :single_quoted)
219
+ key_node.set_tag("!!str")
220
+ m.map_add_node(key_node, visit(v))
191
221
  else
192
222
  m.map_add(key_text(k), visit(v))
193
223
  end
@@ -213,7 +243,7 @@ module Yeptris
213
243
  m = @tree.new_mapping
214
244
  remember(strct, m)
215
245
  m.set_anchor(name) if name
216
- 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}")
217
247
  strct.each_pair { |k, v| m.map_add(key_text(k), visit(v)) }
218
248
  m
219
249
  end
@@ -319,6 +349,9 @@ module Yeptris
319
349
  when ::Yeptris::Psych::Nodes::Mapping then visit_mapping(node)
320
350
  else node&.to_ruby
321
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
322
355
  # the domain-types post-pass (stdlib ToRuby#accept's tail):
323
356
  # registered blocks transform the revived result per node
324
357
  if node && !::Yeptris::Psych.domain_types.empty?
@@ -415,10 +448,38 @@ module Yeptris
415
448
  end
416
449
  end
417
450
 
451
+ # stdlib visit_hash's merge branch, verbatim in structure: the
452
+ # key's SHAPE decides (an explicit !!str '<<' is a literal key,
453
+ # not a merge), the VALUE's node kind picks the arm, and every
454
+ # merge is TypeError-guarded (a bad element keeps the whole
455
+ # '<<' pair literal — merges are all-or-nothing).
418
456
  def hash_into(h, node)
419
457
  anchors[node.anchor] = h if node.anchor
420
458
  node.children.each_slice(2) do |k, v|
421
- h[visit(k)] = visit(v)
459
+ key = visit(k)
460
+ val = visit(v)
461
+ if key == "<<" && k.tag != "tag:yaml.org,2002:str"
462
+ case v
463
+ when ::Yeptris::Psych::Nodes::Alias, ::Yeptris::Psych::Nodes::Mapping
464
+ begin
465
+ h.merge!(val)
466
+ rescue ::TypeError
467
+ h[key] = val
468
+ end
469
+ when ::Yeptris::Psych::Nodes::Sequence
470
+ begin
471
+ merged = {}
472
+ val.reverse_each { |value| merged.merge!(value) }
473
+ h.merge!(merged)
474
+ rescue ::TypeError
475
+ h[key] = val
476
+ end
477
+ else
478
+ h[key] = val
479
+ end
480
+ else
481
+ h[key] = val
482
+ end
422
483
  end
423
484
  h
424
485
  end
data/lib/yeptris/psych.rb CHANGED
@@ -91,6 +91,9 @@ module Yeptris
91
91
  # object instance exists.
92
92
  autoload :Encodable, "yeptris/psych/encodable"
93
93
  class Error < StandardError; end
94
+
95
+ # stdlib psych 5: a *foo with no matching &foo anchor
96
+ class AnchorNotDefined < Error; end
94
97
  # Psych's exact interface (issue #32): same constructor arity,
95
98
  # same reader set (file/line/column/offset/problem/context), same
96
99
  # message shape — drop-in consumers' rescues and constructors
@@ -143,6 +146,31 @@ module Yeptris
143
146
  # !ruby/set, encode_with/init_with, alias identity. Plain-data
144
147
  # loads should use load/safe_load (the Materializer fast path).
145
148
  def unsafe_load(yaml, **)
149
+ # #178: the marshal fast path. Nodes::Builder's tree is per-node
150
+ # FFI (kind/tag/anchor/value + a walk per container) — the whole
151
+ # 11 MB relaton index paid ~9.6 s there. When the document is
152
+ # plain data (no timestamps/merge keys/tagged revivals), ONE C
153
+ # call materializes the tree as Marshal bytes and Marshal.load
154
+ # builds the objects in C (~1 s end to end). Construct-heavy
155
+ # documents keep the full revival walk below, byte for byte.
156
+ if Yeptris::FFI::MARSHAL && ::Yeptris::Psych.domain_types.empty?
157
+ begin
158
+ doc = ::Yeptris::Document.parse(yaml, schema: :compat_11)
159
+ return nil if doc.nil? || doc.document_count.zero?
160
+
161
+ root = doc.root(0)
162
+ fast = root&.marshal_fast
163
+ unless fast.nil?
164
+ doc.free
165
+ return fast
166
+ end
167
+ tree = Nodes::Builder.document(doc) # ownership: the tree frees
168
+ return Visitors::ToRuby.visit(tree.children.first)
169
+ rescue ::Yeptris::ParseError => e
170
+ doc&.free unless doc&.freed?
171
+ translate_parse_error(e)
172
+ end
173
+ end
146
174
  tree = parse(yaml)
147
175
  return nil if tree.nil?
148
176
 
@@ -150,11 +178,16 @@ module Yeptris
150
178
  end
151
179
 
152
180
  def safe_load(yaml, permitted_classes: [::Date, ::Time], aliases: false, **)
153
- doc = Yeptris::Document.parse(yaml, schema: :compat_11)
154
181
  begin
182
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
183
+ return nil if doc.nil? # the legal empty stream
184
+
155
185
  force_utf8_scalars(walk_safe(doc.root(0), permitted_classes, aliases))
186
+ rescue ::Yeptris::ParseError => e
187
+ doc&.free unless doc&.freed?
188
+ translate_parse_error(e)
156
189
  ensure
157
- doc.free
190
+ doc&.free
158
191
  end
159
192
  end
160
193
 
@@ -230,6 +263,8 @@ module Yeptris
230
263
  # children share one C document, so their handles would all
231
264
  # resolve to the first document's tree
232
265
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
266
+ return nil if doc.nil? # the legal empty stream
267
+
233
268
  begin
234
269
  (0...doc.document_count).map { |i| doc.root(i).to_ruby }
235
270
  ensure
@@ -238,9 +273,18 @@ module Yeptris
238
273
  end
239
274
 
240
275
  # The first document's node tree (no Ruby materialization).
276
+ def translate_parse_error(e)
277
+ # yeptris rejects *foo without &foo at PARSE time (stdlib
278
+ # raises at visit) — surface it as stdlib's class so
279
+ # consumers' rescues keep working
280
+ raise AnchorNotDefined, e.message if e.message.include?("undefined anchor")
281
+
282
+ raise SyntaxError.from_parse_error(e)
283
+ end
284
+
241
285
  def parse(yaml)
242
286
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
243
- return nil if doc.document_count.zero?
287
+ return nil if doc.nil? || doc.document_count.zero?
244
288
 
245
289
  Nodes::Builder.document(doc)
246
290
  rescue Yeptris::ParseError => e
@@ -249,16 +293,17 @@ module Yeptris
249
293
 
250
294
  def parse_stream(yaml)
251
295
  doc = Yeptris::Document.parse(yaml, schema: :compat_11)
252
- return nil if doc.document_count.zero?
296
+ return nil if doc.nil? || doc.document_count.zero?
253
297
 
254
298
  stream = Nodes::Stream.new
255
299
  (0...doc.document_count).each do |i|
256
300
  stream.children << Nodes::Builder.document_stream_child(doc, i)
257
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).
258
306
  stream.owner = doc
259
- ObjectSpace.define_finalizer(
260
- stream, proc { doc.free unless doc.freed? }
261
- )
262
307
  stream
263
308
  rescue Yeptris::ParseError => e
264
309
  raise SyntaxError.from_parse_error(e)
@@ -283,6 +328,11 @@ module Yeptris
283
328
  # data anyway
284
329
  out =
285
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
286
336
  when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
287
337
  Yeptris::YAML.dump(obj, header: true)
288
338
  else
@@ -398,12 +448,13 @@ module Yeptris
398
448
  @tags = tags
399
449
  end
400
450
 
401
- # @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)
402
456
  def own(yeptris_doc)
403
457
  @owner = yeptris_doc
404
- ObjectSpace.define_finalizer(
405
- self, proc { yeptris_doc.free unless yeptris_doc.freed? }
406
- )
407
458
  self
408
459
  end
409
460
 
@@ -33,6 +33,8 @@ class Yeptris::YAML::Descriptor < ::Yeptris::JSON::Descriptor
33
33
  def walk(yaml)
34
34
  src = ::Yeptris.read_input(yaml).to_s
35
35
  doc = ::Yeptris::Document.parse(src, schema: @schema)
36
+ return [] if doc.nil? # the legal empty stream
37
+
36
38
  begin
37
39
  st = ::FFI::MemoryPointer.new(:int)
38
40
  raw = ::Yeptris::FFI.yeptris_document_plan_walk(doc.c_ptr, @handle, st)
data/lib/yeptris/yaml.rb CHANGED
@@ -73,7 +73,7 @@ module Yeptris
73
73
  # Parses without materializing: the first document's root Node.
74
74
  def parse(yaml, schema: :core_12)
75
75
  doc = Document.parse(yaml, schema: schema)
76
- return nil if doc.document_count.zero?
76
+ return nil if doc.nil? || doc.document_count.zero?
77
77
 
78
78
  doc.root(0)
79
79
  end
@@ -177,6 +177,14 @@ module Yeptris
177
177
  off[0] += 1
178
178
  elsif k.is_a?(Symbol)
179
179
  scalar(":#{k}", STYLE_PLAIN, emit, blob, off)
180
+ elsif k.is_a?(String) && k == "<<"
181
+ # stdlib yaml_tree: a literal chevron key carries the
182
+ # explicit !!str tag (single-quoted) so it does not
183
+ # re-load as a merge
184
+ scalar("<<", STYLE_SQ, emit, blob, off)
185
+ emit.call(BUILD_TAG, 0, off[0], 5)
186
+ blob << "!!str"
187
+ off[0] += 5
180
188
  elsif k.is_a?(String)
181
189
  place(k, emit, blob, off, seen)
182
190
  else
@@ -343,6 +351,12 @@ module Yeptris
343
351
  key_node = doc.new_scalar("", :single_quoted)
344
352
  key_node.set_tag("!")
345
353
  m.map_add_node(key_node, build(doc, v, seen))
354
+ elsif k.is_a?(::String) && k == "<<"
355
+ # stdlib yaml_tree: a literal chevron key carries the
356
+ # explicit !!str tag so it does not re-load as a merge
357
+ key_node = doc.new_scalar("<<", :single_quoted)
358
+ key_node.set_tag("!!str")
359
+ m.map_add_node(key_node, build(doc, v, seen))
346
360
  else
347
361
  m.map_add(key_text(k), build(doc, v, seen))
348
362
  end
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.14.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
data/libyeptris.so CHANGED
Binary file
@@ -3,7 +3,7 @@
3
3
  cmake_minimum_required(VERSION 3.20)
4
4
 
5
5
  project(yeptris
6
- VERSION 0.6.14
6
+ VERSION 0.6.15
7
7
  DESCRIPTION "Ultra-fast YAML 1.2 parser, emitter and streamer in C"
8
8
  LANGUAGES C CXX
9
9
  )
@@ -721,6 +721,34 @@ YEPTRIS_API YeptrisStatus yeptris_marshal(const char* data, size_t len, YeptrisS
721
721
  return st;
722
722
  }
723
723
 
724
+ /* #178 (ruby #168's fast path): the value records carry the resolver's
725
+ * verdict, NOT the source's explicit tag — a `!ruby/object` mapping
726
+ * would marshal as a plain hash, silently dropping the tag (psych's
727
+ * visitor revives it). One cheap pre-walk: any explicitly tagged node
728
+ * in the subtree → UNSUPPORTED, the host walks. Explicit CORE tags
729
+ * (!!str and friends) ride the same bail — correct, just conservative;
730
+ * bulk data is untagged. */
731
+ static int dom_subtree_tagged(const yep_dom* d, uint32_t id) {
732
+ const yep_dnode* n = yep_dom_node(d, id);
733
+ if (n == NULL) {
734
+ return 0;
735
+ }
736
+ if (n->tag.len != 0) {
737
+ return 1;
738
+ }
739
+ for (uint32_t c = n->first_child; c != UINT32_MAX;) {
740
+ const yep_dnode* cn = yep_dom_node(d, c);
741
+ if (cn == NULL) {
742
+ break;
743
+ }
744
+ if (dom_subtree_tagged(d, c) != 0) {
745
+ return 1;
746
+ }
747
+ c = cn->next_sibling;
748
+ }
749
+ return 0;
750
+ }
751
+
724
752
  YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, size_t* out_len) {
725
753
  if (node == NULL || out == NULL || out_len == NULL) {
726
754
  return YEPTRIS_ERROR_ARG;
@@ -728,6 +756,12 @@ YEPTRIS_API YeptrisStatus yeptris_marshal_node(YeptrisNode node, char** out, siz
728
756
  *out = NULL;
729
757
  *out_len = 0;
730
758
  yeptris_node* h = (yeptris_node*)node;
759
+ if (dom_subtree_tagged(h->doc->dom, h->id) != 0) {
760
+ yep_error_set(yep_error_tls(), YEP_ERR_UNEXPECTED, 0, 0, 0,
761
+ "marshal: explicitly tagged node not expressible; "
762
+ "fall back to the value walk");
763
+ return YEPTRIS_ERROR_UNSUPPORTED;
764
+ }
731
765
  yep_value_ctx* c = NULL;
732
766
  if (yep_values_from_dom(h->doc->dom, h->id, 0, &c) != 0) {
733
767
  return YEPTRIS_ERROR_MEMORY;
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.14.1
4
+ version: 0.6.15.2
5
5
  platform: aarch64-linux
6
6
  authors:
7
7
  - Ribose Inc.