yeptris 0.1.13.3-x86_64-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.
@@ -0,0 +1,256 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ffi"
4
+
5
+ module Yeptris
6
+ # Every public C declaration, attached exactly once (the leptris-ruby
7
+ # seam discipline: status checking and owned-pointer reading live in
8
+ # check_status / read_owned_string, never hand-rolled at call sites).
9
+ module FFI
10
+ extend ::FFI::Library
11
+
12
+ begin
13
+ ffi_lib [
14
+ ENV["YEPTRIS_LIB_PATH"],
15
+ File.expand_path("../../libyeptris.dylib", __dir__),
16
+ File.expand_path("../../libyeptris.so", __dir__),
17
+ File.expand_path("../../libyeptris.dll", __dir__),
18
+ "/usr/local/lib/libyeptris.dylib",
19
+ "/usr/local/lib/libyeptris.so",
20
+ "yeptris",
21
+ ].compact
22
+ rescue LoadError => e
23
+ raise LoadError, <<~MSG
24
+ yeptris: cannot load the libyeptris library.
25
+ Set YEPTRIS_LIB_PATH to a libyeptris.{so,dylib,dll}, or vendor
26
+ the library next to the gem's lib/ directory.
27
+ (Underlying error: #{e.message})
28
+ MSG
29
+ end
30
+
31
+ typedef :pointer, :yeptris_document
32
+ typedef :pointer, :yeptris_node
33
+ typedef :pointer, :yeptris_status_out
34
+ typedef :int, :yeptris_status
35
+
36
+ # YeptrisParseOptions (parse.h): schema, max_depth, strict,
37
+ # tab_policy, recover. ABI-frozen field order.
38
+ class ParseOptions < ::FFI::Struct
39
+ layout :schema, :int,
40
+ :max_depth, :int,
41
+ :strict, :int,
42
+ :tab_policy, :int,
43
+ :recover, :int
44
+ end
45
+
46
+ # yeptris_emit_options (emit.h): versioned by size.
47
+ class EmitOptions < ::FFI::Struct
48
+ layout :size, :uint32,
49
+ :canonical, :int,
50
+ :best_width, :int
51
+ end
52
+
53
+ attach_function :yeptris_version, [], :string
54
+
55
+ attach_function :yeptris_last_error, [:pointer, :pointer], :string
56
+
57
+ attach_function :yeptris_parse, %i[pointer size_t yeptris_status_out], :yeptris_document
58
+ attach_function :yeptris_parse_ex,
59
+ %i[pointer size_t pointer yeptris_status_out], :yeptris_document
60
+ attach_function :yeptris_parse_json, %i[pointer size_t yeptris_status_out], :yeptris_document
61
+
62
+ attach_function :yeptris_document_free, [:yeptris_document], :void
63
+ attach_function :yeptris_document_count, [:yeptris_document], :size_t
64
+ attach_function :yeptris_document_root, [:yeptris_document, :size_t], :yeptris_node
65
+
66
+ # construction (TODO.impl/11 phase 3)
67
+ attach_function :yeptris_document_new, [], :yeptris_document
68
+ attach_function :yeptris_document_set_root,
69
+ %i[yeptris_document yeptris_node], :int
70
+ attach_function :yeptris_node_new_mapping, [:yeptris_document], :yeptris_node
71
+ attach_function :yeptris_node_new_sequence, [:yeptris_document], :yeptris_node
72
+ attach_function :yeptris_node_new_scalar,
73
+ %i[yeptris_document pointer size_t int], :yeptris_node
74
+ attach_function :yeptris_node_map_add,
75
+ %i[yeptris_node pointer size_t yeptris_node], :int
76
+ attach_function :yeptris_node_map_set,
77
+ %i[yeptris_node pointer size_t yeptris_node], :int
78
+ attach_function :yeptris_node_map_del, %i[yeptris_node pointer size_t], :int
79
+ attach_function :yeptris_node_seq_add, %i[yeptris_node yeptris_node], :int
80
+ attach_function :yeptris_node_seq_del, %i[yeptris_node size_t], :int
81
+ attach_function :yeptris_node_set_anchor, %i[yeptris_node pointer size_t], :int
82
+ attach_function :yeptris_node_set_tag, %i[yeptris_node pointer size_t], :int
83
+ attach_function :yeptris_node_new_alias,
84
+ %i[yeptris_document yeptris_node pointer size_t], :yeptris_node
85
+
86
+ attach_function :yeptris_node_kind, [:yeptris_node], :int
87
+ attach_function :yeptris_node_id, [:yeptris_node], :uint32
88
+ attach_function :yeptris_node_value, %i[yeptris_node pointer], :pointer
89
+ attach_function :yeptris_node_style, [:yeptris_node], :int
90
+ attach_function :yeptris_node_tag, %i[yeptris_node pointer], :pointer
91
+ attach_function :yeptris_node_anchor, %i[yeptris_node pointer], :pointer
92
+ attach_function :yeptris_node_alias_target, [:yeptris_node], :yeptris_node
93
+ attach_function :yeptris_node_tag_id, [:yeptris_node], :int
94
+ attach_function :yeptris_node_int, %i[yeptris_node pointer], :yeptris_status
95
+ attach_function :yeptris_node_float, %i[yeptris_node pointer], :yeptris_status
96
+ attach_function :yeptris_node_bool, %i[yeptris_node pointer], :yeptris_status
97
+ attach_function :yeptris_node_seq_count, [:yeptris_node], :size_t
98
+ attach_function :yeptris_node_seq_at, %i[yeptris_node size_t], :yeptris_node
99
+ attach_function :yeptris_node_map_count, [:yeptris_node], :size_t
100
+ attach_function :yeptris_node_map_get, %i[yeptris_node pointer size_t], :yeptris_node
101
+ attach_function :yeptris_node_map_at,
102
+ %i[yeptris_node size_t pointer pointer], :int
103
+
104
+ attach_function :yeptris_tag_uri, [:int], :string
105
+
106
+ # recorder (TODO.impl/12): bulk records + string arena, one drain
107
+ attach_function :yeptris_recorder_new, [], :pointer
108
+ attach_function :yeptris_recorder_new_ex, [:int], :pointer
109
+ attach_function :yeptris_recorder_feed,
110
+ %i[pointer pointer size_t int], :int
111
+ attach_function :yeptris_recorder_records, %i[pointer pointer], :pointer
112
+ attach_function :yeptris_recorder_arena, %i[pointer pointer], :pointer
113
+ attach_function :yeptris_recorder_free, [:pointer], :void
114
+
115
+ # value stream (TODO.impl/15 phase F): one drain of pre-converted
116
+ # typed values — the materialization fast path
117
+ attach_function :yeptris_value_drain,
118
+ %i[pointer size_t int pointer pointer pointer pointer], :int
119
+ attach_function :yeptris_value_free, %i[pointer pointer], :void
120
+
121
+ # Columnar drain (libyeptris > 0.1.1): the same stream as parallel
122
+ # typed buffers, one carved allocation. Feature-detected — the
123
+ # record API is the fallback on older libraries.
124
+ COLUMNS = begin
125
+ attach_function :yeptris_value_drain_columns, %i[pointer size_t int pointer], :int
126
+ attach_function :yeptris_value_free_columns, [:pointer], :void
127
+ true
128
+ rescue ::FFI::NotFoundError
129
+ false
130
+ end
131
+
132
+ class ValueColumns < ::FFI::Struct
133
+ layout :count, :size_t, :arena_len, :size_t,
134
+ :payloads, :pointer, :offs, :pointer, :lens, :pointer,
135
+ :kinds, :pointer, :tags, :pointer, :is_keys, :pointer,
136
+ :bools, :pointer, :arena, :pointer
137
+ end
138
+
139
+ # Marshal 4.8 emission (TODO.restructure/21): the C side converts
140
+ # value records directly into Ruby's wire format; the binding
141
+ # materializes the whole graph with one core-C Marshal.load call
142
+ # instead of walking per-value records in pure Ruby. Same feature-
143
+ # detect discipline as the columnar drain (older libraries fall
144
+ # back to the record walk).
145
+ MARSHAL = begin
146
+ MARSHAL_ALL_DOCS = 0
147
+ MARSHAL_FIRST_DOC = 1
148
+ attach_function :yeptris_marshal,
149
+ %i[pointer size_t int int pointer pointer], :int
150
+ attach_function :yeptris_marshal_node,
151
+ %i[yeptris_node pointer pointer], :int
152
+ attach_function :yeptris_marshal_free, [:pointer], :void
153
+ true
154
+ rescue ::FFI::NotFoundError
155
+ false
156
+ end
157
+
158
+ # bulk build (TODO.impl/15 phase D): one call raises a document
159
+ BUILD_SCALAR = 1
160
+ BUILD_SEQ = 2
161
+ BUILD_MAP = 3
162
+ BUILD_END = 4
163
+
164
+ # the ABI-pinned shape; the bulk builder packs these bytes
165
+ # directly (12 per entry)
166
+ class BuildEntry < ::FFI::Struct
167
+ layout op: :uint8, style: :uint8, reserved: :uint16, off: :uint32, len: :uint32
168
+ end
169
+ attach_function :yeptris_document_build,
170
+ %i[yeptris_document pointer size_t pointer size_t], :int
171
+
172
+ attach_function :yeptris_serialize, %i[yeptris_document pointer], :pointer
173
+ attach_function :yeptris_serialize_ex,
174
+ %i[yeptris_document pointer pointer], :pointer
175
+ attach_function :yeptris_serialize_json, %i[yeptris_document pointer], :pointer
176
+
177
+ # Owned char* results (serialize*): one reader, freed exactly once.
178
+ # The buffers are plain malloc'd C memory (the header contract says
179
+ # "caller frees"), so the release is libc free.
180
+ attach_function :c_free, :free, [:pointer], :void
181
+
182
+ module Owned
183
+ module_function
184
+
185
+ # Reads a NUL-terminated malloc'd C string into an Encoding
186
+ # UTF_8 String, then frees the buffer. len_out (nullable) is a
187
+ # MemoryPointer carrying the byte length from the producing call.
188
+ def string(ptr, len_out = nil)
189
+ return nil if ptr.null?
190
+
191
+ len = len_out&.read_uint64
192
+ s = if len && len > 0
193
+ ptr.read_bytes(len).force_encoding(Encoding::UTF_8)
194
+ else
195
+ ptr.read_string.force_encoding(Encoding::UTF_8)
196
+ end
197
+ ::Yeptris::FFI.c_free(ptr)
198
+ s
199
+ end
200
+ end
201
+
202
+ module_function
203
+
204
+ # Non-OK status -> ParseError carrying the C error channel's
205
+ # message with line/column. NULL-document failures route through
206
+ # here too (parse detail lives on the same channel).
207
+ def check_status(status, action)
208
+ return if status.zero?
209
+
210
+ raise Yeptris::ParseError, "#{action} failed: #{last_error_message}"
211
+ end
212
+
213
+ def last_error_message
214
+ line = ::FFI::MemoryPointer.new(:uint32)
215
+ col = ::FFI::MemoryPointer.new(:uint32)
216
+ msg = yeptris_last_error(line, col)
217
+ detail = msg.to_s
218
+ l = line.read_uint32
219
+ c = col.read_uint32
220
+ l.positive? ? "#{detail} at line #{l}, column #{c}" : detail
221
+ end
222
+
223
+ # Pinned enum values (test_abi): the constants the binding relies
224
+ # on without a C header at runtime.
225
+ NODE_SCALAR = 0
226
+ NODE_SEQUENCE = 1
227
+ NODE_MAPPING = 2
228
+ NODE_ALIAS = 3
229
+
230
+ STYLE_PLAIN = 1
231
+ STYLE_SINGLE_QUOTED = 2
232
+ STYLE_DOUBLE_QUOTED = 3
233
+ STYLE_LITERAL = 4
234
+ STYLE_FOLDED = 5
235
+
236
+ TAG_STR = 0
237
+ TAG_INT = 1
238
+ TAG_FLOAT = 2
239
+ TAG_BOOL = 3
240
+ TAG_NULL = 4
241
+ TAG_TIMESTAMP = 5
242
+
243
+ SCHEMA_12_CORE = 0
244
+ SCHEMA_11_COMPAT = 1
245
+
246
+ OK = 0
247
+ ERROR_PARSE = 1
248
+ ERROR_MEMORY = 2
249
+ ERROR_DEPTH = 3
250
+ ERROR_ENCODING = 4
251
+ ERROR_IO = 5
252
+ ERROR_ARG = 6
253
+ ERROR_UNSUPPORTED = 7
254
+ ERROR_INTERNAL = 8
255
+ end
256
+ end
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Yeptris
4
+ # The STRICT JSON surface (TODO.restructure/31).
5
+ #
6
+ # `Yeptris::JSON.load` targets EXACT `JSON.parse` semantics — that
7
+ # is its parity target, spec-pinned (spec/json_parity_spec.rb).
8
+ # It is deliberately separate from Yeptris::YAML: the YAML surface
9
+ # keeps the Psych contract for every input (JSON-shaped included),
10
+ # so the two never drift into each other's semantics.
11
+ #
12
+ # Engines, fastest first (both exact — the parity spec runs against
13
+ # whichever is loaded):
14
+ # 1. the native materializer (fused C scan → VALUE; opt-in build)
15
+ # 2. the record drain + strict conversion walk (always available)
16
+ module JSON
17
+ class Error < ::Yeptris::Error; end
18
+ class ParseError < Error; end
19
+
20
+ module_function
21
+
22
+ def load(source)
23
+ source = ::Yeptris.read_input(source)
24
+ source = source.to_s
25
+ if defined?(::Yeptris::Native)
26
+ begin
27
+ return ::Yeptris::Native.load_json(source)
28
+ rescue ::Yeptris::ParseError => e
29
+ raise ParseError, e.message
30
+ end
31
+ end
32
+ strict_fallback(source)
33
+ end
34
+
35
+ # The always-available engine: the strict-JSON validator gates
36
+ # (parse_json raises on anything RFC 8259 rejects), then the
37
+ # value records convert WITHOUT the Psych quirk table — floats
38
+ # are always Floats, bools always bools ("1e3" is 1000.0 here
39
+ # and a String on the YAML surface; each surface is its own
40
+ # contract).
41
+ def strict_fallback(source)
42
+ begin
43
+ gate = ::Yeptris::Document.parse_json(source)
44
+ rescue ::Yeptris::ParseError => e
45
+ raise ParseError, e.message
46
+ end
47
+ begin
48
+ cols = ::Yeptris::FFI::ValueColumns.new
49
+ st = ::Yeptris::FFI.yeptris_value_drain_columns(
50
+ source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
51
+ )
52
+ raise ParseError, ::Yeptris::FFI.last_error_message if st != ::Yeptris::FFI::OK
53
+
54
+ begin
55
+ walk_strict(cols)
56
+ ensure
57
+ ::Yeptris::FFI.yeptris_value_free_columns(cols)
58
+ end
59
+ ensure
60
+ gate.free
61
+ end
62
+ end
63
+
64
+ # Placement mechanics mirror ValueML.walk_columns; the CONVERSION
65
+ # is the strict-JSON one (no ':sym' scan, no y/n quirk, no
66
+ # dot-required floats). Anchors/aliases/timestamps cannot occur
67
+ # in strict JSON — reaching them is an internal error.
68
+ def walk_strict(cols)
69
+ n = cols[:count]
70
+ kinds = cols[:kinds].read_bytes(n).unpack("C*")
71
+ ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
72
+ bools = cols[:bools].read_bytes(n).unpack("C*")
73
+ offs = cols[:offs].read_bytes(n * 4).unpack("V*")
74
+ lens = cols[:lens].read_bytes(n * 4).unpack("V*")
75
+ pays = cols[:payloads].read_bytes(n * 8).unpack("q<*")
76
+ arena = cols[:arena_len].zero? ? +"" : cols[:arena].read_bytes(cols[:arena_len])
77
+ arena.force_encoding(Encoding::UTF_8)
78
+
79
+ docs = []
80
+ stack = []
81
+ pending_key = nil
82
+ i = 0
83
+ while i < n
84
+ case kinds[i]
85
+ when ValueML::DOC
86
+ docs.push(nil)
87
+ when ValueML::SEQ_OPEN
88
+ place(docs, stack, pending_key) { [] }
89
+ pending_key = nil
90
+ when ValueML::MAP_OPEN
91
+ place(docs, stack, pending_key) { {} }
92
+ pending_key = nil
93
+ when ValueML::CLOSE
94
+ stack.pop
95
+ when ValueML::V_STR
96
+ text = arena.byteslice(offs[i], lens[i])
97
+ if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
98
+ pending_key = text
99
+ else
100
+ # Records carry int64 payloads: an integer-beyond-int64
101
+ # degrades to a PLAIN string (b==1). In strict JSON a
102
+ # plain (unquoted) scalar can ONLY be a number — every
103
+ # real string is quoted and arrives b==0 — so rebuild the
104
+ # exact Integer (JSON.parse parity, Bignum included).
105
+ if bools[i] == 1
106
+ place(docs, stack, pending_key) { Integer(text, 10) }
107
+ else
108
+ place(docs, stack, pending_key) { text }
109
+ end
110
+ pending_key = nil
111
+ end
112
+ when ValueML::V_INT
113
+ place(docs, stack, pending_key) { pays[i] }
114
+ pending_key = nil
115
+ when ValueML::V_FLOAT
116
+ place(docs, stack, pending_key) { [pays[i]].pack("q<").unpack1("E") }
117
+ pending_key = nil
118
+ when ValueML::V_BOOL
119
+ place(docs, stack, pending_key) { bools[i] == 1 }
120
+ pending_key = nil
121
+ when ValueML::V_NULL
122
+ place(docs, stack, pending_key) { nil }
123
+ pending_key = nil
124
+ else
125
+ raise Error, "internal: impossible record #{kinds[i]} in strict JSON"
126
+ end
127
+ i += 1
128
+ end
129
+ docs.empty? ? nil : docs.first
130
+ end
131
+
132
+ def place(docs, stack, key)
133
+ v = yield
134
+ if stack.empty?
135
+ docs[-1] = v
136
+ elsif key
137
+ stack.last[key] = v
138
+ else
139
+ stack.last.push(v)
140
+ end
141
+ stack.push(v) if v.is_a?(Array) || v.is_a?(Hash)
142
+ v
143
+ end
144
+ end
145
+ end