yeptris 0.2.0.1-aarch64-linux

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,159 @@
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
+ # json gem 3.0 made duplicate keys an error by DEFAULT; 2.x is
21
+ # last-wins. The parity target is the RESOLVED json gem's own
22
+ # behavior — strictness follows it (issue #37, found by canon's
23
+ # CI where json 3.0.0 resolved while the dev box had 2.x).
24
+ require "json"
25
+ STRICT_DUPLICATE_KEYS = Gem::Version.new(::JSON::VERSION) >= Gem::Version.new("3")
26
+
27
+ module_function
28
+
29
+ def load(source)
30
+ source = ::Yeptris.read_input(source)
31
+ source = source.to_s
32
+ if defined?(::Yeptris::Native)
33
+ begin
34
+ return ::Yeptris::Native.load_json(source, STRICT_DUPLICATE_KEYS)
35
+ rescue ::Yeptris::ParseError => e
36
+ raise ParseError, e.message
37
+ end
38
+ end
39
+ strict_fallback(source)
40
+ end
41
+
42
+ # The always-available engine: the strict-JSON validator gates
43
+ # (parse_json raises on anything RFC 8259 rejects), then the
44
+ # value records convert WITHOUT the Psych quirk table — floats
45
+ # are always Floats, bools always bools ("1e3" is 1000.0 here
46
+ # and a String on the YAML surface; each surface is its own
47
+ # contract).
48
+ def strict_fallback(source)
49
+ begin
50
+ gate = ::Yeptris::Document.parse_json(source)
51
+ rescue ::Yeptris::ParseError => e
52
+ raise ParseError, e.message
53
+ end
54
+ begin
55
+ cols = ::Yeptris::FFI::ValueColumns.new
56
+ st = ::Yeptris::FFI.yeptris_value_drain_columns(
57
+ source, source.bytesize, ::Yeptris::FFI::SCHEMA_12_CORE, cols
58
+ )
59
+ raise ParseError, ::Yeptris::FFI.last_error_message if st != ::Yeptris::FFI::OK
60
+
61
+ begin
62
+ walk_strict(cols, STRICT_DUPLICATE_KEYS)
63
+ ensure
64
+ ::Yeptris::FFI.yeptris_value_free_columns(cols)
65
+ end
66
+ ensure
67
+ gate.free
68
+ end
69
+ end
70
+
71
+ # Placement mechanics mirror ValueML.walk_columns; the CONVERSION
72
+ # is the strict-JSON one (no ':sym' scan, no y/n quirk, no
73
+ # dot-required floats). Anchors/aliases/timestamps cannot occur
74
+ # in strict JSON — reaching them is an internal error.
75
+ def walk_strict(cols, strict_dup = STRICT_DUPLICATE_KEYS)
76
+ n = cols[:count]
77
+ kinds = cols[:kinds].read_bytes(n).unpack("C*")
78
+ ikeys = cols[:is_keys].read_bytes(n).unpack("C*")
79
+ bools = cols[:bools].read_bytes(n).unpack("C*")
80
+ offs = cols[:offs].read_bytes(n * 4).unpack("V*")
81
+ lens = cols[:lens].read_bytes(n * 4).unpack("V*")
82
+ pays = cols[:payloads].read_bytes(n * 8).unpack("q<*")
83
+ arena = cols[:arena_len].zero? ? +"" : cols[:arena].read_bytes(cols[:arena_len])
84
+ arena.force_encoding(Encoding::UTF_8)
85
+
86
+ docs = []
87
+ stack = []
88
+ key_sets = strict_dup ? [{}] : nil
89
+ pending_key = nil
90
+ i = 0
91
+ while i < n
92
+ case kinds[i]
93
+ when ValueML::DOC
94
+ docs.push(nil)
95
+ when ValueML::SEQ_OPEN
96
+ place(docs, stack, pending_key) { [] }
97
+ pending_key = nil
98
+ when ValueML::MAP_OPEN
99
+ key_sets&.push({})
100
+ place(docs, stack, pending_key) { {} }
101
+ pending_key = nil
102
+ when ValueML::CLOSE
103
+ stack.pop
104
+ key_sets&.pop
105
+ when ValueML::V_STR
106
+ text = arena.byteslice(offs[i], lens[i])
107
+ if ikeys[i] == 1 && !stack.empty? && stack.last.is_a?(Hash)
108
+ if strict_dup && key_sets.last.key?(text)
109
+ raise ParseError, %(duplicate key "#{text}" in JSON object)
110
+ end
111
+ key_sets&.last&.store(text, true)
112
+ pending_key = text
113
+ else
114
+ # Records carry int64 payloads: an integer-beyond-int64
115
+ # degrades to a PLAIN string (b==1). In strict JSON a
116
+ # plain (unquoted) scalar can ONLY be a number — every
117
+ # real string is quoted and arrives b==0 — so rebuild the
118
+ # exact Integer (JSON.parse parity, Bignum included).
119
+ if bools[i] == 1
120
+ place(docs, stack, pending_key) { Integer(text, 10) }
121
+ else
122
+ place(docs, stack, pending_key) { text }
123
+ end
124
+ pending_key = nil
125
+ end
126
+ when ValueML::V_INT
127
+ place(docs, stack, pending_key) { pays[i] }
128
+ pending_key = nil
129
+ when ValueML::V_FLOAT
130
+ place(docs, stack, pending_key) { [pays[i]].pack("q<").unpack1("E") }
131
+ pending_key = nil
132
+ when ValueML::V_BOOL
133
+ place(docs, stack, pending_key) { bools[i] == 1 }
134
+ pending_key = nil
135
+ when ValueML::V_NULL
136
+ place(docs, stack, pending_key) { nil }
137
+ pending_key = nil
138
+ else
139
+ raise Error, "internal: impossible record #{kinds[i]} in strict JSON"
140
+ end
141
+ i += 1
142
+ end
143
+ docs.empty? ? nil : docs.first
144
+ end
145
+
146
+ def place(docs, stack, key)
147
+ v = yield
148
+ if stack.empty?
149
+ docs[-1] = v
150
+ elsif key
151
+ stack.last[key] = v
152
+ else
153
+ stack.last.push(v)
154
+ end
155
+ stack.push(v) if v.is_a?(Array) || v.is_a?(Hash)
156
+ v
157
+ end
158
+ end
159
+ end