pycrate/pycrate_core/charpy.py

1438 lines
55 KiB
Python

# -*- coding: UTF-8 -*-
#/**
# * Software Name : pycrate
# * Version : 0.4
# *
# * Copyright 2016. Benoit Michau. ANSSI.
# *
# * This library is free software; you can redistribute it and/or
# * modify it under the terms of the GNU Lesser General Public
# * License as published by the Free Software Foundation; either
# * version 2.1 of the License, or (at your option) any later version.
# *
# * This library is distributed in the hope that it will be useful,
# * but WITHOUT ANY WARRANTY; without even the implied warranty of
# * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# * Lesser General Public License for more details.
# *
# * You should have received a copy of the GNU Lesser General Public
# * License along with this library; if not, write to the Free Software
# * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
# * MA 02110-1301 USA
# *
# *--------------------------------------------------------
# * File Name : pycrate_core/charpy.py
# * Created : 2016-02-09
# * Authors : Benoit Michau
# *--------------------------------------------------------
#*/
__all__ = ['CharpyErr', 'Charpy']
from .utils import *
#------------------------------------------------------------------------------#
# Charpy specific error
#------------------------------------------------------------------------------#
class CharpyErr(PycrateErr):
pass
#------------------------------------------------------------------------------#
# Charpy object
#------------------------------------------------------------------------------#
class Charpy(object):
"""
Charpy is a bit-stream handler
It has ways to work over aligned and unaligned bit-stream,
exposing methods to:
- convert it to bytes buffer, bytelist, bitlist, unsigned integer,
signed integer
- consume it byte by byte or bit by bit to produce bytes buffer, bytelist,
bitlist, unsigned integer, signed integer
It uses the following attributes:
- _buf: bytes buffer
- _len_bit: buffer length in bits
- _cur: buffer cursor value in bits
- _REPR: to configure the instance representation
"""
_REPR_POS = ('buf', 'bytelist', 'bitlist', 'uint', 'int', 'hex', 'bin')
_REPR = 'buf'
_REPR_MAX = 512
def __init__(self, buf=None):
"""Initialize the charpy instance
Args:
buf (bytes or None): bytes buffer to initialize the charpy instance
_buf attribute; if None, _buf stays empty
"""
# initialize cursor
self._cur = 0
# initialize the intermediate container for concatenation
self._concat = []
# initialize internal value
if buf is None:
self._buf = b''
self._len_bit = 0
else:
self.set_bytes(buf)
def _pack(self):
if not self._concat:
return
# concatenate the content in _concat at the end of the current instance
cur = self._cur
self._cur = 0
self._concat.insert(0, (TYPE_BYTES, self._buf, self._len_bit))
self.set_bytes( *pack_val(*self._concat) )
self._concat = []
self._cur = cur
def len_bit(self):
"""Return the length in bits
Returns:
len_bit (integer): length in bits of the remaining buffer
"""
if self._concat: self._pack()
return self._len_bit - self._cur
def len_byte(self):
"""Return the length in bytes
Returns:
len_byte (integer): length in bytes of the remaining buffer
"""
bitlen = self.len_bit()
if bitlen % 8:
return 1+(bitlen>>3)
else:
return bitlen>>3
def bin(self):
"""Provide a binary representation of the remaining buffer
Returns:
bit_str (str): string of 0 and 1
"""
bitlen = self.len_bit()
if bitlen == 0:
return ''
else:
return uint_to_bitstr(self.to_uint(), bitlen)
def hex(self):
"""Provide an hexadecimal representation of the remaining buffer
Returns:
hex_str (str): string of hex character
"""
bitlen = self.len_bit()
if bitlen == 0:
return ''
else:
return uint_to_hex(self.to_uint(), bitlen)
# _REPR_POS = ('buf', 'bytelist', 'bitlist', 'uint', 'int', 'hex', 'bin')
def repr(self):
"""Provide a printable representation of the remaining buffer
Returns:
repr_str (str): string, according to _REPR and _REPR_MAX attributes
"""
if self._REPR == 'buf' or self._REPR not in self._REPR_POS:
# bytes buffer, default one
r = self.to_bytes()
if len(r) > self._REPR_MAX:
if python_version < 3:
return 'charpy(b{0}...{1})'.format(
repr(r[0:self._REPR_MAX])[:-1], repr(r[-2:])[1:])
else:
return 'charpy({0}...{1})'.format(
repr(r[0:self._REPR_MAX])[:-1], repr(r[-2:])[2:])
elif python_version < 3:
return 'charpy(b{0})'.format(repr(r))
else:
return 'charpy({0})'.format(repr(r))
elif self._REPR == 'bytelist':
# list of bytes
r = self.to_bytelist()
if len(r) > self._REPR_MAX:
return 'charpy([{0}, ..., {1}])'.format(\
repr(r[0:self._REPR_MAX])[1:-1], r[-1])
else:
return 'charpy({0})'.format(repr(r))
elif self._REPR == 'bitlist':
# list of bits
r = self.to_bitlist()
if len(r) > self._REPR_MAX:
return 'charpy([{0}, ..., {1}])'.format(\
repr(r[0:self._REPR_MAX])[1:-1], r[-1])
else:
return 'charpy({0})'.format(repr(r))
elif self._REPR == 'uint':
# big unsigned int
r = repr(self.to_uint())
if r[-1] == 'L':
# Python2 long case
r = r[:-1]
if len(r) > self._REPR_MAX:
return 'charpy({0}...{1})'.format(r[0:self._REPR_MAX], r[-1])
else:
return 'charpy({0})'.format(r)
elif self._REPR == 'int':
# big signed int
r = repr(self.to_int())
if r[-1] == 'L':
r = r[:-1]
if len(r) > self._REPR_MAX:
return 'charpy({0}...{1})'.format(r[0:self._REPR_MAX], r[-1])
else:
return 'charpy({0})'.format(r)
elif self._REPR == 'bin':
# string of 0 and 1
r = self.bin()
if len(r) > self._REPR_MAX:
return 'charpy(0b{0}...{1})'.format(r[0:self._REPR_MAX], r[-1])
else:
return 'charpy(0b{0})'.format(r)
elif self._REPR == 'hex':
# string of hex char
r = self.hex()
if len(r) > self._REPR_MAX:
return 'charpy(0x{0}...{1})'.format(r[0:self._REPR_MAX], r[-1])
else:
return 'charpy(0x{0})'.format(r)
def rewind(self, bitlen=None):
"""Rewind the charpy instance's cursor for the given bit length
Args:
bitlen : None or unsigned integer
if None, rewinds the charpy instance completely
else, rewinds for the given number of bits
Returns:
None
"""
if self._concat: self._pack()
if bitlen is None or bitlen > self._cur:
self._cur = 0
elif bitlen > 0:
self._cur -= bitlen
def forward(self, bitlen=None):
"""Set the charpy instance's cursor foward for the given bit length
Args:
bitlen : None or unsigned integer
if None, set the cursor at the end of the charpy instance
else, set the cursor forward for the given number of bits
Returns:
None
"""
if self._concat: self._pack()
if bitlen is None or bitlen > (self._len_bit - self._cur):
self._cur = self._len_bit
elif bitlen > 0:
self._cur += bitlen
def set_bytes(self, buf=b'', bitlen=None):
"""Reinitialize the charpy instance and its cursor by setting a Python
bytes buffer into it
Args:
buf (bytes) : bytes buffer
bitlen (integer) : length in bits for the buffer
if None, the whole bytes buffer is taken as is
Returns:
None
Raises:
CharpyErr : if `buf' has not the correct type
"""
if not isinstance(buf, bytes_types):
raise(CharpyErr('invalid argument type: {0}, expecting bytes'\
.format(type(buf).__name__)))
elif bitlen is None or bitlen < 0 or bitlen > 8*len(buf):
self._len_bit = 8*len(buf)
self._buf = buf
elif bitlen == 0:
self._len_bit = 0
self._buf = b''
else:
self._len_bit = bitlen
self._buf = buf
self._cur = 0
self._concat = []
def append_bytes(self, buf=b'', bitlen=None):
"""Append a bytes buffer at the end of the charpy instance
Args:
buf (bytes) : bytes buffer to be appended
bitlen (integer) : length in bits for the buffer to append
if None, the whole bytes buffer is taken as is
Returns:
None
Raises:
CharpyErr : if `buf' has not the correct type
"""
if not isinstance(buf, bytes_types):
raise(CharpyErr('invalid argument type: {0}'.format(type(buf))))
elif bitlen is None or bitlen > 8*len(buf):
bitlen = 8*len(buf)
elif bitlen <= 0:
return
self._concat.append( (TYPE_BYTES, buf, bitlen) )
def to_bytes(self, bitlen=None):
"""Provide the bytes buffer of the charpy instance, starting at the
current cursor position and ending after the given bitlen
Args:
bitlen (integer) : length in bits for the requested bytes buffer
if None, the whole charpy bytes buffer is returned
Returns:
buf (bytes) : the current charpy bytes buffer
if not byte-aligned, it is padded with 0 bits rightmost
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return b''
off_byte, off_bit = self._cur >>3, self._cur % 8
len_byte, len_bit = bitlen >>3, bitlen % 8
if off_bit == 0:
# aligned access
if len_bit == 0:
# byte-aligned buffer
return self._buf[off_byte:off_byte+len_byte]
else:
# byte-unaligned buffer
# need to zero last bits of the last byte
return bytes_zero_last_bits(
self._buf[off_byte:off_byte+len_byte+1], 8-len_bit)
else:
# unaligned access
if off_bit + len_bit > 8:
buf = bytes_lshift(self._buf[off_byte:off_byte+len_byte+2],
off_bit)[:-1]
else:
buf = bytes_lshift(self._buf[off_byte:off_byte+len_byte+1],
off_bit)
if len_bit == 0:
return buf[:-1]
# len_bit > 0
# need to zero last bits of the last byte
return bytes_zero_last_bits(buf, 8-len_bit)
def get_bytes(self, bitlen=None):
"""Consume the bytes buffer of the charpy instance, starting at the
current cursor position and ending after the given bitlen
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length in bits for the requested bytes buffer
if None, the whole charpy bytes buffer is returned
Returns:
buf (bytes) : the current charpy bytes buffer
if not byte-aligned, it is padded with 0 bits rightmost
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return b''
off_byte, off_bit = self._cur >>3, self._cur % 8
len_byte, len_bit = bitlen >>3, bitlen % 8
self._cur += bitlen
if off_bit == 0:
# aligned access
if len_bit == 0:
# byte-aligned buffer
return self._buf[off_byte:off_byte+len_byte]
else:
# byte-unaligned buffer
# need to zero last bits of the last byte
return bytes_zero_last_bits(
self._buf[off_byte:off_byte+len_byte+1], 8-len_bit)
else:
# unaligned access
if off_bit + len_bit > 8:
buf = bytes_lshift(self._buf[off_byte:off_byte+len_byte+2],
off_bit)[:-1]
else:
buf = bytes_lshift(self._buf[off_byte:off_byte+len_byte+1],
off_bit)
if len_bit == 0:
return buf[:-1]
# len_bit > 0
# need to zero last bits of the last byte
return bytes_zero_last_bits(buf, 8-len_bit)
def set_bytelist(self, bytelist=[], bitlen=None):
"""Reinitialize the charpy instance and its cursor by setting a list of
uint8 integer values into it
Args:
bytelist (iterable of integer) : iterable of uint8 values
bitlen (integer) : length in bits for the bytelist
if None, the whole bytelist is taken as is
Returns:
None
Raises:
CharpyErr : if `bytelist' has not the correct format
"""
try:
self._buf = bytelist_to_bytes(bytelist)
except Exception:
raise(CharpyErr('invalid argument: {0}'.format(bytelist)))
if bitlen is None or bitlen <= 0 or bitlen > 8*len(bytelist):
self._len_bit = 8*len(bytelist)
else:
self._len_bit = bitlen
self._cur = 0
self._concat = []
def to_bytelist(self, bitlen=None):
"""Provide the bytelist of the charpy instance, starting at the current
cursor position and ending after the given bitlen
Args:
bitlen (integer) : length in bits for the requested bytelist
if None, the whole charpy bytelist is returned
Returns:
bytelist (list of integer) : the current charpy bytelist
if not byte-aligned, it is padded with 0 bits rightmost
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return []
off_byte, off_bit = self._cur >>3, self._cur % 8
len_byte, len_bit = bitlen >>3, bitlen % 8
if off_bit == 0:
# aligned access
if len_bit == 0:
return bytes_to_bytelist(self._buf[off_byte:off_byte+len_byte])
else:
ret = bytes_to_bytelist(self._buf[off_byte:off_byte+len_byte+1])
ret[-1] &= ((1<<len_bit)-1)<<(8-len_bit)
return ret
else:
# unaligned access
if off_bit + len_bit > 8:
ret = bytes_lshift(self._buf[off_byte:off_byte+len_byte+2],
off_bit)[:-1]
else:
ret = bytes_lshift(self._buf[off_byte:off_byte+len_byte+1],
off_bit)
if len_bit == 0:
return bytes_to_bytelist(ret[:-1])
# len_bit > 0
# need to zero last bits of the last byte
ret = bytes_to_bytelist(ret)
ret[-1] &= ((1<<len_bit)-1)<<(8-len_bit)
return ret
def get_bytelist(self, bitlen=None):
"""Consume the bytelist of the charpy instance, starting at the current
cursor position and ending after the given bitlen
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length in bits for the requested bytelist
if None, the whole charpy bytelist is returned
Returns:
bytelist (list of integer) : the current charpy bytelist
if not byte-aligned, it is padded with 0 bits rightmost
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return []
off_byte, off_bit = self._cur >>3, self._cur % 8
len_byte, len_bit = bitlen >>3, bitlen % 8
self._cur += bitlen
if off_bit == 0:
# aligned access
if len_bit == 0:
return bytes_to_bytelist(self._buf[off_byte:off_byte+len_byte])
else:
ret = bytes_to_bytelist(self._buf[off_byte:off_byte+len_byte+1])
ret[-1] &= ((1<<len_bit)-1)<<(8-len_bit)
return ret
else:
# unaligned access
if off_bit + len_bit > 8:
ret = bytes_lshift(self._buf[off_byte:off_byte+len_byte+2],
off_bit)[:-1]
else:
ret = bytes_lshift(self._buf[off_byte:off_byte+len_byte+1],
off_bit)
if len_bit == 0:
return bytes_to_bytelist(ret[:-1])
# len_bit > 0
# need to zero last bits of the last byte
ret = bytes_to_bytelist(ret)
ret[-1] &= ((1<<len_bit)-1)<<(8-len_bit)
return ret
def set_bitlist(self, bitlist=[]):
"""Reinitialize the charpy instance and its cursor by setting a list of
binary integer values into it
Args:
bitlist (iterable of integer) : iterable of 0 and 1
Returns:
None
Raises:
CharpyErr : if `bitlist' has not the correct format
"""
try:
self._buf = bitlist_to_bytes(bitlist)
except Exception:
raise(CharpyErr('invalid argument: {0}'.format(bitlist)))
self._len_bit = len(bitlist)
self._cur = 0
self._concat = []
def to_bitlist(self, bitlen=None):
"""Provide the bitlist of the charpy instance, starting at the current
cursor position and ending after the given bitlen
Args:
bitlen (integer) : length for the requested bitlist
if None, the whole charpy bitlist is returned
Returns:
bitlist (list of integer) : the current charpy bitlist
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return []
off_byte, off_bit = self._cur >>3, self._cur % 8
len_byte, len_bit = bitlen >>3, bitlen % 8
if off_bit == 0:
# aligned access
if len_bit:
return bytes_to_bitlist( \
self.to_bytes(bitlen))[:len_bit-8]
else:
return bytes_to_bitlist(self.to_bytes(bitlen))
else:
self._cur -= off_bit
ret = bytes_to_bitlist(self.to_bytes(bitlen+off_bit))
self._cur += off_bit
return ret[off_bit:bitlen+off_bit]
def get_bitlist(self, bitlen=None):
"""Consume the bitlist of the charpy instance, starting at the current
cursor position and ending after the given bitlen
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length for the requested bitlist
if None, the whole charpy bitlist is returned
Returns:
bitlist (list of integer) : the current charpy bitlist
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return []
off_byte, off_bit = self._cur >>3, self._cur % 8
len_byte, len_bit = bitlen >>3, bitlen % 8
if off_bit == 0:
# aligned access
if len_bit:
ret = bytes_to_bitlist( \
self.to_bytes(bitlen))[:len_bit-8]
else:
ret = bytes_to_bitlist(self.to_bytes(bitlen))
self._cur += bitlen
return ret
else:
# unaligned access
# restore an aligned cursor to get a byte array
self._cur -= off_bit
ret = bytes_to_bitlist(self.to_bytes(bitlen+off_bit))
self._cur += off_bit + bitlen
return ret[off_bit:bitlen+off_bit]
def set_uint(self, val=0, bitlen=None):
"""Reinitialize the charpy instance and its cursor by setting an
arbitrary unsigned integer value into it
big endian representation is used (most significant byte leftmost,
least significant byte rightmost)
Args:
val (integer) : unsigned integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the value, value is majored to the
maximum uint value according to bitlen
if None, encoding is done in the minimum number of bits
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type or is negative
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif val < 0:
raise(CharpyErr('invalid argument value: {0}'.format(val)))
elif bitlen is None or bitlen < 0:
# Python 2.7 and 3 built-in
bitlen = val.bit_length()
if bitlen == 0:
self._buf = b''
else:
self._buf = uint_to_bytes(val, bitlen)
self._len_bit = bitlen
self._cur = 0
self._concat = []
def append_uint(self, val=0, bitlen=None):
"""Append an unsigned integer value at the end of the charpy instance
big endian representation is used (most significant byte leftmost,
least significant byte rightmost)
Args:
val (integer) : unsigned integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the value, value is majored to the
maximum uint value according to bitlen
if None, encoding is done in the minimum number of bits
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type or is negative
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif val < 0:
raise(CharpyErr('invalid argument value: {0}'.format(val)))
elif bitlen is None or bitlen < 0:
# Python 2.7 and 3 built-in
bitlen = val.bit_length()
elif bitlen == 0:
return
self._concat.append( (TYPE_UINT, val, bitlen) )
def to_uint(self, bitlen=None):
"""Provide the unsigned integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen
Args:
bitlen (integer) : length in bits for the requested unsigned integer
if None, the whole charpy unsigned integer value is returned
Returns:
uint (integer) : an unsigned integer value
or None if cursor is at the end of charpy or bitlen is null
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
off_byte, off_bit = self._cur >>3, self._cur % 8
if off_bit == 0:
# aligned access
return bytes_to_uint(self._buf[off_byte:1+off_byte+(bitlen>>3)], bitlen)
else:
# unaligned access
# convert from a fully-aligned byte buffer with an extended length
# bytes_to_uint takes care of converting only `bitlen+off_bit' bits
# and finally zero the extra left-most bits
return bytes_to_uint(self._buf[off_byte:2+off_byte+(bitlen>>3)],
bitlen+off_bit) & ((1<<bitlen)-1)
def get_uint(self, bitlen=None):
"""Consume the unsigned integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length in bits for the requested unsigned integer
if None, the whole charpy unsigned integer value is returned
Returns:
uint (integer) : an unsigned integer value
or None if cursor is at the end or bitlen is null
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
off_byte, off_bit = self._cur >>3, self._cur % 8
self._cur += bitlen
if off_bit == 0:
# aligned access
return bytes_to_uint(self._buf[off_byte:1+off_byte+(bitlen>>3)], bitlen)
else:
# unaligned access
# convert from a fully-aligned byte buffer with an extended length
# bytes_to_uint takes care of converting only `bitlen+off_bit' bits
# and finally zero the extra left-most bits
return bytes_to_uint(self._buf[off_byte:2+off_byte+(bitlen>>3)],
bitlen+off_bit) & ((1<<bitlen)-1)
def set_int(self, val=0, bitlen=None):
"""Reinitialize the charpy instance and its cursor by setting an
arbitrary signed integer value into it
big endian representation is used (most significant byte leftmost,
least significant byte rightmost) and 2's complement representation
Args:
val (integer) : signed integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the absolute value, value is minored or
majored to the maximum absolute value according to bitlen
if None, encoding is done in the minimum number of bits (can be
+1 to the real minimum of bits)
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif bitlen is None or bitlen < 0:
# Python built-in int.bit_length() does not take the sign into
# account, and we add another bit of dynamic to avoid any issue
bitlen = 1 + val.bit_length()
elif bitlen == 0:
self._buf = b''
self._len_bit = 0
self._cur = 0
self._concat = []
return
#
if val < 0:
valmax = 1 << (bitlen-1)
if val >= -valmax:
# negative integer, sign bit + 2's complement
self._buf = uint_to_bytes((valmax<<1) + val, bitlen)
else:
# minoring to the minimum possible negative value
if bitlen % 8:
self._buf = b'\x80' + (bitlen>>3) * b'\0'
else:
self._buf = b'\x80' + ((bitlen>>3)-1) * b'\0'
else:
self._buf = uint_to_bytes(val, bitlen)
self._len_bit = bitlen
self._cur = 0
self._concat = []
def append_int(self, val=0, bitlen=None):
"""Append a signed integer value at the end of the charpy instance
big endian representation is used (most significant byte leftmost,
least significant byte rightmost) and 2's complement representation
Args:
val (integer) : signed integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the absolute value, value is minored or
majored to the maximum absolute value according to bitlen
if None, encoding is done in the minimum number of bits (can be
+1 to the real minimum of bits)
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif bitlen is None or bitlen < 0:
bitlen = 1 + val.bit_length()
elif bitlen == 0:
return
self._concat.append( (TYPE_INT, val, bitlen) )
def to_int(self, bitlen=None):
"""Provide the signed integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen
Args:
bitlen (integer) : length in bits for the requested signed integer
if None, the whole charpy signed integer value is returned
Returns:
uint (integer) : a signed integer value
or None if cursor is at the end of charpy or bitlen is smaller
than 2
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
off_byte, off_bit = self._cur >>3, self._cur % 8
if off_bit == 0:
# aligned access
val = bytes_to_uint(self._buf[off_byte:1+off_byte+(bitlen>>3)], bitlen)
else:
# unaligned access
# convert from a fully-aligned byte buffer with an extended length
# bytes_to_uint takes care of converting only `bitlen+off_bit' bits
# and finally zero the extra left-most bits
val = bytes_to_uint(self._buf[off_byte:2+off_byte+(bitlen>>3)],
bitlen+off_bit) & ((1<<bitlen)-1)
mask = 1<<(bitlen-1)
if val & mask:
# negative integer
return (val&(mask-1)) - mask
else:
return val
def get_int(self, bitlen=None):
"""Consume the signed integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length in bits for the requested signed integer
if None, the whole charpy signed integer value is returned
Returns:
uint (integer) : a signed integer value
or None if cursor is at the end of charpy or bitlen is smaller
than 2
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
off_byte, off_bit = self._cur >>3, self._cur % 8
self._cur += bitlen
if off_bit == 0:
# aligned access
val = bytes_to_uint(self._buf[off_byte:1+off_byte+(bitlen>>3)], bitlen)
else:
# unaligned access
# convert from a fully-aligned byte buffer with an extended length
# bytes_to_uint takes care of converting only `bitlen+off_bit' bits
# and finally zero the extra left-most bits
val = bytes_to_uint(self._buf[off_byte:2+off_byte+(bitlen>>3)],
bitlen+off_bit) & ((1<<bitlen)-1)
mask = 1<<(bitlen-1)
if val & mask:
# negative integer
return (val&(mask-1)) - mask
else:
return val
def set_uint_le(self, val=0, bitlen=None):
"""Reinitialize the charpy instance and its cursor by setting an
arbitrary unsigned integer value into it
little endian representation is used (least significant byte leftmost,
most significant byte rightmost)
Args:
val (integer) : unsigned integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the value, value is majored to the
maximum uint value according to bitlen
if None, encoding is done in the minimum number of bits
if not byte-aligned, `bitlen' is extended
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type or is negative
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif val < 0:
raise(CharpyErr('invalid argument value: {0}'.format(val)))
elif bitlen is None or bitlen < 0:
# Python 2.7 and 3 built-in
bitlen = val.bit_length()
if bitlen == 0:
self._buf = b''
elif bitlen % 8:
bitlen += 8 - (bitlen%8)
self._buf = uint_le_to_bytes(val, bitlen)
else:
self._buf = uint_le_to_bytes(val, bitlen)
self._len_bit = bitlen
self._cur = 0
self._concat = []
def append_uint_le(self, val=0, bitlen=None):
"""Append an unsigned integer value at the end of the charpy instance
little endian representation is used (least significant byte leftmost,
most significant byte rightmost)
Args:
val (integer) : unsigned integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the value, value is majored to the
maximum uint value according to bitlen
if None, encoding is done in the minimum number of bits
if not byte-aligned, `bitlen' is extended
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type or is negative
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif val < 0:
raise(CharpyErr('invalid argument value: {0}'.format(val)))
elif bitlen is None or bitlen < 0:
# Python 2.7 and 3 built-in
bitlen = val.bit_length()
if bitlen % 8:
bitlen += 8 - (bitlen%8)
elif bitlen == 0:
return
elif bitlen % 8:
bitlen += 8 - (bitlen%8)
self._concat.append( (TYPE_UINT_LE, val, bitlen) )
def to_uint_le(self, bitlen=None):
"""Provide the unsigned integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen, using
the little endian format
Args:
bitlen (integer) : length in bits for the requested unsigned integer
if None, the whole charpy unsigned integer value is returned
if not byte-aligned, `bitlen' is lowered
Returns:
uint (integer) : an unsigned integer value
or None if cursor is at the end of charpy or bitlen is null
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
if bitlen % 8:
bitlen -= bitlen%8
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
elif bitlen % 8:
bitlen -= bitlen%8
off_byte, off_bit = self._cur >>3, self._cur % 8
if off_bit == 0:
# aligned access
return bytes_to_uint_le(self._buf[off_byte:off_byte+(bitlen>>3)],
bitlen)
else:
# unaligned access: shift left the buffer to restore alignment
buf = bytes_lshift(self._buf[off_byte:1+off_byte+(bitlen>>3)],
off_bit)
return bytes_to_uint_le(buf, bitlen)
def get_uint_le(self, bitlen=None):
"""Consume the unsigned integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen, using
the little endian format
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length in bits for the requested unsigned integer
if None, the whole charpy unsigned integer value is returned
if not byte-aligned, `bitlen' is lowered
Returns:
uint (integer) : an unsigned integer value
or None if cursor is at the end or bitlen is null
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
if bitlen % 8:
bitlen -= bitlen%8
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
elif bitlen % 8:
bitlen -= bitlen%8
off_byte, off_bit = self._cur >>3, self._cur % 8
self._cur += bitlen
if off_bit == 0:
# aligned access
return bytes_to_uint_le(self._buf[off_byte:off_byte+(bitlen>>3)],
bitlen)
else:
# unaligned access: shift left the buffer to restore alignment
buf = bytes_lshift(self._buf[off_byte:1+off_byte+(bitlen>>3)],
off_bit)
return bytes_to_uint_le(buf, bitlen)
def set_int_le(self, val=0, bitlen=None):
"""Reinitialize the charpy instance and its cursor by setting an
arbitrary signed integer value into it
little endian representation is used (least significant byte leftmost,
most significant byte rightmost) and 2's complement representation
Args:
val (integer) : signed integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the absolute value, value is minored or
majored to the maximum absolute value according to bitlen
if None, encoding is done in the minimum number of bits (can be
+1 to the real minimum of bits)
if not byte-aligned, `bitlen' is extended
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif bitlen is None or bitlen < 0:
# Python built-in int.bit_length() does not take the sign into
# account, and we add another bit of dynamic to avoid any issue
bitlen = 1 + val.bit_length()
if bitlen % 8:
bitlen += 8 - (bitlen%8)
elif bitlen == 0:
self._buf = b''
self._len_bit = 0
self._cur = 0
self._concat = []
return
elif bitlen % 8:
bitlen += 8 - (bitlen%8)
#
if val < 0:
valmax = 1 << (bitlen-1)
if val >= -valmax:
# negative integer, sign bit + 2's complement
self._buf = uint_le_to_bytes((valmax<<1) + val, bitlen)
else:
# minoring to the minimum possible negative value
self._buf = b'\x80' + ((bitlen>>3)-1) * b'\0'
else:
self._buf = uint_le_to_bytes(val, bitlen)
self._len_bit = bitlen
self._cur = 0
self._concat = []
def append_int_le(self, val=0, bitlen=None):
"""Append a signed integer value at the end of the charpy instance
little endian representation is used (least significant byte leftmost,
most significant byte rightmost) and 2's complement representation
Args:
val (integer) : signed integer
bitlen (integer) : number of bits to be used to store the value
if less than required by the absolute value, value is minored or
majored to the maximum absolute value according to bitlen
if None, encoding is done in the minimum number of bits (can be
+1 to the real minimum of bits)
if not byte-aligned, `bitlen' is extended
Returns:
None
Raises:
CharpyErr : if `val' has not the correct type
"""
if not isinstance(val, integer_types):
raise(CharpyErr('invalid argument type: {0}'\
.format(type(val).__name__)))
elif bitlen is None or bitlen < 0:
bitlen = 1 + val.bit_length()
if bitlen % 8:
bitlen += 8 - (bitlen%8)
elif bitlen == 0:
return
elif bitlen % 8:
bitlen += 8 - (bitlen%8)
self._concat.append( (TYPE_INT_LE, val, bitlen) )
def to_int_le(self, bitlen=None):
"""Provide the signed integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen,
using the little endian format
Args:
bitlen (integer) : length in bits for the requested signed integer
if None, the whole charpy signed integer value is returned
Returns:
uint (integer) : a signed integer value
or None if cursor is at the end of charpy or bitlen is smaller
than 2
if not byte-aligned, `bitlen' is lowered
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
if bitlen % 8:
bitlen -= bitlen% 8
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
elif bitlen % 8:
bitlen -= bitlen%8
off_byte, off_bit = self._cur >>3, self._cur % 8
if off_bit == 0:
# aligned access
val = bytes_to_uint_le(self._buf[off_byte:off_byte+(bitlen>>3)],
bitlen)
else:
# unaligned access: shift left the buffer to restore alignment
buf = bytes_lshift(self._buf[off_byte:1+off_byte+(bitlen>>3)],
off_bit)
val = bytes_to_uint_le(buf, bitlen)
mask = 1<<(bitlen-1)
if val & mask:
# negative integer
return (val&(mask-1)) - mask
else:
return val
def get_int_le(self, bitlen=None):
"""Consume the signed integer value of the charpy instance, starting
at the current cursor position and ending after the given bitlen,
using the little endian format
the charpy instance's cursor is incremented according to bitlen
Args:
bitlen (integer) : length in bits for the requested signed integer
if None, the whole charpy signed integer value is returned
Returns:
uint (integer) : a signed integer value
or None if cursor is at the end of charpy or bitlen is smaller
than 2
if not byte-aligned, `bitlen' is lowered
Raises:
CharpyErr : if `bitlen' is negative or overflow the maximum bitlen
"""
if self._concat: self._pack()
if bitlen is None:
# get the whole charpy buffer
bitlen = self._len_bit - self._cur
if bitlen % 8:
bitlen -= bitlen% 8
elif bitlen < 0:
raise(CharpyErr('negative bitlen: {0}'.format(bitlen)))
elif self._cur + bitlen > self._len_bit:
raise(CharpyErr('bitlen overflow: {0}, max {1}'\
.format(bitlen, self._len_bit-self._cur)))
elif bitlen == 0:
return None
elif bitlen % 8:
bitlen -= bitlen%8
off_byte, off_bit = self._cur >>3, self._cur % 8
self._cur += bitlen
if off_bit == 0:
# aligned access
val = bytes_to_uint_le(self._buf[off_byte:off_byte+(bitlen>>3)],
bitlen)
else:
# unaligned access: shift left the buffer to restore alignment
buf = bytes_lshift(self._buf[off_byte:1+off_byte+(bitlen>>3)],
off_bit)
val = bytes_to_uint_le(buf, bitlen)
mask = 1<<(bitlen-1)
if val & mask:
# negative integer
return (val&(mask-1)) - mask
else:
return val
#--------------------------------------------------------------------------#
# Python built-ins override
#--------------------------------------------------------------------------#
__len__ = len_bit
__repr__ = repr
__index__ = to_uint
if python_version < 3:
__str__ = to_bytes
else:
__bytes__ = to_bytes
def __bool__(self):
if self.len_bit() == 0:
return False
else:
return True
def __getitem__(self, key):
if isinstance(key, integer_types):
if key < 0:
raise(IndexError('Charpy does not support negative index'))
elif key >= self.len_bit():
raise(IndexError('Charpy index out of range'))
cur = self._cur
self._cur += key
ret = self.to_bitlist(1)[0]
self._cur = cur
return ret
elif isinstance(key, slice):
if key.start:
if key.start < 0:
raise(IndexError('Charpy does not support negative index'))
elif key.start >= self.len_bit():
return []
start = key.start
else:
start = 0
if key.stop:
if key.stop < start:
raise(IndexError('Charpy does not support retro indexing'))
elif key.stop > self.len_bit():
stop = self.len_bit()
else:
stop = key.stop
else:
stop = self.len_bit()
cur = self._cur
self._cur += start
if key.step:
ret = self.to_bitlist(stop-start)[::key.step]
else:
ret = self.to_bitlist(stop-start)
self._cur = cur
return ret
else:
raise(TypeError('Charpy indices must be integers, not {0}'\
.format(type(key).__name__)))
def __iter__(self):
return iter(self.to_bitlist())
# those comparisons are broken, but this is still funny !
def __lt__(self, other):
if not isinstance(other, Charpy):
raise(TypeError('unorderable types: Charpy() < {0}()'\
.format(type(other).__name__)))
suint = self.to_uint()
ouint = other.to_uint()
if suint == ouint:
# if both uint values are equal, compare the number of bits
return self.len_bit() < other.len_bit()
else:
return suint < ouint
def __le__(self, other):
if not isinstance(other, Charpy):
raise(TypeError('unorderable types: Charpy() <= {0}()'\
.format(type(other).__name__)))
if self.__eq__(other):
return True
else:
return self.__lt__(other)
def __eq__(self, other):
if not isinstance(other, Charpy):
raise(TypeError('unorderable types: Charpy() == {0}()'\
.format(type(other).__name__)))
return self.to_bytes() == other.to_bytes()
def __ne__(self, other):
if not isinstance(other, Charpy):
raise(TypeError('unorderable types: Charpy() != {0}()'\
.format(type(other).__name__)))
return self.to_bytes() != other.to_bytes()
def __gt__(self, other):
if not isinstance(other, Charpy):
raise(TypeError('unorderable types: Charpy() > {0}()'\
.format(type(other).__name__)))
suint = self.to_uint()
ouint = other.to_uint()
if suint == ouint:
# if both uint values are equal, compare the number of bits
return self.len_bit() > other.len_bit()
else:
return suint > ouint
def __ge__(self, other):
if not isinstance(other, Charpy):
raise(TypeError('unorderable types: Charpy() >= {0}()'\
.format(type(other).__name__)))
if self.__eq__(other):
return True
else:
return self.__gt__(other)