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.
Files changed (3) hide show
  1. checksums.yaml +4 -4
  2. data/nxs.rb +425 -16
  3. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0ebcbfcd1dfc34288e738e81acc006a8cabda0fdc50e9e851c73e55d422dadb2
4
- data.tar.gz: d1eb4b53469a62e382bbf385cf039307af2a5be59b62ba0db0d6f0abe3dffe73
3
+ metadata.gz: 9bd1872ba3ef0efd235fc272ba37c6aee39e556f53ca2945fbaee06bd841b8a4
4
+ data.tar.gz: 251aa729cf14696afc55b02e932d4b5d2d1f4c96b9e0e2a7cc209c8f92fe7c45
5
5
  SHA512:
6
- metadata.gz: 74dcb43ae96b59f6a56b52dd64d03efb2074c8f3789a99fbe1889b26ed931cb9280fd418036d837702a28cf040d79722f7d51a120d43788aa239f1ec6dbb4d35
7
- data.tar.gz: 339bf6357eebf0557ea3d114dbe970eb42f9f796f6665db9ca6369a59c2c63237135b2ff4681b5305bea05a15f1fc6b09c8347e8a836244ae53afd9f354966c6
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
- FLAG_SCHEMA = 0x0002
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 = @data.unpack1('@6 S<')
55
- @tail_ptr = @data.unpack1('@16 Q<')
56
- if @tail_ptr.zero?
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 - 12}Q<")
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
- # Tail-index: u32 EntryCount followed by records
75
- @record_count = @data.unpack1("@#{@tail_ptr}L<")
76
- @tail_start = @tail_ptr + 4
86
+ @col_buf_off = []
87
+ @col_buf_len = []
88
+ parse_layout_tail!(preamble_tail)
77
89
  end
78
90
 
79
- # O(1) record lookup — reads one 10-byte tail-index entry.
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
- # Each tail-index entry: u16 KeyID + u64 AbsoluteOffset = 10 bytes
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
- # Tight allocation-free sum loop.
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 = reader
443
- @offset = offset
444
- @parsed = false
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.0.0
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-20 00:00:00.000000000 Z
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