nyxis 1.0.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/nxs.rb +425 -16
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9bd1872ba3ef0efd235fc272ba37c6aee39e556f53ca2945fbaee06bd841b8a4
|
|
4
|
+
data.tar.gz: 251aa729cf14696afc55b02e932d4b5d2d1f4c96b9e0e2a7cc209c8f92fe7c45
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c184c7035fa80b665290cf86841216a16a007a9fbd65129fad9984562fee38ebc67ce5e0a0ab0123c16d02d13d0e2d1221c9c7bed23d66c661bc52638b4e1ede
|
|
7
|
+
data.tar.gz: cf362b97c76c51a6bed1fc744e1da723c1a728a3c10e3ac7067cd67bbc45e7589d292cadbc30e763b08129f13baa5c9fb5d2c99536dcf79d952728949cdd233f
|
data/nxs.rb
CHANGED
|
@@ -22,8 +22,18 @@
|
|
|
22
22
|
module Nxs
|
|
23
23
|
MAGIC_FILE = 0x4E595842 # NYXB
|
|
24
24
|
MAGIC_OBJ = 0x4E59584F # NYXO
|
|
25
|
+
MAGIC_LIST = 0x4E59584C # NYXL
|
|
26
|
+
MAGIC_PAGE = 0x4E585350 # NYXP
|
|
25
27
|
MAGIC_FOOTER = 0x2153584E # NXS!
|
|
26
|
-
|
|
28
|
+
FLAG_COLUMNAR = 0x0001
|
|
29
|
+
FLAG_PAX = 0x0004
|
|
30
|
+
FLAG_SCHEMA = 0x0002
|
|
31
|
+
|
|
32
|
+
FOOTER_ROW_BYTES = 12
|
|
33
|
+
FOOTER_COL_BYTES = 20
|
|
34
|
+
FOOTER_PAX_BYTES = 28
|
|
35
|
+
COL_TAIL_ENTRY_BYTES = 20
|
|
36
|
+
PAX_TAIL_ENTRY_BYTES = 28
|
|
27
37
|
|
|
28
38
|
class NxsError < StandardError
|
|
29
39
|
attr_reader :code
|
|
@@ -37,7 +47,7 @@ module Nxs
|
|
|
37
47
|
# ── Reader ──────────────────────────────────────────────────────────────────
|
|
38
48
|
|
|
39
49
|
class Reader
|
|
40
|
-
attr_reader :keys, :record_count
|
|
50
|
+
attr_reader :keys, :record_count, :layout
|
|
41
51
|
|
|
42
52
|
def initialize(bytes)
|
|
43
53
|
@data = bytes.b # force binary encoding
|
|
@@ -51,12 +61,14 @@ module Nxs
|
|
|
51
61
|
raise NxsError.new('ERR_BAD_MAGIC', 'footer magic mismatch') if footer != MAGIC_FOOTER
|
|
52
62
|
|
|
53
63
|
# Preamble: Version(2) + Flags(2) + DictHash(8) + TailPtr(8) + Reserved(8)
|
|
54
|
-
@flags
|
|
55
|
-
|
|
56
|
-
|
|
64
|
+
@flags = @data.unpack1('@6 S<')
|
|
65
|
+
preamble_tail = @data.unpack1('@16 Q<')
|
|
66
|
+
@tail_ptr = preamble_tail
|
|
67
|
+
layout_flags = @flags & (FLAG_COLUMNAR | FLAG_PAX)
|
|
68
|
+
if @tail_ptr.zero? && layout_flags.zero?
|
|
57
69
|
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'stream footer') if sz < 44
|
|
58
70
|
|
|
59
|
-
@tail_ptr = @data.unpack1("@#{sz -
|
|
71
|
+
@tail_ptr = @data.unpack1("@#{sz - FOOTER_ROW_BYTES}Q<")
|
|
60
72
|
end
|
|
61
73
|
|
|
62
74
|
@dict_hash = @data.unpack1('@8 Q<')
|
|
@@ -71,24 +83,27 @@ module Nxs
|
|
|
71
83
|
raise NxsError.new('ERR_DICT_MISMATCH', 'schema hash mismatch') if computed != @dict_hash
|
|
72
84
|
end
|
|
73
85
|
|
|
74
|
-
|
|
75
|
-
@
|
|
76
|
-
|
|
86
|
+
@col_buf_off = []
|
|
87
|
+
@col_buf_len = []
|
|
88
|
+
parse_layout_tail!(preamble_tail)
|
|
77
89
|
end
|
|
78
90
|
|
|
79
|
-
# O(1) record lookup —
|
|
91
|
+
# O(1) record lookup — row tail-index or columnar/PAX record index.
|
|
80
92
|
def record(i)
|
|
81
93
|
unless i >= 0 && i < @record_count
|
|
82
94
|
raise NxsError.new('ERR_OUT_OF_BOUNDS', "record #{i} out of [0, #{@record_count})")
|
|
83
95
|
end
|
|
84
96
|
|
|
85
|
-
|
|
97
|
+
return Object.new(self, i, i) if @layout != :row
|
|
98
|
+
|
|
86
99
|
abs_offset = @data.unpack1("@#{@tail_start + i * 10 + 2}Q<")
|
|
87
100
|
Object.new(self, abs_offset)
|
|
88
101
|
end
|
|
89
102
|
|
|
90
|
-
#
|
|
103
|
+
# Sum f64 column — columnar/PAX buffer path or row scan.
|
|
91
104
|
def sum_f64(key)
|
|
105
|
+
return col_sum_f64(key) if @layout != :row
|
|
106
|
+
|
|
92
107
|
slot = @key_index[key]
|
|
93
108
|
raise NxsError.new('ERR_OUT_OF_BOUNDS', "key '#{key}' not in schema") unless slot
|
|
94
109
|
|
|
@@ -106,6 +121,68 @@ module Nxs
|
|
|
106
121
|
sum
|
|
107
122
|
end
|
|
108
123
|
|
|
124
|
+
# Columnar/PAX f64 sum (row layout delegates to sum_f64).
|
|
125
|
+
def col_sum_f64(key)
|
|
126
|
+
slot = @key_index[key]
|
|
127
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', "key '#{key}' not in schema") unless slot
|
|
128
|
+
|
|
129
|
+
return sum_f64(key) if @layout == :row
|
|
130
|
+
return pax_sum_f64(slot) if @layout == :pax
|
|
131
|
+
|
|
132
|
+
bm, vals = col_field_parts(slot)
|
|
133
|
+
n = @record_count
|
|
134
|
+
sum = 0.0
|
|
135
|
+
i = 0
|
|
136
|
+
while i < n
|
|
137
|
+
if col_bit(bm, i)
|
|
138
|
+
off = i * 8
|
|
139
|
+
sum += vals.unpack1("@#{off}E") if off + 8 <= vals.bytesize
|
|
140
|
+
end
|
|
141
|
+
i += 1
|
|
142
|
+
end
|
|
143
|
+
sum
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
# Raw value bytes for a fixed-width column (columnar/PAX).
|
|
147
|
+
def col_buffer(key)
|
|
148
|
+
raise NxsError.new('ERR_LAYOUT', 'col_buffer requires columnar or PAX layout') if @layout == :row
|
|
149
|
+
|
|
150
|
+
slot = @key_index[key]
|
|
151
|
+
return nil unless slot
|
|
152
|
+
return nil if var_sigil?(@key_sigils[slot])
|
|
153
|
+
|
|
154
|
+
_bm, vals = col_field_parts(slot)
|
|
155
|
+
vals
|
|
156
|
+
rescue NxsError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Null bitmap + u32 offsets + values for var-length columns (columnar only).
|
|
161
|
+
def col_var_buffer(key)
|
|
162
|
+
raise NxsError.new('ERR_LAYOUT', 'col_var_buffer is columnar-only') unless @layout == :columnar
|
|
163
|
+
|
|
164
|
+
slot = @key_index[key]
|
|
165
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', "key '#{key}' not in schema") unless slot
|
|
166
|
+
raise NxsError.new('ERR_UNSUPPORTED_FIELD_TYPE', key) unless var_sigil?(@key_sigils[slot])
|
|
167
|
+
|
|
168
|
+
bm, offsets, values = col_var_parts(slot)
|
|
169
|
+
{ bitmap: bm, offsets: offsets, values: values, count: @record_count }
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def col_get_str(key, record_index)
|
|
173
|
+
slot = @key_index[key]
|
|
174
|
+
return nil unless slot && record_index < @record_count && @layout != :row
|
|
175
|
+
return nil unless @key_sigils[slot] == 0x22
|
|
176
|
+
|
|
177
|
+
bm, offsets, values, ok = col_var_parts_at(record_index, slot)
|
|
178
|
+
return nil unless ok
|
|
179
|
+
|
|
180
|
+
bit_idx = @layout == :pax ? pax_find_page(record_index)&.[](:local) : record_index
|
|
181
|
+
return nil if bit_idx.nil? || !col_bit(bm, bit_idx)
|
|
182
|
+
|
|
183
|
+
var_str_at(offsets, values, bit_idx)
|
|
184
|
+
end
|
|
185
|
+
|
|
109
186
|
def min_f64(key)
|
|
110
187
|
slot = @key_index[key]
|
|
111
188
|
raise NxsError.new('ERR_OUT_OF_BOUNDS', "key '#{key}' not in schema") unless slot
|
|
@@ -178,6 +255,8 @@ module Nxs
|
|
|
178
255
|
t_idx = 0
|
|
179
256
|
|
|
180
257
|
loop do
|
|
258
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'bitmask overrun on corrupt input') if p >= data.bytesize
|
|
259
|
+
|
|
181
260
|
b = data.getbyte(p)
|
|
182
261
|
p += 1
|
|
183
262
|
bits = b & 0x7F
|
|
@@ -205,6 +284,285 @@ module Nxs
|
|
|
205
284
|
|
|
206
285
|
private
|
|
207
286
|
|
|
287
|
+
def parse_layout_tail!(preamble_tail)
|
|
288
|
+
if (@flags & FLAG_COLUMNAR != 0) && (@flags & FLAG_PAX != 0)
|
|
289
|
+
raise NxsError.new('ERR_INVALID_FLAGS', 'columnar and PAX both set')
|
|
290
|
+
end
|
|
291
|
+
if (@flags & FLAG_COLUMNAR != 0) && preamble_tail.zero?
|
|
292
|
+
raise NxsError.new('ERR_INCOMPATIBLE_FLAGS', 'columnar with TailPtr=0')
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
if (@flags & FLAG_COLUMNAR) != 0
|
|
296
|
+
@layout = :columnar
|
|
297
|
+
parse_columnar_footer!
|
|
298
|
+
return
|
|
299
|
+
end
|
|
300
|
+
if (@flags & FLAG_PAX) != 0
|
|
301
|
+
@layout = :pax
|
|
302
|
+
parse_pax_footer!
|
|
303
|
+
return
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
@layout = :row
|
|
307
|
+
if preamble_tail.zero?
|
|
308
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'streamable footer') if @data.bytesize < 44
|
|
309
|
+
|
|
310
|
+
@tail_ptr = @data.unpack1("@#{@data.bytesize - FOOTER_ROW_BYTES}Q<")
|
|
311
|
+
end
|
|
312
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'tail index') if @tail_ptr + 4 > @data.bytesize
|
|
313
|
+
|
|
314
|
+
@record_count = @data.unpack1("@#{@tail_ptr}L<")
|
|
315
|
+
@tail_start = @tail_ptr + 4
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
def parse_columnar_footer!
|
|
319
|
+
sz = @data.bytesize
|
|
320
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'columnar footer') if sz < FOOTER_COL_BYTES
|
|
321
|
+
|
|
322
|
+
fo = sz - FOOTER_COL_BYTES
|
|
323
|
+
@tail_ptr = @data.unpack1("@#{fo}Q<")
|
|
324
|
+
@record_count = @data.unpack1("@#{fo + 8}Q<")
|
|
325
|
+
@tail_start = @tail_ptr
|
|
326
|
+
kc = @keys.length
|
|
327
|
+
@col_buf_off = Array.new(kc)
|
|
328
|
+
@col_buf_len = Array.new(kc)
|
|
329
|
+
kc.times do |i|
|
|
330
|
+
e = @tail_start + i * COL_TAIL_ENTRY_BYTES
|
|
331
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'columnar tail entry') if e + COL_TAIL_ENTRY_BYTES > sz
|
|
332
|
+
|
|
333
|
+
fid = @data.unpack1("@#{e}S<")
|
|
334
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', "invalid field ID #{fid}") if fid >= kc
|
|
335
|
+
|
|
336
|
+
@col_buf_off[fid] = @data.unpack1("@#{e + 4}Q<")
|
|
337
|
+
@col_buf_len[fid] = @data.unpack1("@#{e + 12}Q<")
|
|
338
|
+
end
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
def parse_pax_footer!
|
|
342
|
+
sz = @data.bytesize
|
|
343
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'PAX footer') if sz < FOOTER_PAX_BYTES
|
|
344
|
+
|
|
345
|
+
fo = sz - FOOTER_PAX_BYTES
|
|
346
|
+
@tail_ptr = @data.unpack1("@#{fo}Q<")
|
|
347
|
+
@record_count = @data.unpack1("@#{fo + 8}Q<")
|
|
348
|
+
@page_count = @data.unpack1("@#{fo + 16}L<")
|
|
349
|
+
@page_size_hint = @data.unpack1("@#{fo + 20}L<")
|
|
350
|
+
@tail_start = @tail_ptr
|
|
351
|
+
@page_index = []
|
|
352
|
+
@page_rec_start = []
|
|
353
|
+
@page_rec_count = []
|
|
354
|
+
@page_offset = []
|
|
355
|
+
@page_length = []
|
|
356
|
+
|
|
357
|
+
@page_count.times do |i|
|
|
358
|
+
e = @tail_start + i * PAX_TAIL_ENTRY_BYTES
|
|
359
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'PAX tail entry') if e + PAX_TAIL_ENTRY_BYTES > sz
|
|
360
|
+
|
|
361
|
+
@page_index << @data.unpack1("@#{e}L<")
|
|
362
|
+
@page_rec_start << @data.unpack1("@#{e + 4}Q<")
|
|
363
|
+
@page_rec_count << @data.unpack1("@#{e + 12}L<")
|
|
364
|
+
@page_offset << @data.unpack1("@#{e + 16}Q<")
|
|
365
|
+
@page_length << @data.unpack1("@#{e + 24}L<")
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
@page_count.times do |i|
|
|
369
|
+
poff = @page_offset[i]
|
|
370
|
+
if poff > sz || poff + 4 > sz || @data.unpack1("@#{poff}L<") != MAGIC_PAGE
|
|
371
|
+
raise NxsError.new('ERR_INVALID_PAGE_MAGIC', 'PAX page magic mismatch')
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
def null_bitmap_bytes(n)
|
|
377
|
+
raw = (n + 7) / 8
|
|
378
|
+
(raw + 7) & ~7
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
# rubocop:disable Naming/PredicateMethod -- mirrors C col_bit naming
|
|
382
|
+
def col_bit(bm, rec)
|
|
383
|
+
((bm.getbyte(rec / 8) >> (rec % 8)) & 1) == 1
|
|
384
|
+
end
|
|
385
|
+
# rubocop:enable Naming/PredicateMethod
|
|
386
|
+
|
|
387
|
+
def var_sigil?(sig)
|
|
388
|
+
[0x22, 0x3C].include?(sig)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def var_off_bytes_len(rc)
|
|
392
|
+
off = (rc + 1) * 4
|
|
393
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'var offsets overflow') if off > @data.bytesize
|
|
394
|
+
|
|
395
|
+
off
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def field_sector_len(sector_off, rc, sigil)
|
|
399
|
+
bm_len = null_bitmap_bytes(rc)
|
|
400
|
+
return bm_len + rc * 8 unless var_sigil?(sigil)
|
|
401
|
+
|
|
402
|
+
off_bytes = var_off_bytes_len(rc)
|
|
403
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'var offsets') if sector_off + bm_len + off_bytes > @data.bytesize
|
|
404
|
+
|
|
405
|
+
end_off = @data.unpack1("@#{sector_off + bm_len + rc * 4}L<")
|
|
406
|
+
total = bm_len + off_bytes + end_off
|
|
407
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'var values') if sector_off + total > @data.bytesize
|
|
408
|
+
|
|
409
|
+
total
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def var_str_at(offsets, values, record_index)
|
|
413
|
+
need = (record_index + 2) * 4
|
|
414
|
+
return nil if offsets.bytesize < need
|
|
415
|
+
|
|
416
|
+
off = record_index * 4
|
|
417
|
+
start = offsets.unpack1("@#{off}L<")
|
|
418
|
+
end_ = offsets.unpack1("@#{off + 4}L<")
|
|
419
|
+
return nil if end_ < start || end_ > values.bytesize
|
|
420
|
+
|
|
421
|
+
values[start...end_].force_encoding('UTF-8')
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def col_field_parts(slot)
|
|
425
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', "key slot #{slot}") if slot.negative? || slot >= @col_buf_off.length
|
|
426
|
+
|
|
427
|
+
off = @col_buf_off[slot].to_i
|
|
428
|
+
length = @col_buf_len[slot].to_i
|
|
429
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'column buffer') if off + length > @data.bytesize
|
|
430
|
+
|
|
431
|
+
bm_len = null_bitmap_bytes(@record_count)
|
|
432
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'null bitmap') if length < bm_len
|
|
433
|
+
|
|
434
|
+
sector = @data[off, length]
|
|
435
|
+
[sector[0, bm_len], sector[bm_len..]]
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def col_var_parts(slot)
|
|
439
|
+
bm, tail = col_field_parts(slot)
|
|
440
|
+
off_bytes = var_off_bytes_len(@record_count)
|
|
441
|
+
raise NxsError.new('ERR_OUT_OF_BOUNDS', 'var offsets') if tail.bytesize < off_bytes
|
|
442
|
+
|
|
443
|
+
[bm, tail[0, off_bytes], tail[off_bytes..]]
|
|
444
|
+
end
|
|
445
|
+
|
|
446
|
+
def col_var_parts_at(rec, slot)
|
|
447
|
+
return [nil, nil, nil, false] if slot.negative? || slot >= @key_sigils.length || !var_sigil?(@key_sigils[slot])
|
|
448
|
+
|
|
449
|
+
if @layout == :columnar
|
|
450
|
+
bm, offsets, values = col_var_parts(slot)
|
|
451
|
+
return [bm, offsets, values, true]
|
|
452
|
+
end
|
|
453
|
+
if @layout == :pax
|
|
454
|
+
loc = pax_find_page(rec)
|
|
455
|
+
return [nil, nil, nil, false] unless loc
|
|
456
|
+
|
|
457
|
+
bm, tail = page_field_parts(loc[:page], slot)
|
|
458
|
+
return [nil, nil, nil, false] unless bm
|
|
459
|
+
|
|
460
|
+
rc = @page_rec_count[loc[:page]]
|
|
461
|
+
off_bytes = var_off_bytes_len(rc)
|
|
462
|
+
return [nil, nil, nil, false] if tail.bytesize < off_bytes
|
|
463
|
+
|
|
464
|
+
return [bm, tail[0, off_bytes], tail[off_bytes..], true]
|
|
465
|
+
end
|
|
466
|
+
[nil, nil, nil, false]
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
def col_numeric_bytes(rec, slot)
|
|
470
|
+
return nil if slot >= 0 && slot < @key_sigils.length && var_sigil?(@key_sigils[slot])
|
|
471
|
+
|
|
472
|
+
if @layout == :columnar
|
|
473
|
+
bm, vals = col_field_parts(slot)
|
|
474
|
+
return nil if rec >= @record_count || !col_bit(bm, rec)
|
|
475
|
+
|
|
476
|
+
off = rec * 8
|
|
477
|
+
return nil if off + 8 > vals.bytesize
|
|
478
|
+
|
|
479
|
+
return vals[off, 8]
|
|
480
|
+
end
|
|
481
|
+
if @layout == :pax
|
|
482
|
+
loc = pax_find_page(rec)
|
|
483
|
+
return nil unless loc
|
|
484
|
+
|
|
485
|
+
bm, vals = page_field_parts(loc[:page], slot)
|
|
486
|
+
return nil unless bm && col_bit(bm, loc[:local])
|
|
487
|
+
|
|
488
|
+
off = loc[:local] * 8
|
|
489
|
+
return nil if off + 8 > vals.bytesize
|
|
490
|
+
|
|
491
|
+
return vals[off, 8]
|
|
492
|
+
end
|
|
493
|
+
nil
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def pax_find_page(rec)
|
|
497
|
+
return nil if @page_count.zero?
|
|
498
|
+
|
|
499
|
+
lo = 0
|
|
500
|
+
hi = @page_count - 1
|
|
501
|
+
while lo <= hi
|
|
502
|
+
mid = lo + (hi - lo) / 2
|
|
503
|
+
start = @page_rec_start[mid]
|
|
504
|
+
count = @page_rec_count[mid]
|
|
505
|
+
if rec < start
|
|
506
|
+
hi = mid - 1
|
|
507
|
+
elsif rec >= start + count
|
|
508
|
+
lo = mid + 1
|
|
509
|
+
else
|
|
510
|
+
return { page: mid, local: rec - start }
|
|
511
|
+
end
|
|
512
|
+
end
|
|
513
|
+
nil
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
def page_field_sector(pi, slot)
|
|
517
|
+
poff = @page_offset[pi].to_i
|
|
518
|
+
return nil if poff + 24 > @data.bytesize || @data.unpack1("@#{poff}L<") != MAGIC_PAGE
|
|
519
|
+
|
|
520
|
+
fc = @data.unpack1("@#{poff + 20}S<")
|
|
521
|
+
return nil if slot.negative? || slot >= fc || fc > @key_sigils.length
|
|
522
|
+
|
|
523
|
+
rc = @page_rec_count[pi]
|
|
524
|
+
body = poff + 24
|
|
525
|
+
slot.times do |fi|
|
|
526
|
+
sig = fi < @key_sigils.length ? @key_sigils[fi] : 0x3D
|
|
527
|
+
flen = field_sector_len(body, rc, sig)
|
|
528
|
+
body += flen
|
|
529
|
+
end
|
|
530
|
+
sig = slot < @key_sigils.length ? @key_sigils[slot] : 0x3D
|
|
531
|
+
flen = field_sector_len(body, rc, sig)
|
|
532
|
+
return nil if body + flen > @data.bytesize
|
|
533
|
+
|
|
534
|
+
@data[body, flen]
|
|
535
|
+
end
|
|
536
|
+
|
|
537
|
+
def page_field_parts(pi, slot)
|
|
538
|
+
sector = page_field_sector(pi, slot)
|
|
539
|
+
return [nil, nil] unless sector
|
|
540
|
+
|
|
541
|
+
bm_len = null_bitmap_bytes(@page_rec_count[pi])
|
|
542
|
+
return [nil, nil] if sector.bytesize < bm_len
|
|
543
|
+
|
|
544
|
+
[sector[0, bm_len], sector[bm_len..]]
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def pax_sum_f64(slot)
|
|
548
|
+
sum = 0.0
|
|
549
|
+
@page_count.times do |pi|
|
|
550
|
+
bm, vals = page_field_parts(pi, slot)
|
|
551
|
+
next unless bm
|
|
552
|
+
|
|
553
|
+
rc = @page_rec_count[pi]
|
|
554
|
+
i = 0
|
|
555
|
+
while i < rc
|
|
556
|
+
if col_bit(bm, i)
|
|
557
|
+
off = i * 8
|
|
558
|
+
sum += vals.unpack1("@#{off}E") if off + 8 <= vals.bytesize
|
|
559
|
+
end
|
|
560
|
+
i += 1
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
sum
|
|
564
|
+
end
|
|
565
|
+
|
|
208
566
|
def read_schema(offset)
|
|
209
567
|
key_count = @data.unpack1("@#{offset}S<")
|
|
210
568
|
offset += 2
|
|
@@ -438,13 +796,19 @@ module Nxs
|
|
|
438
796
|
# ── Object ───────────────────────────────────────────────────────────────────
|
|
439
797
|
|
|
440
798
|
class Object
|
|
441
|
-
def initialize(reader, offset)
|
|
442
|
-
@reader
|
|
443
|
-
@offset
|
|
444
|
-
@
|
|
799
|
+
def initialize(reader, offset, record_index = nil)
|
|
800
|
+
@reader = reader
|
|
801
|
+
@offset = offset
|
|
802
|
+
@record_index = record_index
|
|
803
|
+
@parsed = false
|
|
445
804
|
end
|
|
446
805
|
|
|
447
806
|
def get_str(key)
|
|
807
|
+
slot = @reader.key_index[key]
|
|
808
|
+
return nil unless slot
|
|
809
|
+
|
|
810
|
+
return @reader.col_get_str(key, record_index) if uses_columnar_field_access?
|
|
811
|
+
|
|
448
812
|
off = field_offset(key)
|
|
449
813
|
return nil unless off
|
|
450
814
|
|
|
@@ -453,6 +817,16 @@ module Nxs
|
|
|
453
817
|
end
|
|
454
818
|
|
|
455
819
|
def get_i64(key)
|
|
820
|
+
slot = @reader.key_index[key]
|
|
821
|
+
return nil unless slot
|
|
822
|
+
|
|
823
|
+
if uses_columnar_field_access?
|
|
824
|
+
cell = @reader.send(:col_numeric_bytes, record_index, slot)
|
|
825
|
+
return nil unless cell
|
|
826
|
+
|
|
827
|
+
return cell.unpack1('q<')
|
|
828
|
+
end
|
|
829
|
+
|
|
456
830
|
off = field_offset(key)
|
|
457
831
|
return nil unless off
|
|
458
832
|
|
|
@@ -460,6 +834,16 @@ module Nxs
|
|
|
460
834
|
end
|
|
461
835
|
|
|
462
836
|
def get_f64(key)
|
|
837
|
+
slot = @reader.key_index[key]
|
|
838
|
+
return nil unless slot
|
|
839
|
+
|
|
840
|
+
if uses_columnar_field_access?
|
|
841
|
+
cell = @reader.send(:col_numeric_bytes, record_index, slot)
|
|
842
|
+
return nil unless cell
|
|
843
|
+
|
|
844
|
+
return cell.unpack1('E')
|
|
845
|
+
end
|
|
846
|
+
|
|
463
847
|
off = field_offset(key)
|
|
464
848
|
return nil unless off
|
|
465
849
|
|
|
@@ -467,6 +851,16 @@ module Nxs
|
|
|
467
851
|
end
|
|
468
852
|
|
|
469
853
|
def get_bool(key)
|
|
854
|
+
slot = @reader.key_index[key]
|
|
855
|
+
return nil unless slot
|
|
856
|
+
|
|
857
|
+
if uses_columnar_field_access?
|
|
858
|
+
cell = @reader.send(:col_numeric_bytes, record_index, slot)
|
|
859
|
+
return nil unless cell
|
|
860
|
+
|
|
861
|
+
return cell.getbyte(0) != 0
|
|
862
|
+
end
|
|
863
|
+
|
|
470
864
|
off = field_offset(key)
|
|
471
865
|
return nil unless off
|
|
472
866
|
|
|
@@ -475,6 +869,21 @@ module Nxs
|
|
|
475
869
|
|
|
476
870
|
private
|
|
477
871
|
|
|
872
|
+
def record_index
|
|
873
|
+
@record_index.nil? ? @offset : @record_index
|
|
874
|
+
end
|
|
875
|
+
|
|
876
|
+
def obj_at_nyxo?
|
|
877
|
+
return false if @offset + 4 > @reader.data.bytesize
|
|
878
|
+
|
|
879
|
+
@reader.data.unpack1("@#{@offset}L<") == MAGIC_OBJ
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
# Columnar/PAX top-level records use record index; nested NYXO blobs use row paths.
|
|
883
|
+
def uses_columnar_field_access?
|
|
884
|
+
@reader.layout != :row && !obj_at_nyxo?
|
|
885
|
+
end
|
|
886
|
+
|
|
478
887
|
# Parse the object header (lazy — only on first field access).
|
|
479
888
|
def parse_header
|
|
480
889
|
return if @parsed
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nyxis
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Micael Malta
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-05-
|
|
11
|
+
date: 2026-05-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: |
|
|
14
14
|
Pure-Ruby reader for NXB files produced by the NXS compiler. Provides
|