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
# 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
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

#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



178
179
180
181
182
# File 'lib/cabriolet/binary/bitstream.rb', line 178

def byte_align
  discard_bits = @bits_left % 8
  @bit_buffer >>= discard_bits
  @bits_left -= discard_bits
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



188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/cabriolet/binary/bitstream.rb', line 188

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:



39
40
41
42
43
44
45
46
47
48
49
50
# File 'lib/cabriolet/binary/bitstream.rb', line 39

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



253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
# File 'lib/cabriolet/binary/bitstream.rb', line 253

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:



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

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_uint16_leInteger

Read a 16-bit little-endian value

Returns:

  • (Integer)

    16-bit value



274
275
276
# File 'lib/cabriolet/binary/bitstream.rb', line 274

def read_uint16_le
  read_bits(16)
end

#read_uint32_leInteger

Read a 32-bit little-endian value

Returns:

  • (Integer)

    32-bit value



281
282
283
284
285
# File 'lib/cabriolet/binary/bitstream.rb', line 281

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



290
291
292
293
294
295
296
# File 'lib/cabriolet/binary/bitstream.rb', line 290

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



244
245
246
247
# File 'lib/cabriolet/binary/bitstream.rb', line 244

def skip_bits(num_bits)
  read_bits(num_bits)
  nil
end