Class: Cabriolet::Binary::Bitstream

Inherits:
Object
  • Object
show all
Defined in:
lib/cabriolet/binary/bitstream.rb

Overview

Bitstream provides bit-level I/O operations for reading compressed data

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(io_system, handle, buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false) ⇒ Bitstream

Initialize a new bitstream

Parameters:

  • io_system (System::IOSystem)

    I/O system for reading data

  • handle (System::FileHandle, System::MemoryHandle)

    Handle to read from

  • buffer_size (Integer) (defaults to: Cabriolet.default_buffer_size)

    Size of the input buffer

  • bit_order (Symbol) (defaults to: :lsb)

    Bit order (:lsb or :msb)

  • salvage (Boolean) (defaults to: false)

    Salvage mode - return 0 on EOF instead of raising



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/cabriolet/binary/bitstream.rb', line 16

def initialize(io_system, handle,
buffer_size = Cabriolet.default_buffer_size, bit_order: :lsb, salvage: false)
  @io_system = io_system
  @handle = handle
  @buffer_size = buffer_size
  @bit_order = bit_order
  @salvage = salvage
  @buffer = ""
  @buffer_pos = 0
  @bit_buffer = 0
  @bits_left = 0
  @input_end = false # Track EOF state (matches libmspack's input_end flag)

  # For MSB mode, we need to know the bit width of the buffer
  # Ruby integers are arbitrary precision, so we use 32 bits as standard
  @bitbuf_width = 32

  # Cache ENV lookups once at initialization
  @debug_bitstream = ENV.fetch("DEBUG_BITSTREAM", nil)
end

Instance Attribute Details

#bit_orderObject (readonly)

Returns the value of attribute bit_order.



7
8
9
# File 'lib/cabriolet/binary/bitstream.rb', line 7

def bit_order
  @bit_order
end

#bits_leftObject (readonly)

Returns the value of attribute bits_left.



7
8
9
# File 'lib/cabriolet/binary/bitstream.rb', line 7

def bits_left
  @bits_left
end

#buffer_sizeObject (readonly)

Returns the value of attribute buffer_size.



7
8
9
# File 'lib/cabriolet/binary/bitstream.rb', line 7

def buffer_size
  @buffer_size
end

#handleObject (readonly)

Returns the value of attribute handle.



7
8
9
# File 'lib/cabriolet/binary/bitstream.rb', line 7

def handle
  @handle
end

#io_systemObject (readonly)

Returns the value of attribute io_system.



7
8
9
# File 'lib/cabriolet/binary/bitstream.rb', line 7

def io_system
  @io_system
end

Instance Method Details

#byte_alignvoid

This method returns an undefined value.

Align to the next byte boundary



203
204
205
206
207
208
209
210
211
212
# File 'lib/cabriolet/binary/bitstream.rb', line 203

def byte_align
  discard_bits = @bits_left % 8
  if @bit_order == :msb
    # MSB mode: valid bits are at the left (high) end, shift left to discard
    @bit_buffer = (@bit_buffer << discard_bits) & ((1 << @bitbuf_width) - 1)
  else
    @bit_buffer >>= discard_bits
  end
  @bits_left -= discard_bits
end

#ensure_bits(num_bits) ⇒ void

This method returns an undefined value.

Ensure at least num_bits are available in the bit buffer. Reads from input if needed. Used for alignment operations.

Parameters:

  • num_bits (Integer)

    Minimum number of bits required



183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
# File 'lib/cabriolet/binary/bitstream.rb', line 183

def ensure_bits(num_bits)
  if @bit_order == :msb
    while @bits_left < num_bits
      word = read_msb_word
      @bit_buffer |= (word << (@bitbuf_width - 16 - @bits_left))
      @bits_left += 16
    end
  else
    while @bits_left < num_bits
      byte = read_byte
      byte = 0 if byte.nil?
      @bit_buffer |= (byte << @bits_left)
      @bits_left += 8
    end
  end
end

#flush_bit_buffervoid

This method returns an undefined value.

Flush the bit buffer entirely (discard all remaining bits). Per libmspack lzxd.c: used when transitioning to raw byte reading for uncompressed blocks. Sets bits_left=0 and bit_buffer=0.



219
220
221
222
# File 'lib/cabriolet/binary/bitstream.rb', line 219

def flush_bit_buffer
  @bit_buffer = 0
  @bits_left = 0
end

#peek_bits(num_bits) ⇒ Integer

Peek at bits without consuming them

Parameters:

  • num_bits (Integer)

    Number of bits to peek at

Returns:

  • (Integer)

    Bits as an integer



238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
# File 'lib/cabriolet/binary/bitstream.rb', line 238

def peek_bits(num_bits)
  if num_bits < 1 || num_bits > 32
    raise ArgumentError,
          "Can only peek 1-32 bits at a time"
  end

  if @bit_order == :msb
    # Ensure we have enough bits
    while @bits_left < num_bits
      # Read 2 bytes at a time (little-endian), like libmspack
      byte0 = read_byte
      if byte0.nil?
        # At EOF: break and work with remaining bits
        break
      end

      byte1 = read_byte
      byte1 = 0 if byte1.nil?

      # Combine as little-endian 16-bit value
      word = byte0 | (byte1 << 8)

      # INJECT_BITS (MSB): inject at the left side
      @bit_buffer |= (word << (@bitbuf_width - 16 - @bits_left))
      @bits_left += 16
    end

    # PEEK_BITS (MSB): extract from the left
    # If we have fewer than num_bits available, result may be incorrect
    # but this matches EOF handling behavior
    @bit_buffer >> (@bitbuf_width - num_bits)
  else
    # Ensure we have enough bits (LSB mode)
    while @bits_left < num_bits
      byte = read_byte
      if byte.nil?
        # At EOF: pad remaining bits with zeros and continue
        # This matches libmspack behavior where peek can use partial bits
        # The missing high bits are implicitly 0
        break
      end

      @bit_buffer |= (byte << @bits_left)
      @bits_left += 8
    end

    # Extract num_bits from bit_buffer
    # If we have fewer than num_bits, the high bits will be 0
    @bit_buffer & ((1 << num_bits) - 1)
  end
end

#read_bits(num_bits) ⇒ Integer

Read specified number of bits from the stream

Parameters:

  • num_bits (Integer)

    Number of bits to read (1-32)

Returns:

  • (Integer)

    Bits read as an integer

Raises:



42
43
44
45
46
47
48
49
50
51
52
53
# File 'lib/cabriolet/binary/bitstream.rb', line 42

def read_bits(num_bits)
  if num_bits < 1 || num_bits > 32
    raise ArgumentError,
          "Can only read 1-32 bits at a time"
  end

  if @bit_order == :msb
    read_bits_msb(num_bits)
  else
    read_bits_lsb(num_bits)
  end
end

#read_bits_be(num_bits) ⇒ Integer

Read bits in big-endian (MSB first) order

Parameters:

  • num_bits (Integer)

    Number of bits to read

Returns:

  • (Integer)

    Bits as an integer



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
# File 'lib/cabriolet/binary/bitstream.rb', line 303

def read_bits_be(num_bits)
  result = 0
  full_bytes = num_bits / 8
  remaining_bits = num_bits % 8

  # Read full bytes first (more efficient than bit-by-bit)
  full_bytes.times do
    result = (result << 8) | read_bits(8)
  end

  # Read remaining bits
  if remaining_bits.positive?
    result = (result << remaining_bits) | read_bits(remaining_bits)
  end

  result
end

#read_byteInteger?

Read a single byte from the input

Per libmspack readbits.h: On first EOF, we pad with zeros. On second EOF, we raise an error (unless salvage mode).

Returns:

  • (Integer, nil)

    Byte value or nil to signal EOF padding needed

Raises:



151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/cabriolet/binary/bitstream.rb', line 151

def read_byte
  if @buffer_pos >= @buffer.bytesize
    @buffer = @io_system.read(@handle, @buffer_size)
    @buffer_pos = 0

    if @buffer.empty?
      # Hit EOF - check if this is first or second EOF
      if @input_end
        # Second EOF: raise error unless salvage mode
        unless @salvage
          raise DecompressionError, "Unexpected end of input stream"
        end

        # In salvage mode, keep returning nil
      else
        # First EOF: signal to pad with zeros (return nil)
        @input_end = true
      end
      return nil
    end
  end

  byte = @buffer.getbyte(@buffer_pos)
  @buffer_pos += 1
  byte
end

#read_raw_byteInteger

Read a raw byte directly from the input, bypassing the bit buffer. Per libmspack lzxd.c: uncompressed block headers and data are read directly from the input pointer (i_ptr), not through the bitstream. Call flush_bit_buffer first to discard any residual bits.

Returns:

  • (Integer)

    Byte value (0 on EOF)



230
231
232
# File 'lib/cabriolet/binary/bitstream.rb', line 230

def read_raw_byte
  read_byte || 0
end

#read_uint16_leInteger

Read a 16-bit little-endian value

Returns:

  • (Integer)

    16-bit value



324
325
326
# File 'lib/cabriolet/binary/bitstream.rb', line 324

def read_uint16_le
  read_bits(16)
end

#read_uint32_leInteger

Read a 32-bit little-endian value

Returns:

  • (Integer)

    32-bit value



331
332
333
334
335
# File 'lib/cabriolet/binary/bitstream.rb', line 331

def read_uint32_le
  low = read_bits(16)
  high = read_bits(16)
  (high << 16) | low
end

#resetvoid

This method returns an undefined value.

Reset the bitstream state



340
341
342
343
344
345
346
# File 'lib/cabriolet/binary/bitstream.rb', line 340

def reset
  @buffer = ""
  @buffer_pos = 0
  @bit_buffer = 0
  @bits_left = 0
  @io_system.seek(@handle, 0, Constants::SEEK_START)
end

#skip_bits(num_bits) ⇒ void

This method returns an undefined value.

Skip specified number of bits

Parameters:

  • num_bits (Integer)

    Number of bits to skip



294
295
296
297
# File 'lib/cabriolet/binary/bitstream.rb', line 294

def skip_bits(num_bits)
  read_bits(num_bits)
  nil
end