from datetime import datetime
from typing import Dict, List, Tuple
from bitarray import bitarray
from bitarray.util import ba2int
[docs]class Reader:
"""Represents a bit reader that can extract bits sequentially from
a bytes consent and return a representation in different formats.
:param consent: The consent to process in bytes.
"""
def __init__(self, consent: bytes):
self._pointer = 0
self._consent = bitarray(endian="big")
self._consent.frombytes(consent)
[docs] def read_bits(self, n: int) -> bitarray:
"""Reads the number of bits requested from the pointer position
and automatically advances the pointer of the reader
for the subsequent read_bits calls.
:param n: Number of bits to retrieve.
"""
self._pointer += n
return self._consent[self._pointer - n : self._pointer]
[docs] def read_bool(self) -> bool:
"""Reads a bit and returns the value as boolean."""
return self.read_bits(1).all()
[docs] def read_int(self, n: int) -> int:
"""Reads certain number of bits from the pointer position
and returns the value as integer.
:param n: Number of bits to retrieve and transform into int.
"""
bits = self.read_bits(n)
return ba2int(bits) if bits else 0
[docs] def read_time(self) -> datetime:
"""Reads 36 bits (the length TCF uses for timestamps) and transforms
the value into a utc datetime object with seconds granularity.
"""
seconds = int(self.read_int(36) / 10)
return datetime.utcfromtimestamp(seconds)
[docs] def read_character(self) -> bytes:
"""Reads 6 bits and returns the value as a character starting with A"""
return bytes([b"A"[0] + self.read_int(6)])
[docs] def read_string(self, n: int) -> bytes:
"""Reads certain number of bits from the pointer position
and returns the value as string character by character.
:param n: Number of bits to retrieve and transform into string.
"""
return b"".join([self.read_character() for _ in range(n)])
[docs] def read_bitfield(self, n: int) -> Dict[int, bool]:
"""Reads certain number of bits from the pointer position
and returns the value as a dictionary. The key is the bit
position (starting by 1) and the value is True if the bit is
1 and False if the bit is 0.
:param n: Number of bits to retrieve and transform into the
dictionary. The dictionary will have as many keys as n.
"""
return {i + 1: self.read_bool() for i in range(n)}
[docs] def read_range(self, n: int) -> List[Tuple[int, int]]:
"""Reads a complex "ranged" type from the reader given
the number of ranges we are expecting to find.
For each range:
- The first bit indicates if it's a range or just a value.
- If it's a range, the next 16 bits are the start int and the
next 16 bits are the end int.
- If it's not a range, the next 16 bits are the start and the
end is the same as start.
:param n: Number of ranges we are going to process.
"""
ranges = []
for _ in range(n):
is_range = self.read_bool()
start = self.read_int(16)
end = start if not is_range else self.read_int(16)
ranges.append((start, end))
return ranges