yeptris 0.6.16.1-s390x-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.
Files changed (139) hide show
  1. checksums.yaml +7 -0
  2. data/README.adoc +141 -0
  3. data/ext/build_windows_native.rb +41 -0
  4. data/ext/libyeptris/extconf.rb +57 -0
  5. data/ext/yeptris_native/cbor_ruby.c +154 -0
  6. data/ext/yeptris_native/extconf.rb +47 -0
  7. data/ext/yeptris_native/json_ruby.c +354 -0
  8. data/ext/yeptris_native/yeptris_native.c +454 -0
  9. data/lib/yeptris/cbor.rb +130 -0
  10. data/lib/yeptris/document.rb +255 -0
  11. data/lib/yeptris/ffi.rb +415 -0
  12. data/lib/yeptris/json/descriptor.rb +266 -0
  13. data/lib/yeptris/json.rb +390 -0
  14. data/lib/yeptris/materializer.rb +354 -0
  15. data/lib/yeptris/native-3.2.so +0 -0
  16. data/lib/yeptris/node.rb +453 -0
  17. data/lib/yeptris/psych/coder_shim.rb +55 -0
  18. data/lib/yeptris/psych/drop_in.rb +63 -0
  19. data/lib/yeptris/psych/encodable.rb +14 -0
  20. data/lib/yeptris/psych/handler.rb +90 -0
  21. data/lib/yeptris/psych/parser.rb +105 -0
  22. data/lib/yeptris/psych/visitors.rb +545 -0
  23. data/lib/yeptris/psych.rb +604 -0
  24. data/lib/yeptris/schema.rb +253 -0
  25. data/lib/yeptris/valueml.rb +455 -0
  26. data/lib/yeptris/yaml/descriptor.rb +56 -0
  27. data/lib/yeptris/yaml.rb +422 -0
  28. data/lib/yeptris.rb +113 -0
  29. data/libyeptris.so +0 -0
  30. data/vendor/libyeptris/CMakeLists.txt +254 -0
  31. data/vendor/libyeptris/cmake/yeptris-config.cmake.in +5 -0
  32. data/vendor/libyeptris/cmake/yeptris.pc.in +11 -0
  33. data/vendor/libyeptris/src/CMakeLists.txt +167 -0
  34. data/vendor/libyeptris/src/include/yeptris/api.h +32 -0
  35. data/vendor/libyeptris/src/include/yeptris/cbor.h +101 -0
  36. data/vendor/libyeptris/src/include/yeptris/dom.h +190 -0
  37. data/vendor/libyeptris/src/include/yeptris/emit.h +81 -0
  38. data/vendor/libyeptris/src/include/yeptris/error.h +41 -0
  39. data/vendor/libyeptris/src/include/yeptris/events.h +157 -0
  40. data/vendor/libyeptris/src/include/yeptris/json.h +45 -0
  41. data/vendor/libyeptris/src/include/yeptris/json.hpp +258 -0
  42. data/vendor/libyeptris/src/include/yeptris/jsonc_compat.h +115 -0
  43. data/vendor/libyeptris/src/include/yeptris/marshal.h +52 -0
  44. data/vendor/libyeptris/src/include/yeptris/parse.h +48 -0
  45. data/vendor/libyeptris/src/include/yeptris/plan.h +100 -0
  46. data/vendor/libyeptris/src/include/yeptris/resolve.h +71 -0
  47. data/vendor/libyeptris/src/include/yeptris/schema.h +119 -0
  48. data/vendor/libyeptris/src/include/yeptris/tape.h +123 -0
  49. data/vendor/libyeptris/src/include/yeptris/types.h +38 -0
  50. data/vendor/libyeptris/src/include/yeptris/values.h +102 -0
  51. data/vendor/libyeptris/src/include/yeptris/version.h.in +29 -0
  52. data/vendor/libyeptris/src/include/yeptris/visit.h +77 -0
  53. data/vendor/libyeptris/src/include/yeptris/yajl_compat.h +135 -0
  54. data/vendor/libyeptris/src/include/yeptris.h +23 -0
  55. data/vendor/libyeptris/src/yeptris/build.c +376 -0
  56. data/vendor/libyeptris/src/yeptris/cbor/cbor.h +25 -0
  57. data/vendor/libyeptris/src/yeptris/cbor/decode.c +848 -0
  58. data/vendor/libyeptris/src/yeptris/cbor/encode.c +835 -0
  59. data/vendor/libyeptris/src/yeptris/common/chartype.c +41 -0
  60. data/vendor/libyeptris/src/yeptris/common/chartype.h +77 -0
  61. data/vendor/libyeptris/src/yeptris/common/cpu.c +81 -0
  62. data/vendor/libyeptris/src/yeptris/common/cpu.h +40 -0
  63. data/vendor/libyeptris/src/yeptris/common/error.c +65 -0
  64. data/vendor/libyeptris/src/yeptris/common/error.h +79 -0
  65. data/vendor/libyeptris/src/yeptris/common/mutex.h +45 -0
  66. data/vendor/libyeptris/src/yeptris/common/nametab.c +275 -0
  67. data/vendor/libyeptris/src/yeptris/common/nametab.h +68 -0
  68. data/vendor/libyeptris/src/yeptris/common/port.h +74 -0
  69. data/vendor/libyeptris/src/yeptris/common/simd_text.c +49 -0
  70. data/vendor/libyeptris/src/yeptris/common/simd_text.h +295 -0
  71. data/vendor/libyeptris/src/yeptris/common/simd_text_avx2.c +534 -0
  72. data/vendor/libyeptris/src/yeptris/common/simd_text_neon.c +662 -0
  73. data/vendor/libyeptris/src/yeptris/common/simd_text_scalar.c +298 -0
  74. data/vendor/libyeptris/src/yeptris/common/string_view.c +34 -0
  75. data/vendor/libyeptris/src/yeptris/common/string_view.h +93 -0
  76. data/vendor/libyeptris/src/yeptris/doc.h +56 -0
  77. data/vendor/libyeptris/src/yeptris/dom/dom.c +1192 -0
  78. data/vendor/libyeptris/src/yeptris/dom/dom.h +312 -0
  79. data/vendor/libyeptris/src/yeptris/dom/hpool.c +107 -0
  80. data/vendor/libyeptris/src/yeptris/dom/mapindex.c +173 -0
  81. data/vendor/libyeptris/src/yeptris/dom/mapindex.h +61 -0
  82. data/vendor/libyeptris/src/yeptris/dom/mutate.c +431 -0
  83. data/vendor/libyeptris/src/yeptris/emit/emit.c +434 -0
  84. data/vendor/libyeptris/src/yeptris/emit/float/api.h +38 -0
  85. data/vendor/libyeptris/src/yeptris/emit/float/dragon.c +405 -0
  86. data/vendor/libyeptris/src/yeptris/emit/float/floatint.h +203 -0
  87. data/vendor/libyeptris/src/yeptris/emit/float/print.c +396 -0
  88. data/vendor/libyeptris/src/yeptris/emit/style.c +81 -0
  89. data/vendor/libyeptris/src/yeptris/emit/style.h +24 -0
  90. data/vendor/libyeptris/src/yeptris/emit/writer.c +802 -0
  91. data/vendor/libyeptris/src/yeptris/emit/writer.h +76 -0
  92. data/vendor/libyeptris/src/yeptris/encoding/bom.c +31 -0
  93. data/vendor/libyeptris/src/yeptris/encoding/encoding.h +63 -0
  94. data/vendor/libyeptris/src/yeptris/encoding/transcode.c +148 -0
  95. data/vendor/libyeptris/src/yeptris/encoding/utf8_validate.c +178 -0
  96. data/vendor/libyeptris/src/yeptris/events/capture.c +150 -0
  97. data/vendor/libyeptris/src/yeptris/events/capture.h +39 -0
  98. data/vendor/libyeptris/src/yeptris/events/iterparse.c +208 -0
  99. data/vendor/libyeptris/src/yeptris/events/pull.c +97 -0
  100. data/vendor/libyeptris/src/yeptris/events/push.c +104 -0
  101. data/vendor/libyeptris/src/yeptris/events/recorder.c +107 -0
  102. data/vendor/libyeptris/src/yeptris/events/values.c +527 -0
  103. data/vendor/libyeptris/src/yeptris/events/values_priv.h +51 -0
  104. data/vendor/libyeptris/src/yeptris/events/yaml_compat.c +86 -0
  105. data/vendor/libyeptris/src/yeptris/events/yaml_compat.h +90 -0
  106. data/vendor/libyeptris/src/yeptris/jsonapi/jsonc_compat.c +687 -0
  107. data/vendor/libyeptris/src/yeptris/jsonapi/yajl_compat.c +728 -0
  108. data/vendor/libyeptris/src/yeptris/marshal.c +782 -0
  109. data/vendor/libyeptris/src/yeptris/memory/allocator.c +20 -0
  110. data/vendor/libyeptris/src/yeptris/memory/allocator.h +43 -0
  111. data/vendor/libyeptris/src/yeptris/memory/arena.c +134 -0
  112. data/vendor/libyeptris/src/yeptris/memory/arena.h +52 -0
  113. data/vendor/libyeptris/src/yeptris/memory/pool.c +122 -0
  114. data/vendor/libyeptris/src/yeptris/memory/pool.h +36 -0
  115. data/vendor/libyeptris/src/yeptris/parse/engine.c +3698 -0
  116. data/vendor/libyeptris/src/yeptris/parse/engine.h +65 -0
  117. data/vendor/libyeptris/src/yeptris/parse/events.h +124 -0
  118. data/vendor/libyeptris/src/yeptris/parse/numbers.c +349 -0
  119. data/vendor/libyeptris/src/yeptris/parse/numbers.h +33 -0
  120. data/vendor/libyeptris/src/yeptris/parse/scalars.c +416 -0
  121. data/vendor/libyeptris/src/yeptris/parse/scalars.h +75 -0
  122. data/vendor/libyeptris/src/yeptris/parse.c +737 -0
  123. data/vendor/libyeptris/src/yeptris/plan.c +824 -0
  124. data/vendor/libyeptris/src/yeptris/resolve/compat11.c +334 -0
  125. data/vendor/libyeptris/src/yeptris/resolve/core12.c +143 -0
  126. data/vendor/libyeptris/src/yeptris/resolve/resolver.h +56 -0
  127. data/vendor/libyeptris/src/yeptris/resolve/tags.c +58 -0
  128. data/vendor/libyeptris/src/yeptris/scan/json.c +792 -0
  129. data/vendor/libyeptris/src/yeptris/scan/json.h +109 -0
  130. data/vendor/libyeptris/src/yeptris/scan/scan.c +463 -0
  131. data/vendor/libyeptris/src/yeptris/scan/scan.h +150 -0
  132. data/vendor/libyeptris/src/yeptris/schema.c +308 -0
  133. data/vendor/libyeptris/src/yeptris/tape.c +1308 -0
  134. data/vendor/libyeptris/src/yeptris/tape_in.h +19 -0
  135. data/vendor/libyeptris/src/yeptris/version.c +7 -0
  136. data/vendor/libyeptris/src/yeptris/visit/dom_visit.c +115 -0
  137. data/vendor/libyeptris/src/yeptris/visit/json_visit.c +293 -0
  138. data/vendor/libyeptris/src/yeptris/visit/yaml_visit.c +127 -0
  139. metadata +199 -0
@@ -0,0 +1,604 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require "set"
6
+
7
+ # Self-sufficient (the #95 yaml.rb fix, same class): the standalone
8
+ # require "yeptris/psych" must register the Document/FFI machinery —
9
+ # consumers load this face directly under the drop-in.
10
+ require "yeptris"
11
+
12
+ # The Psych drop-in namespace (TODO.impl/15 phase C).
13
+ #
14
+ # `require "yeptris/psych"` loads this namespace WITHOUT touching the
15
+ # top-level Psych constant (co-existence, issue #69); the process-
16
+ # exclusive drop-in rebind is `require "yeptris/psych/drop_in"` (the
17
+ # original stdlib, if loaded first, stays reachable as
18
+ # ::Psych::ORIGINAL). Semantics follow the Psych suite: load is
19
+ # SAFE by default (Psych 5 behavior — plain data only; anything
20
+ # tagged raises), unsafe_load materializes everything the yeptris
21
+ # loader understands, and parse returns the Nodes tree over the
22
+ # document without materializing.
23
+ module Yeptris
24
+ module Psych
25
+ # The tag registries (Psych's class-level API, #95 bug 4):
26
+ # load_tags maps a serialized tag to the Class that revives it;
27
+ # dump_tags overrides the emitted tag for a Class. Consulted by
28
+ # the revival visitor (custom tags) and the dumper's tag choice.
29
+ class << self
30
+ def load_tags
31
+ @load_tags ||= {}
32
+ end
33
+
34
+ def load_tags=(tags)
35
+ @load_tags = tags
36
+ end
37
+
38
+ def dump_tags
39
+ @dump_tags ||= {}
40
+ end
41
+
42
+ def dump_tags=(tags)
43
+ @dump_tags = tags
44
+ end
45
+
46
+ # The third registry (stdlib psych carries all three; the rebind
47
+ # broke relaton/lutaml callers that wrote it — the 26x
48
+ # undefined-method report). Keys are normalized tag strings
49
+ # ("tag:DOMAIN:TYPE"), values are [key, block] post-processors.
50
+ def domain_types
51
+ @domain_types ||= {}
52
+ end
53
+
54
+ def domain_types=(types)
55
+ @domain_types = types
56
+ end
57
+
58
+ def add_domain_type(domain, type_tag, &block)
59
+ key = ["tag", domain, type_tag].join(":")
60
+ domain_types[key] = [key, block]
61
+ domain_types["tag:#{type_tag}"] = [key, block]
62
+ end
63
+
64
+ def add_builtin_type(type_tag, &block)
65
+ key = ["tag", "yaml.org,2002", type_tag].join(":")
66
+ domain_types[key] = [key, block]
67
+ end
68
+
69
+ def remove_type(type_tag)
70
+ domain_types.delete type_tag
71
+ end
72
+
73
+ def add_tag(tag, klass)
74
+ load_tags[tag] = klass.name
75
+ dump_tags[klass] = tag
76
+ end
77
+ end
78
+
79
+ # Children load via autoload declared HERE — the immediate parent
80
+ # namespace's file (never internal requires).
81
+ autoload :Handler, "yeptris/psych/handler"
82
+ # Handlers (the Recorder submodule) lives in handler.rb too — its
83
+ # own entry so referencing Psych::Handlers triggers the load
84
+ autoload :Handlers, "yeptris/psych/handler"
85
+ autoload :Parser, "yeptris/psych/parser"
86
+ autoload :CoderShim, "yeptris/psych/coder_shim"
87
+ autoload :Visitors, "yeptris/psych/visitors"
88
+ # The typed opt-in marker for arbitrary-object dump/load
89
+ # (TODO.restructure/23). Eager by intent: classes include it at
90
+ # declaration time, so the autoload must resolve before any
91
+ # object instance exists.
92
+ autoload :Encodable, "yeptris/psych/encodable"
93
+ class Error < StandardError; end
94
+
95
+ # stdlib psych 5: a *foo with no matching &foo anchor
96
+ class AnchorNotDefined < Error; end
97
+ # Psych's exact interface (issue #32): same constructor arity,
98
+ # same reader set (file/line/column/offset/problem/context), same
99
+ # message shape — drop-in consumers' rescues and constructors
100
+ # keep working after the rebind.
101
+ class SyntaxError < Error
102
+ attr_reader :file, :line, :column, :offset, :problem, :context
103
+
104
+ def initialize(file = nil, line = 0, column = 0, offset = 0, problem = nil, context = nil)
105
+ @file = file
106
+ @line = line
107
+ @column = column
108
+ @offset = offset
109
+ @problem = problem
110
+ @context = context
111
+ where = file ? "(#{file})" : "(<unknown>)"
112
+ detail = context ? "#{problem} #{context}" : problem.to_s
113
+ super("#{where}: #{detail} at line #{line} column #{column}")
114
+ end
115
+
116
+ # Structured lift from the C parser's message (carries
117
+ # "line L, column C" detail).
118
+ def self.from_parse_error(error)
119
+ md = /\bline (\d+),? column (\d+)/.match(error.message)
120
+ new(nil, md ? md[1].to_i : 0, md ? md[2].to_i : 0, 0, error.message)
121
+ end
122
+ end
123
+ class BadAlias < Error; end
124
+ class DisallowedClass < Error
125
+ attr_reader :name
126
+
127
+ def initialize(name)
128
+ super("Tried to load unspecified class: #{name}")
129
+ @name = name
130
+ end
131
+ end
132
+ # Psych spells it Psych::AliasesError; the older name stays as an
133
+ # alias for existing rescues.
134
+ class AliasesError < Error; end
135
+ AliasNotEnabled = AliasesError
136
+
137
+ class << self
138
+ # Psych 5: load is safe — plain data structures only. Tagged
139
+ # nodes raise DisallowedClass unless their class is permitted
140
+ # (Date/Time/Symbol are built in; they are plain data here).
141
+ def load(yaml, permitted_classes: [::Date, ::Time], aliases: false, **)
142
+ safe_load(yaml, permitted_classes: permitted_classes, aliases: aliases)
143
+ end
144
+
145
+ # Full revival over the Nodes tree: !ruby/object, !ruby/struct,
146
+ # !ruby/set, encode_with/init_with, alias identity. Plain-data
147
+ # loads should use load/safe_load (the Materializer fast path).
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
174
+ tree = parse(yaml)
175
+ return nil if tree.nil?
176
+
177
+ Visitors::ToRuby.visit(tree.children.first)
178
+ end
179
+
180
+ def safe_load(yaml, permitted_classes: [::Date, ::Time], aliases: false, **)
181
+ begin
182
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
183
+ return nil if doc.nil? # the legal empty stream
184
+
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)
189
+ ensure
190
+ doc&.free
191
+ end
192
+ end
193
+
194
+ # A YAML stream is a Unicode character stream: stdlib psych tags
195
+ # non-ASCII scalars UTF-8 even when the input String is BINARY
196
+ # (#135). ASCII-only values keep their (binary) encoding — a
197
+ # re-tag would be observable there and stdlib does not either.
198
+ def force_utf8_scalars(obj)
199
+ case obj
200
+ when ::String
201
+ # valid_encoding? on an ASCII-8BIT string is ALWAYS true
202
+ # (it is bytes) — the discriminator is whether the bytes
203
+ # READ as UTF-8: #135's case (UTF-8 scalars the C side
204
+ # materialized as BINARY) re-tags; DECODED !binary bytes
205
+ # that are not UTF-8 stay BINARY (#168). Binary content that
206
+ # happens to be valid UTF-8 re-tags — noted divergence from
207
+ # stdlib, which keeps BINARY; real payloads (zips, bodies)
208
+ # never are.
209
+ if obj.encoding == ::Encoding::ASCII_8BIT && !obj.ascii_only?
210
+ probe = obj.dup.force_encoding(::Encoding::UTF_8)
211
+ obj.force_encoding(::Encoding::UTF_8) if probe.valid_encoding?
212
+ end
213
+ obj
214
+ when ::Hash
215
+ obj.each { |k, v| force_utf8_scalars(k); force_utf8_scalars(v) }
216
+ when ::Array
217
+ obj.each { |v| force_utf8_scalars(v) }
218
+ else
219
+ obj
220
+ end
221
+ end
222
+
223
+ # stdlib's file-level faces (#135): BOM-tolerant UTF-8 reads with
224
+ # the fallback contract (load_file path, fallback: false default).
225
+ def load_file(path, fallback: false, **kwargs)
226
+ File.open(path, "r:bom|utf-8") { |f| load(f, **kwargs) }
227
+ rescue Errno::ENOENT
228
+ raise unless fallback
229
+
230
+ false
231
+ end
232
+
233
+ def safe_load_file(path, fallback: false, **kwargs)
234
+ File.open(path, "r:bom|utf-8") { |f| safe_load(f, **kwargs) }
235
+ rescue Errno::ENOENT
236
+ raise unless fallback
237
+
238
+ false
239
+ end
240
+
241
+ def unsafe_load_file(path, fallback: false, **kwargs)
242
+ File.open(path, "r:bom|utf-8") { |f| unsafe_load(f, **kwargs) }
243
+ rescue Errno::ENOENT
244
+ raise unless fallback
245
+
246
+ false
247
+ end
248
+
249
+ def parse_file(path, **kwargs)
250
+ File.open(path, "r:bom|utf-8") { |f| parse(f, **kwargs) }
251
+ end
252
+
253
+
254
+
255
+ # stdlib's deprecated safe/dump split — same output here (every
256
+ # value this loader produces is safe data)
257
+ def safe_dump(obj, io = nil, options = {})
258
+ ::Yeptris::Psych.dump(obj, io, options)
259
+ end
260
+
261
+ def load_stream(yaml, **kwargs)
262
+ # materialize each document's root directly — the stream
263
+ # children share one C document, so their handles would all
264
+ # resolve to the first document's tree
265
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
266
+ return nil if doc.nil? # the legal empty stream
267
+
268
+ begin
269
+ (0...doc.document_count).map { |i| doc.root(i).to_ruby }
270
+ ensure
271
+ doc.free
272
+ end
273
+ end
274
+
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
+
285
+ def parse(yaml)
286
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
287
+ return nil if doc.nil? || doc.document_count.zero?
288
+
289
+ Nodes::Builder.document(doc)
290
+ rescue Yeptris::ParseError => e
291
+ raise SyntaxError.from_parse_error(e)
292
+ end
293
+
294
+ def parse_stream(yaml, &block)
295
+ # stdlib yields each document to a block if one was given (and
296
+ # the stream it returns holds the same children either way).
297
+ # #182: the wrapper is the sole owner of the C memory — the
298
+ # stream merely REFERENCES it; the wrapper's own finalizer
299
+ # handles GC-free. (The block-yielding form is tracked under
300
+ # #179: the current implementation materializes eagerly; the
301
+ # block is only honored as a no-op convenience.)
302
+ _ = block # reserved for the future yielding form (#179)
303
+ doc = Yeptris::Document.parse(yaml, schema: :compat_11)
304
+ return nil if doc.nil? || doc.document_count.zero?
305
+
306
+ stream = Nodes::Stream.new
307
+ (0...doc.document_count).each do |i|
308
+ stream.children << Nodes::Builder.document_stream_child(doc, i)
309
+ end
310
+ # ownership: the DOCUMENT wrapper is the sole owner — its own
311
+ # finalizer (pointer-only closure) frees the C memory; the
312
+ # stream merely references it. A tree-side finalizer closing
313
+ # over the wrapper raced the wrapper's sweep (#182's abort).
314
+ stream.owner = doc
315
+ stream
316
+ rescue Yeptris::ParseError => e
317
+ raise SyntaxError.from_parse_error(e)
318
+ ensure
319
+ # the returned stream OWNS the doc's lifetime (its free delegates
320
+ # to the owner); only free here when the stream wasn't built
321
+ doc&.free if doc && !stream
322
+ end
323
+
324
+ # Arbitrary object graphs through the YAMLTree visitor
325
+ # (anchors, !ruby/ tags); plain data rides the fast builder.
326
+ def dump(obj, io = nil, options = {})
327
+ # stdlib's Object#to_yaml calls Psych.dump(self, options) —
328
+ # the options hash lands in the io slot positionally; treat a
329
+ # Hash there as options (the options themselves ride the
330
+ # visitor's defaults, #95 thread)
331
+ if io.is_a?(::Hash)
332
+ options = io
333
+ io = nil
334
+ end
335
+ # scalars take the fast builder; EVERYTHING else (including
336
+ # plain containers — they may nest custom objects) goes
337
+ # through the visitor, which builds the same DOM for plain
338
+ # data anyway
339
+ out =
340
+ case obj
341
+ # DateTime < Date in ruby's hierarchy — the fast scalar
342
+ # path would emit it tag-less iso8601 (re-loading as Time);
343
+ # the visitor carries stdlib's !ruby/object:DateTime tag
344
+ when ::DateTime
345
+ Visitors::YAMLTree.new.push(obj).finish
346
+ when nil, true, false, ::String, ::Integer, ::Float, ::Symbol, ::Date, ::Time
347
+ Yeptris::YAML.dump(obj, header: true)
348
+ else
349
+ Visitors::YAMLTree.new.push(obj).finish
350
+ end
351
+ return out unless io
352
+
353
+ io.write(out)
354
+ io
355
+ end
356
+
357
+ private
358
+
359
+ # yeptris materializes plain data only — there is nothing
360
+ # unsafe it COULD load. The safety walk enforces Psych's
361
+ # contract: aliases need opt-in, explicit non-core tags raise
362
+ # DisallowedClass, and a scalar whose IMPLICIT typing yields a
363
+ # class outside the permitted set raises too (issue #69: a
364
+ # compat_11 date must not become a Date unless Date is
365
+ # permitted — Psych::DisallowedClass semantics).
366
+ def walk_safe(root, permitted, aliases_enabled)
367
+ check(root, permitted, aliases_enabled) if root
368
+ root.to_ruby
369
+ end
370
+
371
+ PERMITTED_BY_DEFAULT = [TrueClass, FalseClass, NilClass, Integer, Float,
372
+ String, Array, Hash].freeze
373
+
374
+ def check(node, permitted, aliases_enabled)
375
+ case node.kind
376
+ when :alias
377
+ raise AliasesError, "Unknown alias" unless aliases_enabled
378
+ when :scalar, :mapping, :sequence
379
+ tag = node.tag
380
+ unless tag.nil?
381
+ # both tag spellings pass: the URI form's last segment and
382
+ # the short "!binary" shorthand (#168's cassettes carry it)
383
+ name = tag.split(":").last.sub(/\A!/, "")
384
+ unless %w[str int float bool null timestamp seq map merge value
385
+ binary].include?(name)
386
+ raise DisallowedClass, name
387
+ end
388
+ end
389
+ if node.kind == :scalar && node.tag_id == :timestamp &&
390
+ !permitted.include?(Date) && !permitted.include?(Time)
391
+ raise DisallowedClass, "Date"
392
+ end
393
+ case node.kind
394
+ when :mapping
395
+ node.each_pair do |k, v|
396
+ check(k, permitted, aliases_enabled)
397
+ check(v, permitted, aliases_enabled)
398
+ end
399
+ when :sequence
400
+ node.each { |e| check(e, permitted, aliases_enabled) }
401
+ end
402
+ end
403
+ end
404
+ end
405
+
406
+ # Psych::Nodes over the yeptris document: the tree IS the parsed
407
+ # document (children are node handles, not copies) — parse cost
408
+ # is the parse, and to_ruby reuses the Materializer.
409
+ module Nodes
410
+ class Node
411
+ include Enumerable
412
+
413
+ attr_reader :children
414
+ attr_reader :handle # @api private — the Yeptris::Node
415
+ # @api private — the tree builder attaches handles; a writer,
416
+ # never instance_variable_set from outside
417
+ attr_writer :handle
418
+ # The Document owning this tree's C memory; a plain writer —
419
+ # never instance_variable_set from outside (encapsulation law).
420
+ attr_accessor :owner
421
+
422
+ def initialize(handle = nil, children = [])
423
+ @handle = handle
424
+ @children = children
425
+ end
426
+
427
+ # Every node HAS an anchor concept (none by default) — the
428
+ # anchored search needs no type probe, just the model.
429
+ def anchor
430
+ nil
431
+ end
432
+
433
+ def each(&block)
434
+ @children.each(&block)
435
+ end
436
+
437
+ # The Ruby object for this subtree (Materializer semantics).
438
+ def to_ruby
439
+ @handle.to_ruby
440
+ end
441
+ end
442
+
443
+ class Stream < Node
444
+ def free
445
+ @owner&.free
446
+ end
447
+ end
448
+
449
+ # Owns the underlying Yeptris::Document: node handles in the
450
+ # tree stay valid while the tree is reachable; a GC finalizer
451
+ # releases the C memory when it is not.
452
+ class Document < Node
453
+ attr_reader :version, :tags
454
+
455
+ def initialize(version = [], tags = {})
456
+ super(nil)
457
+ @version = version
458
+ @tags = tags
459
+ end
460
+
461
+ # @api private — attach the document: the tree REFERENCES it
462
+ # (keeping the C memory alive while the tree is reachable);
463
+ # the wrapper's own finalizer is the sole freer (#182: a
464
+ # tree-side finalizer closing over the wrapper raced the
465
+ # wrapper's sweep — SIGABRT under document churn)
466
+ def own(yeptris_doc)
467
+ @owner = yeptris_doc
468
+ self
469
+ end
470
+
471
+ def free
472
+ @owner&.free
473
+ end
474
+ end
475
+
476
+ class Scalar < Node
477
+ attr_reader :value, :tag, :anchor, :plain, :quoted, :style
478
+
479
+ def initialize(value = nil, anchor: nil, tag: nil, plain: true,
480
+ quoted: false, style: :plain)
481
+ super(nil)
482
+ @value = value
483
+ @anchor = anchor
484
+ @tag = tag
485
+ @plain = plain
486
+ @quoted = quoted
487
+ @style = style
488
+ end
489
+ end
490
+
491
+ class Sequence < Node
492
+ attr_reader :anchor, :tag, :style
493
+
494
+ def initialize(anchor: nil, tag: nil, style: :block)
495
+ super(nil, [])
496
+ @anchor = anchor
497
+ @tag = tag
498
+ @style = style
499
+ end
500
+ end
501
+
502
+ class Mapping < Node
503
+ attr_reader :anchor, :tag, :style
504
+
505
+ def initialize(anchor: nil, tag: nil, style: :block)
506
+ super(nil, [])
507
+ @anchor = anchor
508
+ @tag = tag
509
+ @style = style
510
+ end
511
+ end
512
+
513
+ class Alias < Node
514
+ attr_reader :anchor
515
+
516
+ def initialize(anchor)
517
+ super(nil)
518
+ @anchor = anchor
519
+ end
520
+ end
521
+
522
+ # Builds the Nodes tree from a parsed document.
523
+ module Builder
524
+ module_function
525
+
526
+ # parse(): one document, owning the yeptris document
527
+ def document(doc, index = 0)
528
+ document_stream_child(doc, index).own(doc)
529
+ end
530
+
531
+ # parse_stream(): a child document sharing one owner (the
532
+ # stream owns the yeptris document)
533
+ def document_stream_child(doc, index)
534
+ root = doc.root(index)
535
+ # #182's root cause: three per-stream Document wrappers all
536
+ # capture the same c_ptr in their finalizers → three frees.
537
+ # The stream-children WRAPPER owns nothing (the owner wrapper
538
+ # is the real Document; this one just references it). Build
539
+ # a non-owning wrapper with no finalizer.
540
+ d = Document.send(:new, root&.document, false)
541
+ d.children << node(root) if root
542
+ d
543
+ end
544
+
545
+ def node(n)
546
+ case n.kind
547
+ when :mapping
548
+ m = Mapping.new(anchor: n.anchor, tag: n.tag)
549
+ n.each_pair do |k, v|
550
+ m.children << node(k)
551
+ m.children << node(v)
552
+ end
553
+ m.handle = n
554
+ m
555
+ when :sequence
556
+ s = Sequence.new(anchor: n.anchor, tag: n.tag)
557
+ n.each { |e| s.children << node(e) }
558
+ s.handle = n
559
+ s
560
+ when :alias
561
+ a = Alias.new(n.value)
562
+ a.handle = n
563
+ a
564
+ else
565
+ sc = Scalar.new(n.value, anchor: n.anchor, tag: n.tag,
566
+ plain: n.style == :plain, style: n.style)
567
+ sc.handle = n
568
+ sc
569
+ end
570
+ end
571
+ end
572
+ end
573
+ end
574
+ end
575
+
576
+ # The drop-in is OPT-IN (issue #69): `require "yeptris/psych"` loads
577
+ # the namespace only and coexists with stdlib psych in ANY load
578
+ # order (this file never touches ::Psych). The rebind lives in
579
+ # yeptris/psych/drop_in — process-exclusive by nature, since the
580
+ # stdlib cannot be prevented from re-opening whatever ::Psych points
581
+ # at once IT loads.
582
+ # The core extension (stdlib psych/core_ext parity): Object#to_yaml
583
+ # and Object.yaml_tag exist whether the consumer came through stdlib
584
+ # psych first or pure drop-in. Both definitions are compatible —
585
+ # whoever loads last wins, and both call the rebound Psych.dump.
586
+ class Object
587
+ def self.yaml_tag(url)
588
+ ::Yeptris::Psych.add_tag(url, self)
589
+ end
590
+
591
+ def to_yaml(options = {})
592
+ ::Yeptris::Psych.dump(self, options)
593
+ end
594
+ end
595
+
596
+ # 0.4-contract migration signal (issue #95): this require used to
597
+ # rebind ::Psych. It does not anymore, and re-binding it here would
598
+ # regress #69 (any stdlib psych loaded afterwards would explode with
599
+ # a superclass mismatch), so the old path warns instead of acting.
600
+ if defined?(::Psych) && !::Psych.equal?(Yeptris::Psych)
601
+ warn "yeptris: \"yeptris/psych\" defines the namespace only — it no " \
602
+ "longer rebinds ::Psych. Require \"yeptris/psych/drop_in\" for the " \
603
+ "drop-in rebind, or call Yeptris::Psych explicitly."
604
+ end