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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ # The YAML leg of the Descriptor plan walk (yeptris#293 slice three,
4
+ # over the C DOM plan walk of libyeptris v0.6.4): the same compiled
5
+ # row shape as Yeptris::JSON::Descriptor, applied to a parsed YAML
6
+ # document — block YAML hydrates to typed columns with no
7
+ # intermediate Ruby tree.
8
+ #
9
+ # descriptor = Yeptris::YAML::Descriptor.build(
10
+ # kind: :map, path: ["data", "items"], # segmented paths work
11
+ # children: [{ name: "id", kind: :int }, { name: "name", kind: :str }])
12
+ # descriptor.walk(yaml).column("id") # => [1, 2, ...]
13
+ #
14
+ # Typed extraction rides the parse-time tag ids (schema: selects the
15
+ # resolver — :compat_11 keeps the Psych/libyaml implicit typing
16
+ # YAML.load uses); string columns are materialized from the
17
+ # document's regions before the result returns (nothing borrowed).
18
+ class Yeptris::YAML::Descriptor < ::Yeptris::JSON::Descriptor
19
+ # schema: :core_12 (YAML 1.2) or :compat_11 (the Psych-compatible
20
+ # implicit typing Yeptris::YAML.load uses).
21
+ def initialize(handle, names, schema)
22
+ super(handle, names)
23
+ @schema = schema
24
+ end
25
+
26
+ # @api private — the compile rides the JSON Descriptor's grammar
27
+ # (inherited private class method); schema selects the resolver
28
+ def self.build(kind:, path: nil, children:, schema: :compat_11)
29
+ handle, names = compile_handle(kind, path, children)
30
+ new(handle, names, schema)
31
+ end
32
+
33
+ def walk(yaml)
34
+ src = ::Yeptris.read_input(yaml).to_s
35
+ doc = ::Yeptris::Document.parse(src, schema: @schema)
36
+ return [] if doc.nil? # the legal empty stream
37
+
38
+ begin
39
+ st = ::FFI::MemoryPointer.new(:int)
40
+ raw = ::Yeptris::FFI.yeptris_document_plan_walk(doc.c_ptr, @handle, st)
41
+ if raw.null?
42
+ raise ::Yeptris::JSON::Descriptor::Error,
43
+ "plan walk failed (status=#{st.read_int}) — the document shape " \
44
+ "disagrees with the plan"
45
+ end
46
+
47
+ begin
48
+ ::Yeptris::JSON::Descriptor::PlanResult.read_dom(raw, @names)
49
+ ensure
50
+ ::Yeptris::FFI.yeptris_plan_result_free(raw)
51
+ end
52
+ ensure
53
+ doc.free
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,422 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Self-sufficient (issue #95): the neutral surface is a valid entry
4
+ # point on its own — lutaml-model requires exactly this file.
5
+ require "yeptris"
6
+
7
+ module Yeptris
8
+ # The neutral Ruby surface (the Psych-compat namespace arrives with
9
+ # the recorder-driven Visitors in phase B; this is the yeptris-native
10
+ # face users target first).
11
+ module YAML
12
+ autoload :Descriptor, "yeptris/yaml/descriptor"
13
+
14
+ module_function
15
+
16
+ # Loads the FIRST document of a YAML stream as native Ruby objects.
17
+ # schema: :compat_11 selects Psych/libyaml implicit typing
18
+ # (yes/no, 0o/octal, sexagesimal); :core_12 (default) is YAML 1.2.
19
+ #
20
+ # This surface keeps the Psych contract for EVERY input — including
21
+ # JSON-shaped ones (`{"a": [1,]}` is legal flow YAML; `"1e3"` is a
22
+ # Psych String). Strict RFC 8259 semantics live on Yeptris::JSON
23
+ # (TODO.restructure/31): defaults follow proof, not benchmarks.
24
+ def load(yaml, schema: :compat_11)
25
+ yaml = Yeptris.read_input(yaml)
26
+ yaml = yaml.to_s
27
+ docs = _drain_all(yaml, schema)
28
+ docs.empty? ? nil : docs.first
29
+ end
30
+
31
+ # Psych-semantics safe_load on the native surface (issue #69 —
32
+ # the entry frameworks actually want): plain data only; a leaf
33
+ # whose class is not permitted raises Yeptris::Psych::
34
+ # DisallowedClass; aliases: false raises Yeptris::Psych::
35
+ # AliasesError on alias use. Delegates to the compat namespace's
36
+ # tree walk (correctness first; the fused drain comes later).
37
+ def safe_load(yaml, permitted_classes: [], aliases: false, schema: :compat_11)
38
+ yaml = Yeptris.read_input(yaml)
39
+ Psych.safe_load(yaml.to_s, permitted_classes: permitted_classes, aliases: aliases)
40
+ end
41
+
42
+ # Every document in the stream, in order.
43
+ def load_stream(yaml, schema: :compat_11)
44
+ yaml = Yeptris.read_input(yaml)
45
+ yaml = yaml.to_s
46
+ _drain_all(yaml, schema)
47
+ end
48
+
49
+ # The Marshal fast path when the loaded libyeptris has it (>= 0.1.11
50
+ # era builds), falling back to the columnar drain and finally the
51
+ # record drain — one code path, the fastest the library offers.
52
+ def _drain_all(yaml, schema)
53
+ if FFI::MARSHAL
54
+ result = ValueML.load_all_marshal(yaml, schema: schema, mode: :all)
55
+ return result unless result.nil?
56
+ end
57
+ if FFI::COLUMNS
58
+ ValueML.load_all_columns(yaml, schema: schema)
59
+ else
60
+ ValueML.load_all(yaml, schema: schema)
61
+ end
62
+ end
63
+
64
+ # The Psych-suite port's spelling (spec/psych/): compat typing.
65
+ def parse_yaml(yaml)
66
+ load(yaml)
67
+ end
68
+
69
+ def load_file(path, schema: :compat_11)
70
+ File.open(path, "rb") { |f| load(f, schema: schema) }
71
+ end
72
+
73
+ # Parses without materializing: the first document's root Node.
74
+ def parse(yaml, schema: :core_12)
75
+ doc = Document.parse(yaml, schema: schema)
76
+ return nil if doc.nil? || doc.document_count.zero?
77
+
78
+ doc.root(0)
79
+ end
80
+
81
+ # Serializes a Ruby object graph to YAML via the DOM builder
82
+ # (TODO.impl/11 phase 3). Strings are emitted plain only when the
83
+ # resolver round-trips them as strings — everything else takes a
84
+ # quoted style, so dump(load(x)) == x for the scalar types.
85
+ def dump(obj, canonical: false, header: false)
86
+ BulkBuilder.dump(obj, canonical: canonical, header: header)
87
+ end
88
+
89
+
90
+ # The dump-side mirror of the Materializer's bulk drain (TODO.impl
91
+ # 15 phase D): the tree walks into one flat entry array plus one
92
+ # string blob, and yeptris_document_build raises the DOM in a
93
+ # SINGLE FFI call — per-node FFI is gone. Same semantics as the
94
+ # per-node Builder it replaces (cycle refusal, :symbol scalars,
95
+ # Date/Time iso8601, plain-only-when-it-round-trips strings),
96
+ # with plain_string? pinned to the resolver by a differential
97
+ # spec.
98
+ module BulkBuilder
99
+ SCALAR = Yeptris::FFI::BUILD_SCALAR
100
+ SEQ = Yeptris::FFI::BUILD_SEQ
101
+ MAP = Yeptris::FFI::BUILD_MAP
102
+ STOP = Yeptris::FFI::BUILD_END
103
+ STYLE_PLAIN = 1
104
+ STYLE_SQ = 2
105
+ STYLE_DQ = 3
106
+ STYLE_LIT = 4
107
+ BUILD_TAG = Yeptris::FFI::BUILD_TAG
108
+
109
+ module_function
110
+
111
+ # Psych's exact timestamp spelling (format_time, #300): space
112
+ # separated, nanosecond width, Z for UTC — iso8601 diverged
113
+ def time_text(t)
114
+ t.utc? ? t.strftime("%Y-%m-%d %H:%M:%S.%9N Z") : t.strftime("%Y-%m-%d %H:%M:%S.%9N %:z")
115
+ end
116
+
117
+ # Psych's float spelling (#290): Infinity/NAN ride the YAML words,
118
+ # not Ruby's to_s (which prints "Infinity"/"NaN" — a STRING on
119
+ # reload)
120
+ def float_text(f)
121
+ return ".nan" if f.respond_to?(:nan?) && f.nan?
122
+ case (inf = f.infinite?)
123
+ when 1 then ".inf"
124
+ when -1 then "-.inf"
125
+ else f.to_s
126
+ end
127
+ end
128
+
129
+ # container entries are constant bytes — no pack per op
130
+ MAP_ENTRY = [Yeptris::FFI::BUILD_MAP, 0, 0, 0].pack("CCx2VV").freeze
131
+ SEQ_ENTRY = [Yeptris::FFI::BUILD_SEQ, 0, 0, 0].pack("CCx2VV").freeze
132
+ END_ENTRY = [Yeptris::FFI::BUILD_END, 0, 0, 0].pack("CCx2VV").freeze
133
+ CONT_ENTRY = { Yeptris::FFI::BUILD_MAP => MAP_ENTRY,
134
+ Yeptris::FFI::BUILD_SEQ => SEQ_ENTRY,
135
+ Yeptris::FFI::BUILD_END => END_ENTRY }.freeze
136
+
137
+ def dump(obj, canonical: false, header: false)
138
+ parts = []
139
+ blob = String.new(encoding: Encoding::BINARY)
140
+ off = [0]
141
+ # one pack per SCALAR; containers reuse frozen constants
142
+ emit = lambda do |op, style, o, len|
143
+ parts << (o.zero? && len.zero? && style.zero? ? CONT_ENTRY[op] :
144
+ [op, style, o, len].pack("CCx2VV"))
145
+ end
146
+ place(obj, emit, blob, off, {})
147
+ doc = Document.create
148
+ buf = ::FFI::MemoryPointer.from_string(parts.join)
149
+ bblob = ::FFI::MemoryPointer.from_string(blob)
150
+ rc = doc.build_entries(buf, parts.length, bblob, blob.bytesize)
151
+ raise DumpError, "document_build failed: #{rc}" unless rc == FFI::OK
152
+ doc.serialize(canonical: canonical, explicit_doc_start: header)
153
+ ensure
154
+ doc&.free
155
+ end
156
+
157
+ def place(obj, emit, blob, off, seen)
158
+ case obj
159
+ when Hash
160
+ cycle_guard(obj, seen) do
161
+ emit.call(MAP, 0, 0, 0)
162
+ obj.each do |k, v|
163
+ # Symbol keys emit as bare ":k" plain — the compat
164
+ # reader resolves them back to Symbols; string keys
165
+ # ride the visit_String rules like any other scalar
166
+ # Symbol keys emit as bare ":k" plain — the compat
167
+ # reader resolves them back to Symbols; Integer/Float/
168
+ # bool keys ride plain like their values (Psych emits
169
+ # 1: unquoted — #290 family 5); string keys ride the
170
+ # visit_String rules like any other scalar; NIL keys
171
+ # ride Psych's explicit-`!` empty-scalar form
172
+ # (#300 family 2)
173
+ if k.nil?
174
+ scalar("", STYLE_SQ, emit, blob, off)
175
+ emit.call(BUILD_TAG, 0, off[0], 1)
176
+ blob << "!"
177
+ off[0] += 1
178
+ elsif k.is_a?(Symbol)
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
188
+ elsif k.is_a?(String)
189
+ place(k, emit, blob, off, seen)
190
+ else
191
+ scalar(k.to_s, STYLE_PLAIN, emit, blob, off)
192
+ end
193
+ place(v, emit, blob, off, seen)
194
+ end
195
+ emit.call(STOP, 0, 0, 0)
196
+ end
197
+ when Array
198
+ cycle_guard(obj, seen) do
199
+ emit.call(SEQ, 0, 0, 0)
200
+ obj.each { |e| place(e, emit, blob, off, seen) }
201
+ emit.call(STOP, 0, 0, 0)
202
+ end
203
+ when String
204
+ if obj.encoding == ::Encoding::ASCII_8BIT && !obj.ascii_only?
205
+ # #168: psych visit_String's binary branch — strict base64
206
+ # + the short !binary tag + literal style (Builder
207
+ # build_string carries the same branch; the tagged-literal
208
+ # writer keeps the block form)
209
+ scalar([obj].pack("m0"), STYLE_LIT, emit, blob, off)
210
+ emit.call(BUILD_TAG, 0, off[0], 7)
211
+ blob << "!binary"
212
+ off[0] += 7
213
+ else
214
+ scalar(obj, STYLE_BY_NAME[string_style(obj)], emit, blob, off)
215
+ end
216
+ when Symbol then scalar(":#{obj}", STYLE_PLAIN, emit, blob, off)
217
+ when Integer then scalar(obj.to_s, STYLE_PLAIN, emit, blob, off)
218
+ when Float then scalar(float_text(obj), STYLE_PLAIN, emit, blob, off)
219
+ when true, false then scalar(obj.to_s, STYLE_PLAIN, emit, blob, off)
220
+ when nil then scalar("", STYLE_PLAIN, emit, blob, off)
221
+ when Time then scalar(time_text(obj), STYLE_PLAIN, emit, blob, off)
222
+ when Date then scalar(obj.iso8601, STYLE_PLAIN, emit, blob, off)
223
+ else
224
+ raise DumpError,
225
+ "cannot dump #{obj.class}: unsupported object " \
226
+ "(custom to_yaml support lands with the Psych Visitors)"
227
+ end
228
+ end
229
+
230
+ def scalar(text, style, emit, blob, off)
231
+ # a BINARY blob absorbs any String bytewise (String#<< on
232
+ # ASCII-8BIT is compatible with every encoding) — the old
233
+ # text.b made a throwaway copy of every scalar before the
234
+ # blob's own copy
235
+ emit.call(SCALAR, style, off[0], text.bytesize)
236
+ blob << text
237
+ off[0] += text.bytesize
238
+ end
239
+
240
+ def key_text(k)
241
+ k = ":#{k}" if k.is_a?(Symbol)
242
+ k.to_s
243
+ end
244
+
245
+ # A plain scalar that the compat resolver re-reads as STR stays
246
+ # plain; anything resolvable (null/bool words, int/float/timestamp
247
+ # shapes, indicators, merge '<<') takes double quotes so the
248
+ # reparse yields String again. Pinned to the C resolver's own
249
+ # verdicts by the differential spec (spec/yaml_spec.rb).
250
+ RESHAPES = %w[~ null Null NULL y Y yes Yes YES n N no No NO true True
251
+ TRUE false False FALSE on On ON off Off OFF <<].freeze
252
+ # the fast lane consults the reshape table per string: an Array
253
+ # scan of 36 words was ~a third of the whole dump walk
254
+ RESHAPES_SET = RESHAPES.each_with_object({}) { |w, h| h[w] = true }.freeze
255
+
256
+ SAFE_WORD = /\A[A-Za-z][A-Za-z0-9_\-.\/ ]*\z/
257
+
258
+ def plain_string?(s)
259
+ # fast lane: letter-started, safe characters incl. spaces — no
260
+ # number/timestamp/indicator shape is possible; only the
261
+ # reshape words need the set lookup
262
+ if SAFE_WORD.match?(s) && !s.end_with?(" ") && !RESHAPES_SET[s]
263
+ return true
264
+ end
265
+ return false if s.empty? || s != s.strip
266
+ return false if s.match?(/[\n\t]/)
267
+ c = s[0]
268
+ return false if "#,[]{}&*!|>'\"%@`".include?(c)
269
+ return false if "-?:".include?(c) && (s.length == 1 || s[1] =~ /[ \t]/)
270
+ return false if s.include?(": ") || s.end_with?(":") || s.include?(" #")
271
+ return false if RESHAPES_SET[s]
272
+ # compat's float grammar REQUIRES the dot ("1e3" re-reads as a
273
+ # String and may dump plain); ints/sexagesimals still reshape.
274
+ # psych's FLOAT has no trailing [.:] group — "1.2.3" is a
275
+ # String and dumps plain (pinned by spec/yaml_spec.rb)
276
+ return false if s.match?(/\A[-+]?(0|[1-9][0-9_]*)(:[0-5]?[0-9])+\z/)
277
+ return false if s.match?(/\A[-+]?(0|[1-9][0-9_]*)\z/)
278
+ return false if s.match?(/\A[-+]?[0-9][0-9_]*\.[0-9_]*([eE][-+]?[0-9]+)?\z/)
279
+ return false if s.match?(/\A[-+]?[0-9][0-9_]*(:[0-5]?[0-9])+\.[0-9_]*\z/)
280
+ return false if s.match?(/\A[-+]?(0x[0-9a-fA-F_]+|0b[01_]+|0o?[0-7_]+)\z/)
281
+ return false if s.match?(/\A[-+]?\.(inf|Inf|INF)\z|\A\.(nan|NaN|NAN)\z/)
282
+ !s.match?(/\A\d{4}-\d\d?-\d\d?([Tt ]|$)/)
283
+ end
284
+
285
+ # Psych parity (psych yaml_tree#visit_String) — the quoting
286
+ # decision for a String, in psych's own order:
287
+ # y/Y/n/N (the 1.1 bool words psych double-quotes) → double
288
+ # a leading non-word character, no " anywhere → double
289
+ # a 1.1 bad-octal shape (0[0-7]*[89]) → single
290
+ # re-loads as something other than String (plain_string?
291
+ # rejects the 1.1 number/bool/null/timestamp shapes) → single
292
+ # needs escapes (control bytes, multiline) → double
293
+ # otherwise → plain
294
+ # psych's literal/folded/binary forms and its !!str tag for
295
+ # "<<" are follow-ups; those strings double-quote today.
296
+ STYLE_BY_NAME = { plain: STYLE_PLAIN, single: STYLE_SQ, double: STYLE_DQ,
297
+ literal: STYLE_LIT }.freeze
298
+
299
+ def string_style(s)
300
+ # psych's visit_String: any interior newline rides a literal
301
+ # block (the C writer's libyaml indicator rules render it) —
302
+ # first, exactly as psych orders it (#290 family 4)
303
+ return :literal if s.match?(/\n(?!\z)/)
304
+ return :double if s == "y" || s == "Y" || s == "n" || s == "N"
305
+ return :double if !s.empty? && !s.include?('"') && s.match?(/\A[^[:word:]]/)
306
+ return :single if s.match?(/\A0[0-7]*[89]/)
307
+ return :plain if plain_string?(s)
308
+ return :double if s.each_byte.any? { |b| b < 0x20 || b == 0x7f }
309
+ :single
310
+ end
311
+
312
+ def cycle_guard(obj, seen)
313
+ id = obj.object_id
314
+ raise DumpError, "cycle detected: cannot dump recursive #{obj.class}" if seen[id]
315
+
316
+ seen[id] = true
317
+ out = yield
318
+ seen.delete(id)
319
+ out
320
+ end
321
+ end
322
+
323
+ # From-scratch builder over the public construction API.
324
+ module Builder
325
+ module_function
326
+
327
+ def build(doc, obj, seen = {})
328
+ case obj
329
+ when Hash then build_map(doc, obj, seen)
330
+ when Array then build_seq(doc, obj, seen)
331
+ when String then build_string(doc, obj)
332
+ when Symbol then new_scalar(doc, ":#{obj}")
333
+ when Integer then new_scalar(doc, obj.to_s)
334
+ when Float then new_scalar(doc, BulkBuilder.float_text(obj))
335
+ when true, false then new_scalar(doc, obj.to_s)
336
+ when nil then new_scalar(doc, "")
337
+ when Time then new_scalar(doc, BulkBuilder.time_text(obj))
338
+ when Date then new_scalar(doc, obj.iso8601)
339
+ else
340
+ raise DumpError,
341
+ "cannot dump #{obj.class}: unsupported object " \
342
+ "(custom to_yaml support lands with the Psych Visitors)"
343
+ end
344
+ end
345
+
346
+ def build_map(doc, h, seen)
347
+ cycle_guard(h, seen) do
348
+ m = doc.new_mapping
349
+ h.each do |k, v|
350
+ if k.nil?
351
+ key_node = doc.new_scalar("", :single_quoted)
352
+ key_node.set_tag("!")
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))
360
+ else
361
+ m.map_add(key_text(k), build(doc, v, seen))
362
+ end
363
+ end
364
+ m
365
+ end
366
+ end
367
+
368
+ def build_seq(doc, a, seen)
369
+ cycle_guard(a, seen) do
370
+ s = doc.new_sequence
371
+ a.each { |e| s.seq_add(build(doc, e, seen)) }
372
+ s
373
+ end
374
+ end
375
+
376
+ # One quoting table (BulkBuilder.string_style — psych's
377
+ # visit_String matrix) serves both builders; this side carries
378
+ # the style onto the per-node DOM.
379
+ def build_string(doc, s)
380
+ # psych 5.5 visit_String's binary branch, verbatim: an
381
+ # ASCII-8BIT string with non-ASCII bytes emits as strict base64
382
+ # (pack('m0'), never wrapped) under the short !binary tag with
383
+ # literal style — the writer keeps the block form for tagged
384
+ # literals (#168). BOTH dump paths converge here (the top-level
385
+ # scalar fast route and the YAMLTree visitor).
386
+ if s.encoding == ::Encoding::ASCII_8BIT && !s.ascii_only?
387
+ bin = new_scalar(doc, [s].pack("m0"), :force_lit)
388
+ bin.set_tag("!binary")
389
+ return bin
390
+ end
391
+ case BulkBuilder.string_style(s)
392
+ when :single then new_scalar(doc, s, :force_str_sq)
393
+ when :double then new_scalar(doc, s, :force_str)
394
+ when :literal then new_scalar(doc, s, :force_lit)
395
+ else new_scalar(doc, s)
396
+ end
397
+ end
398
+
399
+ def key_text(k)
400
+ k = ":#{k}" if k.is_a?(Symbol)
401
+ k.to_s
402
+ end
403
+
404
+ def new_scalar(doc, text, mode = nil)
405
+ doc.new_scalar(text.to_s,
406
+ mode == :force_str ? :double_quoted :
407
+ mode == :force_str_sq ? :single_quoted :
408
+ mode == :force_lit ? :literal : :plain)
409
+ end
410
+
411
+ def cycle_guard(obj, seen)
412
+ id = obj.object_id
413
+ raise DumpError, "cycle detected: cannot dump recursive #{obj.class}" if seen[id]
414
+
415
+ seen[id] = true
416
+ out = yield
417
+ seen.delete(id)
418
+ out
419
+ end
420
+ end
421
+ end
422
+ end
data/lib/yeptris.rb ADDED
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The gem's version lives in the parent namespace's file — the last
5
+ # internal require (yeptris/version) retired with it.
6
+ VERSION = "0.6.18.2".freeze
7
+ # The error hierarchy lives in THIS file (the parent namespace's
8
+ # own file): nested constants do not trigger a parent-constant
9
+ # autoload, and the law forbids internal requires — defining the
10
+ # hierarchy here makes it eager by construction.
11
+
12
+ # The base error for everything this library raises deliberately.
13
+ class Error < StandardError; end
14
+
15
+ # The input is not valid YAML (or valid for the requested mode).
16
+ # message carries the C parser's line/column detail.
17
+ class ParseError < Error; end
18
+
19
+ # A handle was used after its document was freed. Raised, never a
20
+ # segfault: the Document is the sole C-memory owner and every Node
21
+ # checks liveness through it.
22
+ class FreedError < Error; end
23
+
24
+ # Building a document from a Ruby object graph hit something the
25
+ # builder refuses (cycles, unsupported objects).
26
+ class DumpError < Error; end
27
+
28
+ # Input coercion — the ONE place the input boundary is typed
29
+ # (no respond_to? duck-probing): IO-like objects read, Strings
30
+ # pass through, anything else must be stringable and is.
31
+ def self.read_input(source)
32
+ case source
33
+ when String then source
34
+ when IO, StringIO then source.read
35
+ else source.to_s
36
+ end
37
+ end
38
+
39
+ autoload :Document, "yeptris/document"
40
+ autoload :Node, "yeptris/node"
41
+ autoload :YAML, "yeptris/yaml"
42
+ autoload :JSON, "yeptris/json"
43
+ autoload :Materializer, "yeptris/materializer"
44
+ autoload :ValueML, "yeptris/valueml"
45
+ autoload :Psych, "yeptris/psych"
46
+ autoload :Schema, "yeptris/schema"
47
+ autoload :CBOR, "yeptris/cbor"
48
+ end
49
+
50
+ # NOTE (the #318 poisoned-process class): the Psych error-name aliases
51
+ # live BELOW the ffi require — reading Psych here would trigger the
52
+ # psych autoload BEFORE the native library resolves, and a mid-load
53
+ # ffi failure under a drop-in rebind left ::Psych rebound against a
54
+ # half-initialized FFI module (every to_yaml in the process died).
55
+ # With this order an ffi failure is a clean, loud require failure.
56
+
57
+ # Eager native-library resolution (leptris-ruby lesson): fail at
58
+ # require time, not at first parse. The ffi require MUST come after
59
+ # the autoload registrations — ffi.rb opens module Yeptris, and
60
+ # requiring it first would shadow the manifest (leptris-ruby#53).
61
+ begin
62
+ require "yeptris/ffi"
63
+ rescue LoadError => e
64
+ raise LoadError, <<~MSG
65
+ Yeptris could not load the native libyeptris library.
66
+ Set YEPTRIS_LIB_PATH to a libyeptris.{so,dylib,dll}, or use the
67
+ platform gem that vendors it.
68
+ (Underlying error: #{e.message})
69
+ MSG
70
+ end
71
+
72
+
73
+ # The safe_load error names, aliased at the top level so consumers
74
+ # writing rescues need no nesting knowledge (issue #73's naming
75
+ # note): Yeptris::DisallowedClass is Yeptris::Psych::DisallowedClass.
76
+ # LAZY (const_missing): a standalone require "yeptris/psych" loads
77
+ # yeptris.rb MID-psych — an eager alias here would re-enter the
78
+ # half-loaded psych.rb through the autoload and NameError. At first
79
+ # touch psych is complete in every load order.
80
+ module Yeptris
81
+ def self.const_missing(name)
82
+ case name
83
+ when :DisallowedClass, :AliasesError
84
+ const_set(name, Psych.const_get(name))
85
+ else
86
+ super
87
+ end
88
+ end
89
+ end
90
+
91
+ # Optional C-API materializer (TODO.restructure/22): fused visit →
92
+ # Ruby objects via the Ruby C API. Feature-detected — LoadError leaves
93
+ # the FFI ladder (Marshal → columns → records) as the sole path.
94
+ # Windows names the artifact per Ruby minor (the leptris-ruby #207/
95
+ # #227 lesson): a PE DLL must bind its build Ruby's runtime, so one
96
+ # DLL per supported minor ships and RUBY_VERSION selects. A missing
97
+ # cell (Ruby 3.3 arm64 has no build) falls back LOUDLY to the ladder.
98
+ begin
99
+ # per-minor FIRST everywhere: a single version-agnostic native.so
100
+ # loads under ANY Ruby (symbols resolve from the host process), and
101
+ # one built for a different minor is an ABI gamble — Ruby 3.0 ran
102
+ # a 3.3-built ext and the JSON parse failed. The plain name stays
103
+ # as the dev-checkout fallback (built for the running Ruby).
104
+ require "yeptris/native-#{RUBY_VERSION[/\A\d+\.\d+/]}"
105
+ rescue LoadError
106
+ begin
107
+ require "yeptris/native"
108
+ rescue LoadError => e
109
+ warn "yeptris: no precompiled native materializer for Ruby " \
110
+ "#{RUBY_VERSION[/\A\d+\.\d+/]} on this platform " \
111
+ "(#{e.message}); the FFI ladder carries the load"
112
+ end
113
+ end
data/libyeptris.so ADDED
Binary file