bzync-nextsql 0.0.1

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,1147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+ require "time"
5
+ require "json"
6
+ require "bigdecimal"
7
+
8
+ require_relative "errors"
9
+
10
+ module NextSQL
11
+ # Wire encoding/decoding for NSQL v1 — mirrors drivers/php/src/Protocol.php
12
+ # and drivers/go/nextsql.go byte-for-byte. Keep the three in sync: this is
13
+ # a faithful reimplementation, not an independent design.
14
+ module Protocol
15
+ VERSION = 1
16
+ MAX_PACKET = 1 << 20
17
+ MAX_SQL = 1 << 20
18
+ MAX_NAME = 256
19
+ MAX_PARAMS = 256
20
+ MAX_ENUM_LABELS = 4096
21
+ MAX_ENUM_LABEL_BYTES = 255
22
+
23
+ TYPE_HELLO = 1
24
+ TYPE_HELLO_OK = 2
25
+ TYPE_AUTH = 3
26
+ TYPE_AUTH_OK = 4
27
+ TYPE_QUERY = 5
28
+ TYPE_PREPARE = 6
29
+ TYPE_PREPARE_OK = 7
30
+ TYPE_EXECUTE = 8
31
+ TYPE_CLOSE_STMT = 9
32
+ TYPE_CLOSE_OK = 10
33
+ TYPE_FLOW_ACK = 11
34
+ TYPE_CANCEL = 12
35
+ TYPE_TERMINATE = 13
36
+ TYPE_ROW_DESC = 14
37
+ TYPE_DATA_BATCH = 15
38
+ TYPE_COMMAND_COMPLETE = 16
39
+ TYPE_ERROR = 17
40
+ TYPE_READY = 18
41
+ TYPE_UNLOCK = 19
42
+ TYPE_UNLOCK_OK = 20
43
+ TYPE_IDEMPOTENT_QUERY = 21
44
+ TYPE_SET_READ_CONSISTENCY = 22
45
+ TYPE_NODE_STATUS = 23
46
+ TYPE_NODE_STATUS_RESP = 24
47
+
48
+ # Read-consistency modes. Values match the wire byte ordering.
49
+ READ_STRONG = 0
50
+ READ_BOUNDED = 1
51
+ READ_STALE = 2
52
+
53
+ AUTH_PASSWORD = 1
54
+ AUTH_PASSWORD_KEY = 2
55
+ FLAG_CANCEL = 1
56
+ # FLAG_PUBLIC_ERROR_CODES asks the server for the stable ERR_* error
57
+ # taxonomy (docs/error-codes.md). A server that does not implement it
58
+ # ignores the bit and keeps the NSQL v1 error shape, which decode_error
59
+ # still reads, so setting it is safe against any server version. The server
60
+ # echoes it in the hello-ok flags when it was accepted.
61
+ FLAG_PUBLIC_ERROR_CODES = 2
62
+ FLAG_NULL = 0x01
63
+
64
+ KIND_UUID = 1
65
+ KIND_STRING = 2
66
+ KIND_TEXT = 3
67
+ KIND_DECIMAL = 4
68
+ KIND_TIMESTAMPTZ = 5
69
+ KIND_JSON = 6
70
+ KIND_VECTOR = 7
71
+ KIND_BOOL = 8
72
+ KIND_NULL = 9
73
+ KIND_POINT = 10
74
+ KIND_BOX = 11
75
+ KIND_LINE = 12
76
+ KIND_POLYGON = 13
77
+ KIND_BLOB = 14
78
+ KIND_INT8 = 15
79
+ KIND_INT16 = 16
80
+ KIND_INT32 = 17
81
+ KIND_INT64 = 18
82
+ KIND_UINT8 = 19
83
+ KIND_UINT16 = 20
84
+ KIND_UINT32 = 21
85
+ KIND_UINT64 = 22
86
+ KIND_DATE = 23
87
+ KIND_TIME = 24
88
+ KIND_CHAR = 25
89
+ KIND_VARCHAR = 26
90
+ KIND_TIMESTAMP = 27
91
+ KIND_FLOAT32 = 28
92
+ KIND_FLOAT64 = 29
93
+ KIND_ENUM = 30
94
+ KIND_INTERVAL = 31
95
+ KIND_STRUCT = 32
96
+ KIND_ARRAY = 33
97
+ KIND_MAP = 34
98
+ KIND_GEOMETRY = 35
99
+ KIND_GEOGRAPHY = 36
100
+ MAX_NEST_DEPTH = 8
101
+ MAX_STRUCT_FIELDS = 128
102
+ MAX_COLLECTION_LEN = 1 << 20
103
+
104
+ Column = Struct.new(:name, :kind, :labels, :coll_type)
105
+ NodeStatus = Struct.new(:role, :has_leader, :healthy, :applied_lsn, :last_contact_ms, :apply_backlog)
106
+
107
+ # Dense (values), reference (ref=true, dim only), or sparse
108
+ # (indices + values) VECTOR/BITVECTOR/SPARSEVECTOR payload.
109
+ Vector = Struct.new(:dim, :values, :indices, :ref) do
110
+ def initialize(dim:, values: [], indices: nil, ref: false)
111
+ super(dim, values, indices, ref)
112
+ end
113
+ end
114
+
115
+ Point = Struct.new(:lon, :lat)
116
+ Box = Struct.new(:west, :south, :east, :north)
117
+ Line = Struct.new(:coords)
118
+ Polygon = Struct.new(:rings)
119
+
120
+ # Int8/16/32/64 (D2, Datatype expansion track): explicit fixed-width int
121
+ # wrappers. A bare Integer still defaults to KIND_DECIMAL (see
122
+ # encode_param) and coerces server-side into any numeric column — these
123
+ # are only needed to pin an exact wire width (Ruby Integer is arbitrary
124
+ # precision, so there is no natural bare-value mapping to one width).
125
+ Int8 = Struct.new(:value)
126
+ Int16 = Struct.new(:value)
127
+ Int32 = Struct.new(:value)
128
+ Int64 = Struct.new(:value)
129
+
130
+ # Uint8/16/32/64 (D3, Datatype expansion track): explicit fixed-width
131
+ # unsigned int wrappers, mirroring Int8/16/32/64 above.
132
+ Uint8 = Struct.new(:value)
133
+ Uint16 = Struct.new(:value)
134
+ Uint32 = Struct.new(:value)
135
+ Uint64 = Struct.new(:value)
136
+
137
+ # EnumValue (D11, Datatype expansion track): an explicit ENUM parameter
138
+ # wrapper (value:, labels:). Ordinary INSERT/UPDATE params can just pass
139
+ # a plain String — the server coerces STRING -> ENUM against the
140
+ # destination column, same as a SQL string literal. This wrapper exists
141
+ # for explicit round-tripping and mirrors Int8/Uint8's precedent.
142
+ EnumValue = Struct.new(:value, :labels) do
143
+ def initialize(value:, labels:)
144
+ super(value, labels)
145
+ end
146
+ end
147
+
148
+ # NaiveTimestamp/TimeOfDay/Float32/Float64 (D7/D5/D8, Datatype expansion
149
+ # track): explicit wrappers for types with no unambiguous native Ruby
150
+ # mapping. A bare Time/DateTime already means TimestampTZ (see below), so
151
+ # NaiveTimestamp is required to select the no-timezone Kind instead.
152
+ # Ruby has no time-only stdlib class, so TimeOfDay carries nanoseconds
153
+ # since midnight directly. Date needs no wrapper: it is unambiguous (not
154
+ # a Time/DateTime superclass — DateTime is Date's *subclass*, checked
155
+ # first below) and has no prior meaning to conflict with.
156
+ NaiveTimestamp = Struct.new(:value) do
157
+ def initialize(value:)
158
+ super(value)
159
+ end
160
+ end
161
+ TimeOfDay = Struct.new(:nanos_since_midnight) do
162
+ def initialize(nanos_since_midnight:)
163
+ super(nanos_since_midnight)
164
+ end
165
+ end
166
+ Float32 = Struct.new(:value)
167
+ Float64 = Struct.new(:value)
168
+
169
+ # Interval (D6, Datatype expansion track): months (Integer, calendar) +
170
+ # days (Integer, calendar) + nanos (Integer, time-of-day component) —
171
+ # Postgres-style 3-field storage. A plain String still works as an
172
+ # INTERVAL param for INSERT/UPDATE column assignment (server-side
173
+ # Coerce) but not inside an arithmetic expression like `dur + $1`,
174
+ # which requires the actual wire Kind.
175
+ # StructValue / MapValue (Collections track, docs/design-collections.md):
176
+ # explicit STRUCT / MAP parameter wrappers. StructValue.fields is an
177
+ # ordered Array of [name, value]; MapValue.entries is a Hash or an Array
178
+ # of [key, value].
179
+ StructValue = Struct.new(:fields, keyword_init: true)
180
+ MapValue = Struct.new(:entries, keyword_init: true)
181
+
182
+ # Geometry (Spatial track, docs/design-spatial.md): a decoded GEOMETRY /
183
+ # GEOGRAPHY value. +type+ is the OGC subtype name, +coordinates+ nested
184
+ # per type (Point: [x, y]); a GeometryCollection uses +geometries+
185
+ # instead. Also doubles as an explicit param wrapper: Geometry.new(type:
186
+ # nil, srid: 4326, wkt: "POINT(1 2)").
187
+ Geometry = Struct.new(:type, :srid, :coordinates, :geometries, :wkt, keyword_init: true) do
188
+ def to_wkt
189
+ return wkt if wkt
190
+
191
+ pt = ->(xy) { "#{xy[0]} #{xy[1]}" }
192
+ ring = ->(r) { "(#{r.map(&pt).join(', ')})" }
193
+ case type
194
+ when 'Point' then "POINT(#{pt.call(coordinates)})"
195
+ when 'LineString' then "LINESTRING(#{coordinates.map(&pt).join(', ')})"
196
+ when 'Polygon' then "POLYGON(#{coordinates.map(&ring).join(', ')})"
197
+ when 'MultiPoint' then "MULTIPOINT(#{coordinates.map { |c| "(#{pt.call(c)})" }.join(', ')})"
198
+ when 'MultiLineString' then "MULTILINESTRING(#{coordinates.map(&ring).join(', ')})"
199
+ when 'MultiPolygon'
200
+ "MULTIPOLYGON(#{coordinates.map { |poly| "(#{poly.map(&ring).join(', ')})" }.join(', ')})"
201
+ when 'GeometryCollection'
202
+ "GEOMETRYCOLLECTION(#{geometries.map(&:to_wkt).join(', ')})"
203
+ else
204
+ raise Error.new('invalid_argument', 'unsupported geometry type')
205
+ end
206
+ end
207
+ end
208
+
209
+ Interval = Struct.new(:months, :days, :nanos) do
210
+ def initialize(months:, days:, nanos:)
211
+ super(months, days, nanos)
212
+ end
213
+ end
214
+
215
+ class ProtocolError < Error
216
+ def initialize(message)
217
+ super("protocol", message)
218
+ end
219
+ end
220
+
221
+ module_function
222
+
223
+ def need!(b, off, n, what)
224
+ raise ProtocolError, "truncated #{what}" if off + n > b.bytesize
225
+ end
226
+
227
+ def u16(b, off)
228
+ need!(b, off, 2, "u16")
229
+ b.byteslice(off, 2).unpack1("v")
230
+ end
231
+
232
+ def u32(b, off)
233
+ need!(b, off, 4, "u32")
234
+ b.byteslice(off, 4).unpack1("V")
235
+ end
236
+
237
+ def u64(b, off)
238
+ need!(b, off, 8, "u64")
239
+ b.byteslice(off, 8).unpack1("Q<")
240
+ end
241
+
242
+ def i64(b, off)
243
+ need!(b, off, 8, "i64")
244
+ b.byteslice(off, 8).unpack1("q<")
245
+ end
246
+
247
+ def u16le(n) = [n].pack("v")
248
+ def u32le(n) = [n].pack("V")
249
+ def u64le(n) = [n & 0xFFFFFFFFFFFFFFFF].pack("Q<")
250
+
251
+ def u16str(s, max = MAX_NAME)
252
+ raw = s.to_s.b
253
+ raise ProtocolError, "string exceeds limit" if raw.bytesize > max || raw.bytesize > 0xFFFF
254
+
255
+ u16le(raw.bytesize) + raw
256
+ end
257
+
258
+ def u32bytes(raw, max)
259
+ raw = raw.b
260
+ raise ProtocolError, "bytes exceed limit" if raw.bytesize > max
261
+
262
+ u32le(raw.bytesize) + raw
263
+ end
264
+
265
+ def read_u16_string(b, off, max)
266
+ need!(b, off, 2, "string length")
267
+ n = u16(b, off)
268
+ raise ProtocolError, "truncated string" if n > max
269
+
270
+ need!(b, off + 2, n, "string")
271
+ [b.byteslice(off + 2, n).force_encoding("UTF-8"), off + 2 + n]
272
+ end
273
+
274
+ def read_u32_bytes(b, off, max)
275
+ need!(b, off, 4, "bytes length")
276
+ n = u32(b, off)
277
+ raise ProtocolError, "truncated bytes" if n > max
278
+
279
+ need!(b, off + 4, n, "bytes")
280
+ [b.byteslice(off + 4, n), off + 4 + n]
281
+ end
282
+
283
+ # append_enum_labels/read_enum_labels carry an ENUM Type's declared
284
+ # label list on the wire, matching internal/protocol/value.go's
285
+ # appendEnumLabels/readEnumLabels exactly (docs/design-datatypes.md
286
+ # D11): ENUM is the first D-track type whose Type needs variable-length
287
+ # metadata beyond the fixed 5/6-byte Precision/Scale/VecElem shape every
288
+ # other type fits into.
289
+ def append_enum_labels(labels)
290
+ out = +u16le(labels.size)
291
+ labels.each { |l| out << u16str(l, MAX_ENUM_LABEL_BYTES) }
292
+ out
293
+ end
294
+
295
+ def read_enum_labels(b, off)
296
+ need!(b, off, 2, "enum label count")
297
+ n = u16(b, off)
298
+ raise ProtocolError, "enum label count exceeds limit" if n > MAX_ENUM_LABELS
299
+
300
+ off += 2
301
+ labels = []
302
+ n.times do
303
+ label, next_off = read_u16_string(b, off, MAX_ENUM_LABEL_BYTES)
304
+ labels << label
305
+ off = next_off
306
+ end
307
+ [labels, off]
308
+ end
309
+
310
+ def encode_hello(version, flags, secret, database, user, realm = "")
311
+ sec = (secret || "").b
312
+ sec = sec[0, 8].ljust(8, "\x00")
313
+ out = u16le(version) + u16le(flags) + sec + u16str(database) + u16str(user)
314
+ # Realm is an optional trailing field (M2-2): emitted only when
315
+ # selected, so a Hello with no realm is byte-identical to the
316
+ # pre-realm wire shape.
317
+ out << u16str(realm) unless realm.nil? || realm.empty?
318
+ out
319
+ end
320
+
321
+ def decode_hello_ok(b)
322
+ raise ProtocolError, "bad hello-ok length" unless [11, 13].include?(b.bytesize)
323
+
324
+ flags = 0
325
+ if b.bytesize == 13
326
+ flags = u16(b, 11)
327
+ # The server omits the field when it accepted no capability, so a
328
+ # present-but-zero field is a second encoding of the v1 hello-ok.
329
+ raise ProtocolError, "empty hello-ok flags" if flags.zero?
330
+ end
331
+ [u16(b, 0), b.getbyte(2), b.byteslice(3, 8), flags]
332
+ end
333
+
334
+ def encode_query(sql, params)
335
+ raise ProtocolError, "too many parameters" if params.size > MAX_PARAMS
336
+
337
+ out = +u32bytes(sql.to_s.b, MAX_SQL)
338
+ out << u16le(params.size)
339
+ params.each { |v| out << u16str("") << encode_param(v) }
340
+ out
341
+ end
342
+
343
+ def encode_execute(stmt_id, params)
344
+ raise ProtocolError, "too many parameters" if params.size > MAX_PARAMS
345
+
346
+ out = +u32le(stmt_id)
347
+ out << u16le(params.size)
348
+ params.each { |v| out << u16str("") << encode_param(v) }
349
+ out
350
+ end
351
+
352
+ def encode_idempotent_query(key, sql, params)
353
+ raise ProtocolError, "too many parameters" if params.size > MAX_PARAMS
354
+
355
+ out = +u16str(key)
356
+ out << u32bytes(sql.to_s.b, MAX_SQL)
357
+ out << u16le(params.size)
358
+ params.each { |v| out << u16str("") << encode_param(v) }
359
+ out
360
+ end
361
+
362
+ def reserved5 = "\x00\x00\x00\x00\x00"
363
+
364
+ def encode_int(kind, value, min, max, pack_fmt)
365
+ raise Error.new("invalid_argument", "integer out of range") if value < min || value > max
366
+
367
+ (kind.chr + "\x00" + reserved5).b + [value].pack(pack_fmt)
368
+ end
369
+
370
+ def encode_uint(kind, value, max, pack_fmt)
371
+ raise Error.new("invalid_argument", "integer out of range") if value < 0 || value > max
372
+
373
+ (kind.chr + "\x00" + reserved5).b + [value].pack(pack_fmt)
374
+ end
375
+
376
+ def encode_enum(label, labels)
377
+ ord = labels.index(label)
378
+ raise Error.new("invalid_argument", "value is not a member of the ENUM label set") if ord.nil?
379
+
380
+ (KIND_ENUM.chr + "\x00" + reserved5).b + append_enum_labels(labels) + u16le(ord)
381
+ end
382
+
383
+ def encode_param(v)
384
+ case v
385
+ when nil
386
+ (KIND_STRING.chr + FLAG_NULL.chr + reserved5).b
387
+ when true, false
388
+ (KIND_BOOL.chr + "\x00" + reserved5 + (v ? "\x01" : "\x00")).b
389
+ when Integer
390
+ (KIND_DECIMAL.chr + "\x00" + reserved5).b + encode_decimal(v.to_s)
391
+ when BigDecimal
392
+ (KIND_DECIMAL.chr + "\x00" + reserved5).b + encode_decimal(v.to_s("F"))
393
+ when Float
394
+ raise Error.new("invalid_argument", "parameter is not finite") unless v.finite?
395
+
396
+ (KIND_DECIMAL.chr + "\x00" + reserved5).b + encode_decimal(format("%.17g", v))
397
+ when String
398
+ # A binary-encoded (ASCII-8BIT, e.g. via String#b) String is a BLOB;
399
+ # any other encoding (the UTF-8 default included) stays STRING.
400
+ kind = v.encoding == Encoding::ASCII_8BIT ? KIND_BLOB : KIND_STRING
401
+ (kind.chr + "\x00" + reserved5).b + u32bytes(v.b, MAX_PACKET)
402
+ when Time, DateTime
403
+ t = v.is_a?(DateTime) ? v.to_time : v
404
+ ns = (t.to_r * 1_000_000_000).to_i
405
+ (KIND_TIMESTAMPTZ.chr + "\x00" + reserved5).b + [ns].pack("q<")
406
+ when Date
407
+ # Checked after Time/DateTime above — DateTime is Date's own
408
+ # subclass in Ruby's stdlib, so only a bare Date reaches here.
409
+ day_count = (v - Date.new(1970, 1, 1)).to_i
410
+ (KIND_DATE.chr + "\x00" + reserved5).b + [day_count].pack("l<")
411
+ when NaiveTimestamp
412
+ # Treats v.value's own wall-clock fields as literal, ignoring
413
+ # whatever offset it carries — constructs a UTC Time from the same
414
+ # Y/M/D/H/M/S/usec fields rather than converting through the
415
+ # absolute instant, matching "the civil value read literally with
416
+ # no offset applied" (docs/design-datatypes.md D7). Converting
417
+ # through the absolute instant (t.to_r directly, as TimestampTZ
418
+ # does above) would be wrong here: a local-zoned Time's wall-clock
419
+ # reading is not its UTC epoch value.
420
+ t = v.value
421
+ t = t.to_time if t.is_a?(DateTime)
422
+ usec = (t.subsec * 1_000_000).to_i
423
+ utc_civil = Time.utc(t.year, t.month, t.day, t.hour, t.min, t.sec, usec)
424
+ ns = (utc_civil.to_r * 1_000_000_000).to_i
425
+ (KIND_TIMESTAMP.chr + "\x00" + reserved5).b + [ns].pack("q<")
426
+ when TimeOfDay
427
+ (KIND_TIME.chr + "\x00" + reserved5).b + [v.nanos_since_midnight].pack("Q<")
428
+ when Float32
429
+ # NaN/+-Infinity are valid FLOAT32/FLOAT64 values (unlike the bare
430
+ # Float -> Decimal path above, which requires finite) — the server
431
+ # canonicalizes -0.0 -> +0.0 and every NaN payload to one value
432
+ # (docs/design-datatypes.md D8).
433
+ (KIND_FLOAT32.chr + "\x00" + reserved5).b + [v.value].pack("e")
434
+ when Float64
435
+ (KIND_FLOAT64.chr + "\x00" + reserved5).b + [v.value].pack("E")
436
+ when Interval
437
+ (KIND_INTERVAL.chr + "\x00" + reserved5).b + [v.months, v.days].pack("l<l<") + [v.nanos].pack("q<")
438
+ when Int8
439
+ encode_int(KIND_INT8, v.value, -0x80, 0x7f, "c")
440
+ when Int16
441
+ encode_int(KIND_INT16, v.value, -0x8000, 0x7fff, "s<")
442
+ when Int32
443
+ encode_int(KIND_INT32, v.value, -0x80000000, 0x7fffffff, "l<")
444
+ when Int64
445
+ encode_int(KIND_INT64, v.value, -0x8000000000000000, 0x7fffffffffffffff, "q<")
446
+ when Uint8
447
+ encode_uint(KIND_UINT8, v.value, 0xff, "C")
448
+ when Uint16
449
+ encode_uint(KIND_UINT16, v.value, 0xffff, "S<")
450
+ when Uint32
451
+ encode_uint(KIND_UINT32, v.value, 0xffffffff, "L<")
452
+ when Uint64
453
+ encode_uint(KIND_UINT64, v.value, 0xffffffffffffffff, "Q<")
454
+ when EnumValue
455
+ encode_enum(v.value, v.labels)
456
+ when Point
457
+ (KIND_POINT.chr + "\x00" + reserved5).b + [v.lon, v.lat].pack("E2")
458
+ when Box
459
+ (KIND_BOX.chr + "\x00" + reserved5).b + [v.west, v.south, v.east, v.north].pack("E4")
460
+ when Vector
461
+ encode_vector(v)
462
+ when StructValue, MapValue
463
+ encode_collection_param(v)
464
+ when Geometry
465
+ wkt = v.wkt || v.to_wkt
466
+ wkt = "SRID=#{v.srid};#{wkt}" if v.srid && wkt !~ /\ASRID=/i
467
+ (KIND_STRING.chr + "\x00" + reserved5).b + u32bytes(wkt.b, MAX_PACKET)
468
+ when Array
469
+ if !v.empty? && v.all? { |x| x.is_a?(Numeric) }
470
+ encode_vector(Vector.new(dim: v.size, values: v.map(&:to_f)))
471
+ else
472
+ # A non-numeric (or empty) Array is an ARRAY collection param; the
473
+ # server re-coerces element types against the destination column.
474
+ encode_collection_param(v)
475
+ end
476
+ when Hash
477
+ json = JSON.generate(v)
478
+ (KIND_STRING.chr + "\x00" + reserved5).b + u32bytes(json.b, MAX_PACKET)
479
+ else
480
+ raise Error.new("invalid_argument", "unsupported parameter type: #{v.class}")
481
+ end
482
+ end
483
+
484
+ # --- Collections (STRUCT / ARRAY / MAP), docs/design-collections.md -------
485
+
486
+ def read_type_full(b, off, depth)
487
+ need!(b, off, 6, "type")
488
+ t = { kind: b.getbyte(off), precision: u16(b, off + 1), scale: u16(b, off + 3), elem: b.getbyte(off + 5) }
489
+ nxt = read_nested_descriptor(b, off + 6, t, depth)
490
+ [t, nxt]
491
+ end
492
+
493
+ def read_nested_descriptor(b, off, t, depth)
494
+ raise ProtocolError, "collection type nesting too deep" if depth > MAX_NEST_DEPTH + 1
495
+
496
+ case t[:kind]
497
+ when KIND_ENUM
498
+ labels, off = read_enum_labels(b, off)
499
+ t[:labels] = labels
500
+ off
501
+ when KIND_ARRAY
502
+ et, off = read_type_full(b, off, depth + 1)
503
+ t[:elem_type] = et
504
+ off
505
+ when KIND_MAP
506
+ kt, off = read_type_full(b, off, depth + 1)
507
+ vt, off = read_type_full(b, off, depth + 1)
508
+ t[:key_type] = kt
509
+ t[:elem_type] = vt
510
+ off
511
+ when KIND_STRUCT
512
+ need!(b, off, 2, "struct field count")
513
+ n = u16(b, off)
514
+ raise ProtocolError, "struct field count out of range" if n.zero? || n > MAX_STRUCT_FIELDS
515
+
516
+ off += 2
517
+ fields = []
518
+ n.times do
519
+ name, off = read_u16_string(b, off, 255)
520
+ ft, off = read_type_full(b, off, depth + 1)
521
+ fields << [name, ft]
522
+ end
523
+ t[:fields] = fields
524
+ off
525
+ else
526
+ off
527
+ end
528
+ end
529
+
530
+ def decode_payload(b, off, t)
531
+ kind = t[:kind]
532
+ return decode_collection_payload(b, off, t) if [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
533
+
534
+ header = (kind.chr + "\x00" + reserved5).b
535
+ header << append_enum_labels(t[:labels] || []) if kind == KIND_ENUM
536
+ synthetic = header + b.byteslice(off, b.bytesize - off)
537
+ value, nxt, = decode_value(synthetic, 0)
538
+ [value, off + (nxt - header.bytesize)]
539
+ end
540
+
541
+ def decode_collection_payload(b, off, t)
542
+ need!(b, off, 4, "collection")
543
+ body_len = u32(b, off)
544
+ body_end = off + 4 + body_len
545
+ need!(b, off + 4, body_len, "collection body")
546
+ p = off + 4
547
+ n = u32(b, p)
548
+ p += 4
549
+ raise ProtocolError, "collection member count out of range" if n > (2 * MAX_COLLECTION_LEN) + 2 || n > body_len
550
+
551
+ nb = (n + 7) / 8
552
+ nulls = b.byteslice(p, nb)
553
+ p += nb
554
+ kind = t[:kind]
555
+ members = []
556
+ n.times do |i|
557
+ if (nulls.getbyte(i / 8) & (1 << (i % 8))) != 0
558
+ members << nil
559
+ next
560
+ end
561
+ mt = if kind == KIND_STRUCT
562
+ t[:fields][i][1]
563
+ elsif kind == KIND_ARRAY
564
+ t[:elem_type]
565
+ else
566
+ i.even? ? t[:key_type] : t[:elem_type]
567
+ end
568
+ value, p = decode_payload(b, p, mt)
569
+ members << value
570
+ end
571
+ case kind
572
+ when KIND_STRUCT
573
+ out = {}
574
+ t[:fields].each_with_index { |(name, _), i| out[name] = members[i] }
575
+ [out, body_end]
576
+ when KIND_ARRAY
577
+ [members, body_end]
578
+ else
579
+ out = {}
580
+ (0...(members.size - 1)).step(2) { |i| out[members[i]] = members[i + 1] }
581
+ [out, body_end]
582
+ end
583
+ end
584
+
585
+ def encode_type_full(t)
586
+ out = +(t[:kind].chr + reserved5).b
587
+ case t[:kind]
588
+ when KIND_ENUM
589
+ out << append_enum_labels(t[:labels] || [])
590
+ when KIND_ARRAY
591
+ out << encode_type_full(t[:elem_type])
592
+ when KIND_MAP
593
+ out << encode_type_full(t[:key_type]) << encode_type_full(t[:elem_type])
594
+ when KIND_STRUCT
595
+ out << u16le(t[:fields].size)
596
+ t[:fields].each { |name, ft| out << u16str(name, 255) << encode_type_full(ft) }
597
+ end
598
+ out
599
+ end
600
+
601
+ def infer_value(v)
602
+ case v
603
+ when nil
604
+ return [{ kind: KIND_STRING }, nil]
605
+ when StructValue
606
+ fields = []
607
+ payloads = []
608
+ v.fields.each do |name, fv|
609
+ ft, pl = infer_value(fv)
610
+ fields << [name.to_s, ft]
611
+ payloads << pl
612
+ end
613
+ return [{ kind: KIND_STRUCT, fields: fields }, collection_payload(payloads)]
614
+ when MapValue
615
+ items = v.entries.is_a?(Hash) ? v.entries.to_a : v.entries
616
+ types = []
617
+ payloads = []
618
+ items.each do |k, val|
619
+ kt, kp = infer_value(k)
620
+ vt, vp = infer_value(val)
621
+ types << kt << vt
622
+ payloads << kp << vp
623
+ end
624
+ ki = (0...payloads.size).step(2).find { |i| !payloads[i].nil? }
625
+ vi = (1...payloads.size).step(2).find { |i| !payloads[i].nil? }
626
+ key_type = ki ? types[ki] : { kind: KIND_STRING }
627
+ val_type = vi ? types[vi] : { kind: KIND_STRING }
628
+ return [{ kind: KIND_MAP, key_type: key_type, elem_type: val_type }, collection_payload(payloads)]
629
+ when Array
630
+ types = []
631
+ payloads = []
632
+ v.each do |x|
633
+ xt, xp = infer_value(x)
634
+ types << xt
635
+ payloads << xp
636
+ end
637
+ ei = payloads.index { |pl| !pl.nil? }
638
+ elem_type = ei ? types[ei] : { kind: KIND_STRING }
639
+ return [{ kind: KIND_ARRAY, elem_type: elem_type }, collection_payload(payloads)]
640
+ end
641
+ enc = encode_param(v)
642
+ kind = enc.getbyte(0)
643
+ hdr = 7
644
+ if kind == KIND_ENUM
645
+ lc = u16(enc, 7)
646
+ hdr = 9
647
+ lc.times { hdr += 2 + u16(enc, hdr) }
648
+ end
649
+ [{ kind: kind }, enc.byteslice(hdr, enc.bytesize - hdr)]
650
+ end
651
+
652
+ def collection_payload(payloads)
653
+ n = payloads.size
654
+ nb = (n + 7) / 8
655
+ nulls = Array.new(nb, 0)
656
+ chunks = +"".b
657
+ payloads.each_with_index do |pl, i|
658
+ if pl.nil?
659
+ nulls[i / 8] |= 1 << (i % 8)
660
+ else
661
+ chunks << pl
662
+ end
663
+ end
664
+ body = u32le(n) + nulls.pack("C*") + chunks
665
+ u32le(body.bytesize) + body
666
+ end
667
+
668
+ def encode_collection_param(v)
669
+ t, payload = infer_value(v)
670
+ full = encode_type_full(t)
671
+ type_body = full.byteslice(1, full.bytesize - 1)
672
+ (t[:kind].chr + "\x00").b + type_body + (payload || "".b)
673
+ end
674
+
675
+ # --- Spatial: EWKB decode (Spatial track, docs/design-spatial.md) ------
676
+
677
+ EWKB_TYPES = {
678
+ 1 => "Point", 2 => "LineString", 3 => "Polygon",
679
+ 4 => "MultiPoint", 5 => "MultiLineString", 6 => "MultiPolygon",
680
+ 7 => "GeometryCollection"
681
+ }.freeze
682
+ EWKB_SRID_FLAG = 0x20000000
683
+
684
+ def decode_ewkb(b, off, depth)
685
+ raise ProtocolError, "geometry nesting too deep" if depth > 8
686
+
687
+ need!(b, off, 5, "geometry header")
688
+ raise ProtocolError, "only little-endian EWKB is supported" unless b.getbyte(off) == 1
689
+
690
+ tword = u32(b, off + 1)
691
+ gtype = tword & ~EWKB_SRID_FLAG
692
+ p = off + 5
693
+ srid = 0
694
+ if (tword & EWKB_SRID_FLAG) != 0
695
+ srid = u32(b, p)
696
+ p += 4
697
+ end
698
+ name = EWKB_TYPES[gtype]
699
+ raise ProtocolError, "unknown geometry type" unless name
700
+
701
+ f64 = lambda {
702
+ v = b.byteslice(p, 8).unpack1("E")
703
+ p += 8
704
+ v
705
+ }
706
+ u32f = lambda {
707
+ v = u32(b, p)
708
+ p += 4
709
+ v
710
+ }
711
+ pts = ->(n) { Array.new(n) { [f64.call, f64.call] } }
712
+
713
+ case gtype
714
+ when 1
715
+ [Geometry.new(type: name, srid: srid, coordinates: [f64.call, f64.call]), p]
716
+ when 2
717
+ [Geometry.new(type: name, srid: srid, coordinates: pts.call(u32f.call)), p]
718
+ when 3
719
+ nr = u32f.call
720
+ rings = Array.new(nr) { pts.call(u32f.call) }
721
+ [Geometry.new(type: name, srid: srid, coordinates: rings), p]
722
+ else
723
+ np = u32f.call
724
+ parts = []
725
+ np.times do
726
+ sub, p = decode_ewkb(b, p, depth + 1)
727
+ parts << sub
728
+ end
729
+ if gtype == 7
730
+ [Geometry.new(type: name, srid: srid, geometries: parts), p]
731
+ else
732
+ [Geometry.new(type: name, srid: srid, coordinates: parts.map(&:coordinates)), p]
733
+ end
734
+ end
735
+ end
736
+
737
+ def decode_value(b, off)
738
+ need!(b, off, 7, "value header")
739
+ kind = b.getbyte(off)
740
+ flags = b.getbyte(off + 1)
741
+ off += 7
742
+ enum_labels = nil
743
+ coll_type = nil
744
+ if kind == KIND_ENUM
745
+ enum_labels, off = read_enum_labels(b, off)
746
+ elsif [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
747
+ coll_type = { kind: kind }
748
+ off = read_nested_descriptor(b, off, coll_type, 0)
749
+ end
750
+ return [nil, off, kind] if flags & FLAG_NULL != 0
751
+
752
+ if [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
753
+ value, nxt = decode_collection_payload(b, off, coll_type)
754
+ return [value, nxt, kind]
755
+ end
756
+
757
+ case kind
758
+ when KIND_ENUM
759
+ need!(b, off, 2, "enum")
760
+ ord = u16(b, off)
761
+ raise ProtocolError, "ENUM ordinal out of range" if ord >= enum_labels.size
762
+
763
+ [enum_labels[ord], off + 2, kind]
764
+ when KIND_UUID
765
+ need!(b, off, 16, "uuid")
766
+ [format_uuid(b.byteslice(off, 16)), off + 16, kind]
767
+ when KIND_STRING, KIND_TEXT, KIND_CHAR, KIND_VARCHAR
768
+ raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
769
+ [raw.dup.force_encoding("UTF-8"), next_off, kind]
770
+ when KIND_BLOB
771
+ raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
772
+ [raw.dup.force_encoding("ASCII-8BIT"), next_off, kind]
773
+ when KIND_JSON
774
+ raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
775
+ [decode_nsjb(raw), next_off, kind]
776
+ when KIND_DECIMAL
777
+ raw, next_off = read_u32_bytes(b, off, MAX_PACKET)
778
+ [decode_decimal(raw), next_off, kind]
779
+ when KIND_TIMESTAMPTZ
780
+ ns = i64(b, off)
781
+ sec, nsec = ns.divmod(1_000_000_000)
782
+ [Time.at(sec, nsec, :nanosecond, in: "UTC"), off + 8, kind]
783
+ when KIND_TIMESTAMP
784
+ # Naive/no-timezone: same wire shape as TimestampTZ. Ruby has no
785
+ # distinct naive-time type, so this returns a UTC-tagged Time whose
786
+ # fields are the intended civil value — same convention as
787
+ # TimestampTZ's own decode above, just carrying no real zone
788
+ # information (docs/design-datatypes.md D7).
789
+ ns = i64(b, off)
790
+ sec, nsec = ns.divmod(1_000_000_000)
791
+ [Time.at(sec, nsec, :nanosecond, in: "UTC"), off + 8, kind]
792
+ when KIND_DATE
793
+ need!(b, off, 4, "date")
794
+ day_count = b.byteslice(off, 4).unpack1("l<")
795
+ [Date.new(1970, 1, 1) + day_count, off + 4, kind]
796
+ when KIND_TIME
797
+ need!(b, off, 8, "time")
798
+ [u64(b, off), off + 8, kind]
799
+ when KIND_FLOAT32
800
+ need!(b, off, 4, "float32")
801
+ [b.byteslice(off, 4).unpack1("e"), off + 4, kind]
802
+ when KIND_FLOAT64
803
+ need!(b, off, 8, "float64")
804
+ [b.byteslice(off, 8).unpack1("E"), off + 8, kind]
805
+ when KIND_INTERVAL
806
+ need!(b, off, 16, "interval")
807
+ months, days = b.byteslice(off, 8).unpack("l<l<")
808
+ nanos = b.byteslice(off + 8, 8).unpack1("q<")
809
+ [Interval.new(months: months, days: days, nanos: nanos), off + 16, kind]
810
+ when KIND_BOOL
811
+ need!(b, off, 1, "bool")
812
+ [b.getbyte(off) != 0, off + 1, kind]
813
+ when KIND_INT8
814
+ need!(b, off, 1, "int8")
815
+ [b.byteslice(off, 1).unpack1("c"), off + 1, kind]
816
+ when KIND_INT16
817
+ need!(b, off, 2, "int16")
818
+ [b.byteslice(off, 2).unpack1("s<"), off + 2, kind]
819
+ when KIND_INT32
820
+ need!(b, off, 4, "int32")
821
+ [b.byteslice(off, 4).unpack1("l<"), off + 4, kind]
822
+ when KIND_INT64
823
+ need!(b, off, 8, "int64")
824
+ [b.byteslice(off, 8).unpack1("q<"), off + 8, kind]
825
+ when KIND_UINT8
826
+ need!(b, off, 1, "uint8")
827
+ [b.getbyte(off), off + 1, kind]
828
+ when KIND_UINT16
829
+ need!(b, off, 2, "uint16")
830
+ [b.byteslice(off, 2).unpack1("S<"), off + 2, kind]
831
+ when KIND_UINT32
832
+ need!(b, off, 4, "uint32")
833
+ [b.byteslice(off, 4).unpack1("L<"), off + 4, kind]
834
+ when KIND_UINT64
835
+ need!(b, off, 8, "uint64")
836
+ [b.byteslice(off, 8).unpack1("Q<"), off + 8, kind]
837
+ when KIND_VECTOR
838
+ value, next_off = decode_vector(b, off)
839
+ [value, next_off, kind]
840
+ when KIND_POINT
841
+ need!(b, off, 16, "point")
842
+ lon, lat = b.byteslice(off, 16).unpack("E2")
843
+ [Point.new(lon, lat), off + 16, kind]
844
+ when KIND_BOX
845
+ need!(b, off, 32, "box")
846
+ w, s, e, n = b.byteslice(off, 32).unpack("E4")
847
+ [Box.new(w, s, e, n), off + 32, kind]
848
+ when KIND_LINE
849
+ n = u16(b, off)
850
+ p = off + 2
851
+ coords = []
852
+ (n * 2).times do
853
+ need!(b, p, 8, "line coord")
854
+ coords << b.byteslice(p, 8).unpack1("E")
855
+ p += 8
856
+ end
857
+ [Line.new(coords), p, kind]
858
+ when KIND_GEOMETRY, KIND_GEOGRAPHY
859
+ len = u32(b, off)
860
+ g, = decode_ewkb(b, off + 4, 0)
861
+ [g, off + 4 + len, kind]
862
+ when KIND_POLYGON
863
+ nr = u16(b, off)
864
+ p = off + 2
865
+ rings = []
866
+ nr.times do
867
+ npts = u16(b, p)
868
+ p += 2
869
+ ring = []
870
+ (npts * 2).times do
871
+ need!(b, p, 8, "polygon coord")
872
+ ring << b.byteslice(p, 8).unpack1("E")
873
+ p += 8
874
+ end
875
+ rings << ring
876
+ end
877
+ [Polygon.new(rings), p, kind]
878
+ else
879
+ raise ProtocolError, "unsupported type"
880
+ end
881
+ end
882
+
883
+ def decode_vector(b, off)
884
+ dim = u16(b, off)
885
+ flag = b.getbyte(off + 2)
886
+ if flag & 1 != 0
887
+ return [Vector.new(dim: dim, ref: true), off + 3]
888
+ end
889
+ if flag & 2 != 0
890
+ need!(b, off + 3, 4, "sparse nnz")
891
+ nnz = u32(b, off + 3)
892
+ indices = []
893
+ values = []
894
+ p = off + 7
895
+ nnz.times do
896
+ need!(b, p, 8, "sparse entry")
897
+ indices << u32(b, p)
898
+ values << b.byteslice(p + 4, 4).unpack1("e")
899
+ p += 8
900
+ end
901
+ return [Vector.new(dim: dim, values: values, indices: indices), p]
902
+ end
903
+ p = off + 3
904
+ values = []
905
+ dim.times do
906
+ need!(b, p, 4, "vector component")
907
+ values << b.byteslice(p, 4).unpack1("e")
908
+ p += 4
909
+ end
910
+ [Vector.new(dim: dim, values: values), p]
911
+ end
912
+
913
+ def decode_row_desc(b)
914
+ n = u16(b, 0)
915
+ off = 2
916
+ cols = []
917
+ n.times do
918
+ name, off2 = read_u16_string(b, off, MAX_NAME)
919
+ off = off2
920
+ need!(b, off, 6, "column type")
921
+ kind = b.getbyte(off)
922
+ off += 6
923
+ labels = nil
924
+ coll_type = nil
925
+ if kind == KIND_ENUM
926
+ labels, off = read_enum_labels(b, off)
927
+ elsif [KIND_STRUCT, KIND_ARRAY, KIND_MAP].include?(kind)
928
+ coll_type = { kind: kind }
929
+ off = read_nested_descriptor(b, off, coll_type, 0)
930
+ end
931
+ cols << Column.new(name, kind, labels, coll_type)
932
+ end
933
+ cols
934
+ end
935
+
936
+ def decode_data_batch(b)
937
+ nrows = u32(b, 0)
938
+ off = 4
939
+ rows = []
940
+ nrows.times do
941
+ ncols = u16(b, off)
942
+ off += 2
943
+ row = []
944
+ ncols.times do
945
+ value, next_off, = decode_value(b, off)
946
+ row << value
947
+ off = next_off
948
+ end
949
+ rows << row
950
+ end
951
+ rows
952
+ end
953
+
954
+ def decode_command_complete(b)
955
+ raise ProtocolError, "bad command-complete length" unless b.bytesize == 8
956
+
957
+ u64(b, 0)
958
+ end
959
+
960
+ def decode_error(b)
961
+ code, off = read_u16_string(b, 0, MAX_NAME)
962
+ msg, off = read_u16_string(b, off, MAX_NAME)
963
+ # Optional trailing field, present only from a server that accepted
964
+ # FLAG_PUBLIC_ERROR_CODES. Its absence is normal -- an older server
965
+ # ignores the request bit -- so this must never be required.
966
+ public_code = ""
967
+ if off < b.bytesize
968
+ public_code, = read_u16_string(b, off, MAX_NAME)
969
+ raise ProtocolError, "empty public error code" if public_code.empty?
970
+ end
971
+ Error.new(code, msg, public_code)
972
+ end
973
+
974
+ def encode_set_read_consistency(mode, max_staleness_ms)
975
+ raise Error.new("invalid_argument", "unknown read consistency mode") unless [READ_STRONG, READ_BOUNDED,
976
+ READ_STALE].include?(mode)
977
+
978
+ ms = max_staleness_ms.positive? ? max_staleness_ms : 0
979
+ mode.chr + u64le(ms)
980
+ end
981
+
982
+ def decode_node_status(b)
983
+ role, off = read_u16_string(b, 0, MAX_NAME)
984
+ raise ProtocolError, "bad node-status length" unless b.bytesize - off == 25
985
+
986
+ flags = b.getbyte(off)
987
+ off += 1
988
+ applied_lsn = u64(b, off)
989
+ raw_contact = u64(b, off + 8)
990
+ last_contact_ms = raw_contact == 0xFFFFFFFFFFFFFFFF ? -1 : raw_contact
991
+ apply_backlog = u64(b, off + 16)
992
+ NodeStatus.new(role, flags & 1 != 0, flags & 2 != 0, applied_lsn, last_contact_ms, apply_backlog)
993
+ end
994
+
995
+ DECIMAL_RE = /\A\d+(\.\d+)?\z/.freeze
996
+
997
+ def encode_decimal(s)
998
+ s = s.strip
999
+ neg = false
1000
+ s = s[1..] if s.start_with?("+")
1001
+ if s.start_with?("-")
1002
+ neg = true
1003
+ s = s[1..]
1004
+ end
1005
+ raise Error.new("invalid_argument", "invalid decimal") unless DECIMAL_RE.match?(s)
1006
+
1007
+ int_part, frac_part = s.split(".", 2)
1008
+ frac_part ||= ""
1009
+ scale = frac_part.length
1010
+ digits = (int_part + frac_part).sub(/\A0+(?=\d)/, "")
1011
+ coef = dec_to_bytes(digits)
1012
+ body = (neg ? "\x01" : "\x00").b + "\x00".b + u16le(scale) + coef
1013
+ u32bytes(body, MAX_PACKET)
1014
+ end
1015
+
1016
+ def decode_decimal(body)
1017
+ raise Error.new("invalid_format", "truncated decimal") if body.bytesize < 4
1018
+
1019
+ neg = body.getbyte(0) & 1 != 0
1020
+ scale = u16(body, 2)
1021
+ digits = bytes_to_dec(body.byteslice(4, body.bytesize - 4))
1022
+ s = if scale.positive?
1023
+ digits = digits.rjust(scale + 1, "0")
1024
+ "#{digits[0..-scale - 1]}.#{digits[-scale..]}"
1025
+ else
1026
+ digits
1027
+ end
1028
+ s = "-#{s}" if neg && !(s == "0" || s.match?(/\A0\.0+\z/))
1029
+ BigDecimal(s)
1030
+ end
1031
+
1032
+ def dec_to_bytes(digits)
1033
+ n = digits.to_i
1034
+ return "".b if n.zero?
1035
+
1036
+ nbytes = (n.bit_length + 7) / 8
1037
+ out = +"".b
1038
+ nbytes.times { |i| out.prepend(((n >> (8 * i)) & 0xFF).chr) }
1039
+ out
1040
+ end
1041
+
1042
+ def bytes_to_dec(raw)
1043
+ n = 0
1044
+ raw.each_byte { |byte| n = (n * 256) + byte }
1045
+ n.to_s
1046
+ end
1047
+
1048
+ def format_uuid(raw)
1049
+ hex = raw.unpack1("H*")
1050
+ "#{hex[0, 8]}-#{hex[8, 4]}-#{hex[12, 4]}-#{hex[16, 4]}-#{hex[20, 12]}"
1051
+ end
1052
+
1053
+ def encode_vector(v)
1054
+ dim = v.dim && v.dim.positive? ? v.dim : v.values.size
1055
+ header = (KIND_VECTOR.chr + "\x00" + reserved5).b
1056
+ if v.indices
1057
+ payload = +(u16le(dim) + "\x02")
1058
+ payload << u32le(v.indices.size)
1059
+ v.indices.each_with_index do |idx, i|
1060
+ payload << u32le(idx)
1061
+ payload << [v.values[i]].pack("e")
1062
+ end
1063
+ return header + payload
1064
+ end
1065
+ payload = +(u16le(dim) + "\x00")
1066
+ v.values.each { |val| payload << [val].pack("e") }
1067
+ header + payload
1068
+ end
1069
+
1070
+ # --- NSJB binary JSON (see internal/protocol/messages.go EncodeJSON) ---
1071
+
1072
+ def decode_nsjb(doc)
1073
+ raise Error.new("invalid_format", "not binary JSON") if doc.bytesize < 5 || doc.byteslice(0,
1074
+ 4) != "NSJB" || doc.getbyte(4) != 1
1075
+
1076
+ value, next_off = read_nsjb(doc, 5)
1077
+ raise Error.new("invalid_format", "trailing JSON bytes") unless next_off == doc.bytesize
1078
+
1079
+ value
1080
+ end
1081
+
1082
+ def read_nsjb(b, off)
1083
+ raise Error.new("invalid_format", "truncated JSON") if off >= b.bytesize
1084
+
1085
+ tag = b.getbyte(off)
1086
+ case tag
1087
+ when 0x00 then [nil, off + 1]
1088
+ when 0x01 then [false, off + 1]
1089
+ when 0x02 then [true, off + 1]
1090
+ when 0x03 then [i64(b, off + 1), off + 9]
1091
+ when 0x04 then read_nsjb_str(b, off, false)
1092
+ when 0x05 then read_nsjb_str(b, off, true)
1093
+ when 0x06 then read_nsjb_array(b, off)
1094
+ when 0x07 then read_nsjb_object(b, off)
1095
+ else raise Error.new("invalid_format", "unknown JSON tag")
1096
+ end
1097
+ end
1098
+
1099
+ def read_nsjb_str(b, off, number)
1100
+ n = u32(b, off + 1)
1101
+ end_off = off + 5 + n
1102
+ s = b.byteslice(off + 5, n).force_encoding("UTF-8")
1103
+ if number
1104
+ if /\A-?\d+\z/.match?(s)
1105
+ return [s.to_i, end_off]
1106
+ elsif /\A-?\d+\.\d+\z/.match?(s)
1107
+ return [s.to_f, end_off]
1108
+ end
1109
+ end
1110
+ [s, end_off]
1111
+ end
1112
+
1113
+ def read_nsjb_array(b, off)
1114
+ size = u32(b, off + 1)
1115
+ body = off + 5
1116
+ end_off = body + size
1117
+ count = u32(b, body)
1118
+ cur = body + 4
1119
+ out = []
1120
+ count.times do
1121
+ v, cur2 = read_nsjb(b, cur)
1122
+ cur = cur2
1123
+ out << v
1124
+ end
1125
+ [out, end_off]
1126
+ end
1127
+
1128
+ def read_nsjb_object(b, off)
1129
+ size = u32(b, off + 1)
1130
+ body = off + 5
1131
+ end_off = body + size
1132
+ count = u16(b, body)
1133
+ cur = body + 2
1134
+ out = {}
1135
+ count.times do
1136
+ klen = u16(b, cur)
1137
+ cur += 2
1138
+ key = b.byteslice(cur, klen).force_encoding("UTF-8")
1139
+ cur += klen
1140
+ v, cur2 = read_nsjb(b, cur)
1141
+ cur = cur2
1142
+ out[key] = v
1143
+ end
1144
+ [out, end_off]
1145
+ end
1146
+ end
1147
+ end