1
0
mirror of synced 2024-11-12 01:00:46 +01:00
bemaniutils/bemani/protocol/stream.py

203 lines
6.6 KiB
Python

import struct
from typing import List, Optional
class StreamError(Exception):
"""
An exception thrown when something goes wrong with the stream.
"""
class InputStream:
"""
A class that treats a binary blob as a stream of bytes to be emitted.
Makes stream-like algorithms much easier to implement. All accessor
functions that read data will advance the current position. It is not
rewindable.
"""
def __init__(self, data: bytes) -> None:
"""
Initialize the object. Given a data blob, will set this as the stream
and set the location to the beginning of the data blob.
Parameters:
data - A binary blob to read from.
"""
self.data = data
self.pos = 0
self.left = len(self.data)
def read_blob(self, blob_size: int) -> Optional[bytes]:
"""
Given a blob size, read the next blob_size bytes as a binary blob.
Parameters:
blob_size - An integer representing the number of bytes to read.
Returns:
a binary string representing blob_size bytes from the current location, or None
if there wasn't enough bytes to satisfy this request.
"""
if blob_size <= 0:
return None
if blob_size <= self.left:
bytedata = self.data[self.pos : (self.pos + blob_size)]
self.pos += blob_size
self.left -= blob_size
return bytedata
return None
def read_byte(self) -> Optional[bytes]:
"""
Grab the next byte at the current position. If no byte is available,
return None.
Returns:
a raw byte
"""
return self.read_blob(1)
def read_int(self, size: int = 1, is_unsigned: bool = True) -> Optional[int]:
"""
Grab the next integer of size 'size' at the current position. If not enough
bytes are available to decode this integer, return None.
Parameters:
size - Integer representing the integer size to decode. Valid values are
1, 2 and 4 for char, short and int respectively.
is_unsigned - An optional boolean specifying whether the integer read should be
unsigned. Defaults to True.
Returns:
a python integer representing the big-endian decoding of the current
position
"""
if size == 1:
data = self.read_blob(1)
if data is None:
return None
if is_unsigned:
# Fastpath, just use python's own decoder
return data[0]
else:
return struct.unpack(">b", data)[0]
elif size == 2:
data = self.read_blob(2)
if data is None:
return None
if is_unsigned:
return struct.unpack(">H", data)[0]
else:
return struct.unpack(">h", data)[0]
elif size == 4:
data = self.read_blob(4)
if data is None:
return None
if is_unsigned:
return struct.unpack(">I", data)[0]
else:
return struct.unpack(">i", data)[0]
else:
raise StreamError(f"Unsupported size {size}")
class OutputStream:
"""
A class that treats a binary blob as a stream of bytes to be constructed.
Makes stream-like algorithms much easier to implement. All accessor
functions that write data will advance the current position. It is not
rewindable. When finished writing, access the finished blob by copying from
data.
"""
def __init__(self) -> None:
"""
Initialize the object.
"""
self.__data: List[bytes] = []
self.__data_len = 0
self.__formatted_data: Optional[bytes] = None
@property
def data(self) -> bytes:
if self.__formatted_data is None:
self.__formatted_data = b"".join(self.__data)
return self.__formatted_data
def write_blob(self, blob: bytes) -> int:
"""
Write a binary blob of data to the stream
Parameters:
blob - An blob of data to write.
Returns:
the number of bytes written
"""
self.__data.append(blob)
self.__data_len += len(blob)
return len(blob)
def write_byte(self, byte: bytes) -> None:
"""
Write a raw byte to the end of the output stream.
Parameters:
A byte that should be appended to the current stream.
"""
self.__data.append(byte)
self.__data_len = self.__data_len + 1
self.__formatted_data = None
def write_int(self, integer: int, size: int = 1, is_unsigned: bool = True) -> None:
"""
Write an integer to the end of the output stream.
Parameters:
integer - The integer that should be written to the stream.
size - The byte size of the integer. Supports 1, 2 and 4 byte
integer types.
is_unsigned - Whether the integer should be written unsigned or
signed. Defaults to True.
"""
if size == 1:
if is_unsigned:
self.__data.append(struct.pack(">B", integer))
else:
self.__data.append(struct.pack(">b", integer))
self.__data_len = self.__data_len + 1
elif size == 2:
if is_unsigned:
self.__data.append(struct.pack(">H", integer))
else:
self.__data.append(struct.pack(">h", integer))
self.__data_len = self.__data_len + 2
elif size == 4:
if is_unsigned:
self.__data.append(struct.pack(">I", integer))
else:
self.__data.append(struct.pack(">i", integer))
self.__data_len = self.__data_len + 4
else:
raise StreamError(f"Unsupported size {size}")
self.__formatted_data = None
def write_pad(self, pad_to: int) -> None:
"""
Pad the current stream to a byte boundary specified by pad_to.
Parameters:
pad_to - An integer specifying the byte alignment that should be present
after padding is complete. Supports 1, 2, 4, 8, 16 or any other power of
two padding. After calling this, the next write_byte or write_int will
be placed on a boundary compatible with the pad_to parameter.
"""
while (self.__data_len & (pad_to - 1)) != 0:
self.__data.append(b"\0")
self.__data_len = self.__data_len + 1
self.__formatted_data = None