yeptris 0.6.18.2-arm-linux-eabihf

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