Module refinery.lib.un7z.bcj2
BCJ2 decoder for 7z archives.
BCJ2 is a complex x86 branch filter with 4 input streams and 1 output stream. It uses a range coder to predict whether the relative address of an x86 CALL, JMP, or conditional jump instruction was converted to an absolute one.
Stream layout: - Stream 0 (MAIN): Non-branch bytes and branch instruction opcodes - Stream 1 (CALL): 4-byte big-endian CALL target addresses - Stream 2 (JUMP): 4-byte big-endian JMP and conditional jump target addresses - Stream 3 (RC): Range coder data for branch prediction
Expand source code Browse git
"""
BCJ2 decoder for 7z archives.
BCJ2 is a complex x86 branch filter with 4 input streams and 1 output stream.
It uses a range coder to predict whether the relative address of an x86 CALL, JMP, or conditional
jump instruction was converted to an absolute one.
Stream layout:
- Stream 0 (MAIN): Non-branch bytes and branch instruction opcodes
- Stream 1 (CALL): 4-byte big-endian CALL target addresses
- Stream 2 (JUMP): 4-byte big-endian JMP and conditional jump target addresses
- Stream 3 (RC): Range coder data for branch prediction
"""
from __future__ import annotations
import re
from typing import TYPE_CHECKING
from refinery.lib.un7z.headers import SzCorruptArchive
if TYPE_CHECKING:
from refinery.lib.types import buf
_TOP_VALUE = 1 << 24
_NUM_MODEL_BITS = 11
_BIT_MODEL_TOTAL = 1 << _NUM_MODEL_BITS
_NUM_MOVE_BITS = 5
_NUM_PROBS = 2 + 256
_MASK32 = 0xFFFFFFFF
_BRANCH_OPCODE = re.compile(B'[\xE8\xE9]|\x0F[\x80-\x8F]')
def decode_bcj2(
main_data: buf,
call_data: buf,
jump_data: buf,
rc_data: buf,
output_size: int,
) -> bytearray:
"""
Decode the four BCJ2 streams. The output is shorter than `output_size` when the main stream
ends early.
"""
main = memoryview(main_data)
call = memoryview(call_data)
jump = memoryview(jump_data)
rc = memoryview(rc_data)
if len(rc) < 5:
raise SzCorruptArchive('BCJ2: range coder stream too short.')
if rc[0] != 0:
raise SzCorruptArchive('BCJ2: range coder stream must start with 0x00.')
code = int.from_bytes(rc[1:5], 'big')
if code == _MASK32:
raise SzCorruptArchive('BCJ2: range coder stream starts with an invalid code.')
range_ = _MASK32
rc_pos = 5
call_pos = 0
jump_pos = 0
main_pos = 0
prev_byte = 0
probs = [_BIT_MODEL_TOTAL >> 1] * _NUM_PROBS
output = bytearray()
find_opcode = _BRANCH_OPCODE.search
while True:
if range_ < _TOP_VALUE:
if rc_pos >= len(rc):
raise SzCorruptArchive('BCJ2: unexpected end of range coder stream.')
range_ = (range_ << 8) & _MASK32
code = ((code << 8) | rc[rc_pos]) & _MASK32
rc_pos += 1
end = min(len(main), main_pos + output_size - len(output))
if prev_byte == 0x0F and main_pos < end and main[main_pos] & 0xF0 == 0x80:
opcode_pos = main_pos
elif match := find_opcode(main, main_pos, end):
opcode_pos = match.end() - 1
else:
output.extend(main[main_pos:end])
break
opcode = main[opcode_pos]
if opcode_pos > main_pos:
prev_byte = main[opcode_pos - 1]
output.extend(main[main_pos:opcode_pos + 1])
main_pos = opcode_pos + 1
if opcode == 0xE8:
prob_index = 2 + prev_byte
elif opcode == 0xE9:
prob_index = 1
else:
prob_index = 0
ttt = probs[prob_index]
bound = (range_ >> _NUM_MODEL_BITS) * ttt
if code < bound:
range_ = bound
probs[prob_index] = ttt + ((_BIT_MODEL_TOTAL - ttt) >> _NUM_MOVE_BITS)
prev_byte = opcode
continue
range_ -= bound
code -= bound
probs[prob_index] = ttt - (ttt >> _NUM_MOVE_BITS)
if opcode == 0xE8:
if call_pos + 4 > len(call):
raise SzCorruptArchive('BCJ2: unexpected end of CALL stream.')
val = int.from_bytes(call[call_pos:call_pos + 4], 'big')
call_pos += 4
else:
if jump_pos + 4 > len(jump):
raise SzCorruptArchive('BCJ2: unexpected end of JUMP stream.')
val = int.from_bytes(jump[jump_pos:jump_pos + 4], 'big')
jump_pos += 4
val = (val - len(output) - 4) & _MASK32
output.extend(val.to_bytes(4, 'little'))
prev_byte = val >> 24
del output[output_size:]
return output
Functions
def decode_bcj2(main_data, call_data, jump_data, rc_data, output_size)-
Decode the four BCJ2 streams. The output is shorter than
output_sizewhen the main stream ends early.Expand source code Browse git
def decode_bcj2( main_data: buf, call_data: buf, jump_data: buf, rc_data: buf, output_size: int, ) -> bytearray: """ Decode the four BCJ2 streams. The output is shorter than `output_size` when the main stream ends early. """ main = memoryview(main_data) call = memoryview(call_data) jump = memoryview(jump_data) rc = memoryview(rc_data) if len(rc) < 5: raise SzCorruptArchive('BCJ2: range coder stream too short.') if rc[0] != 0: raise SzCorruptArchive('BCJ2: range coder stream must start with 0x00.') code = int.from_bytes(rc[1:5], 'big') if code == _MASK32: raise SzCorruptArchive('BCJ2: range coder stream starts with an invalid code.') range_ = _MASK32 rc_pos = 5 call_pos = 0 jump_pos = 0 main_pos = 0 prev_byte = 0 probs = [_BIT_MODEL_TOTAL >> 1] * _NUM_PROBS output = bytearray() find_opcode = _BRANCH_OPCODE.search while True: if range_ < _TOP_VALUE: if rc_pos >= len(rc): raise SzCorruptArchive('BCJ2: unexpected end of range coder stream.') range_ = (range_ << 8) & _MASK32 code = ((code << 8) | rc[rc_pos]) & _MASK32 rc_pos += 1 end = min(len(main), main_pos + output_size - len(output)) if prev_byte == 0x0F and main_pos < end and main[main_pos] & 0xF0 == 0x80: opcode_pos = main_pos elif match := find_opcode(main, main_pos, end): opcode_pos = match.end() - 1 else: output.extend(main[main_pos:end]) break opcode = main[opcode_pos] if opcode_pos > main_pos: prev_byte = main[opcode_pos - 1] output.extend(main[main_pos:opcode_pos + 1]) main_pos = opcode_pos + 1 if opcode == 0xE8: prob_index = 2 + prev_byte elif opcode == 0xE9: prob_index = 1 else: prob_index = 0 ttt = probs[prob_index] bound = (range_ >> _NUM_MODEL_BITS) * ttt if code < bound: range_ = bound probs[prob_index] = ttt + ((_BIT_MODEL_TOTAL - ttt) >> _NUM_MOVE_BITS) prev_byte = opcode continue range_ -= bound code -= bound probs[prob_index] = ttt - (ttt >> _NUM_MOVE_BITS) if opcode == 0xE8: if call_pos + 4 > len(call): raise SzCorruptArchive('BCJ2: unexpected end of CALL stream.') val = int.from_bytes(call[call_pos:call_pos + 4], 'big') call_pos += 4 else: if jump_pos + 4 > len(jump): raise SzCorruptArchive('BCJ2: unexpected end of JUMP stream.') val = int.from_bytes(jump[jump_pos:jump_pos + 4], 'big') jump_pos += 4 val = (val - len(output) - 4) & _MASK32 output.extend(val.to_bytes(4, 'little')) prev_byte = val >> 24 del output[output_size:] return output