# -*- coding: utf-8 -*-
"""numerical field class"""
import enum
import math
from typing import TYPE_CHECKING, Generic, TypeVar, Union, cast
import aenum
from pcapkit.corekit.fields.field import Field, NoValue
from pcapkit.utilities.exceptions import IntError
__all__ = [
'NumberField',
'Int32Field', 'UInt32Field',
'Int16Field', 'UInt16Field',
'Int64Field', 'UInt64Field',
'Int8Field', 'UInt8Field',
'EnumField',
]
if TYPE_CHECKING:
from enum import IntEnum as StdlibEnum
from typing import Any, Callable, Optional, Type
from aenum import IntEnum as AenumEnum
from typing_extensions import Literal, Self
from pcapkit.corekit.fields.field import NoValueType
_T = TypeVar('_T', bound='int')
[docs]
class NumberField(Field[int], Generic[_T]):
"""Numerical value for protocol fields.
Args:
length: Field size (in bytes); if a callable is given, it should return
an integer value and accept the current packet as its only argument.
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = None # type: Optional[int]
__template__ = None # type: Optional[str]
__signed__ = None # type: Optional[bool]
@property
def bit_length(self) -> 'int':
"""Field bit length."""
return self._bit_length
def __init__(self, length: 'Optional[int | Callable[[dict[str, Any]], int]]' = None,
default: 'int | NoValueType' = NoValue, signed: 'bool' = False,
byteorder: 'Literal["little", "big"]' = 'big',
bit_length: 'Optional[int]' = None,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
if length is None:
if self.__length__ is None:
raise IntError(f'Field has no length.')
length = self.__length__
super().__init__(length, default, callback)
if bit_length is not None:
self._bit_length = bit_length
self._bit_mask = (1 << bit_length) - 1
else:
self._bit_length, self._bit_mask = -1, -1
self._signed = signed if self.__signed__ is None else self.__signed__
self._byteorder = byteorder
self._need_process = False
endian = '>' if byteorder == 'big' else '<'
if self.__template__ is not None:
struct_fmt = self.__template__
else:
struct_fmt = self.build_template(self._length, signed)
self._template = f'{endian}{struct_fmt}'
[docs]
def __call__(self, packet: 'dict[str, Any]') -> 'Self':
"""Update field attributes.
Args:
packet: Packet data.
Returns:
New instance of :class:`NumberField`.
This method will return a new instance of :class:`NumberField` instead of
updating the current instance.
"""
new_self = super().__call__(packet)
if new_self._bit_length < 0:
new_self._bit_length = new_self._length * 8
new_self._bit_mask = (1 << new_self._bit_length) - 1
endian = '>' if new_self._byteorder == 'big' else '<'
struct_fmt = new_self.build_template(new_self._length, new_self._signed)
new_self._template = f'{endian}{struct_fmt}'
return new_self
[docs]
def build_template(self, length: 'int', signed: 'bool') -> 'str':
"""Build template for field.
Arguments:
length: Field size (in bytes)
signed: Whether the field is signed
Returns:
Template for field.
"""
if length == 8: # unpack to 8-byte integer (long long)
struct_fmt = 'q' if signed else 'Q'
elif length == 4: # unpack to 4-byte integer (int / long)
struct_fmt = 'i' if signed else 'I'
elif length == 2: # unpack to 2-byte integer (short)
struct_fmt = 'h' if signed else 'H'
elif length == 1: # unpack to 1-byte integer (char)
struct_fmt = 'b' if signed else 'B'
else: # do not unpack
struct_fmt = f'{length}s'
self._need_process = True
return struct_fmt
[docs]
def pre_process(self, value: 'int', packet: 'dict[str, Any]') -> 'int | bytes': # pylint: disable=unused-argument
"""Process field value before construction (packing).
Arguments:
value: Field value.
packet: Packet data.
Returns:
Processed field value.
"""
value = value & self._bit_mask
if not self._need_process:
return value
if self._length < 0:
self._length = math.ceil(value.bit_length() // 8)
endian = '>' if self._byteorder == 'big' else '<'
struct_fmt = self.build_template(self._length, self._signed)
self._template = f'{endian}{struct_fmt}'
return value.to_bytes(
self._length, self._byteorder, signed=self._signed
)
[docs]
def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'int': # pylint: disable=unused-argument
"""Process field value after parsing (unpacked).
Args:
value: Field value.
packet: Packet data.
Returns:
Processed field value.
"""
if not self._need_process:
return cast('int', value) & self._bit_mask
return int.from_bytes(
cast('bytes', value), self._byteorder, signed=self._signed
) & self._bit_mask
[docs]
class Int32Field(NumberField):
"""Integer value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 4
__template__ = 'i'
__signed__ = True
[docs]
class UInt32Field(NumberField):
"""Unsigned integer value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 4
__template__ = 'I'
__signed__ = False
[docs]
class Int16Field(NumberField):
"""Short integer value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 2
__template__ = 'h'
__signed__ = True
[docs]
class UInt16Field(NumberField):
"""Unsigned short integer value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 2
__template__ = 'H'
__signed__ = False
[docs]
class Int64Field(NumberField):
"""Long integer value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 8
__template__ = 'q'
__signed__ = True
[docs]
class UInt64Field(NumberField):
"""Unsigned long integer value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 8
__template__ = 'Q'
__signed__ = False
[docs]
class Int8Field(NumberField):
"""Byte value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 1
__template__ = 'b'
__signed__ = True
[docs]
class UInt8Field(NumberField):
"""Unsigned byte value for protocol fields.
Args:
length: Field size (in bytes).
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
__length__ = 1
__template__ = 'B'
__signed__ = False
[docs]
class EnumField(NumberField[Union[enum.IntEnum, aenum.IntEnum]]):
"""Enumerated value for protocol fields.
Args:
length: Field size (in bytes); if a callable is given, it should return
an integer value and accept the current packet as its only argument.
default: Field default value, if any.
signed: Whether the field is signed.
byteorder: Field byte order.
bit_length: Field bit length.
namespace: Field namespace (a :class:`enum.IntEnum` class).
callback: Callback function to be called upon
:meth:`self.__call__ <pcapkit.corekit.fields.field.FieldBase.__call__>`.
"""
def __init__(self, length: 'int | Callable[[dict[str, Any]], int]',
default: 'StdlibEnum | AenumEnum | NoValueType' = NoValue, signed: 'bool' = False,
byteorder: 'Literal["little", "big"]' = 'big',
bit_length: 'Optional[int]' = None,
namespace: 'Optional[Type[StdlibEnum] | Type[AenumEnum]]' = None,
callback: 'Callable[[Self, dict[str, Any]], None]' = lambda *_: None) -> 'None':
super().__init__(length, default, signed, byteorder, bit_length, callback)
self._namespace = namespace
[docs]
def post_process(self, value: 'int | bytes', packet: 'dict[str, Any]') -> 'StdlibEnum | AenumEnum':
"""Process field value after parsing (unpacked).
Args:
value: Field value.
packet: Packet data.
Returns:
Processed field value.
"""
value = super().post_process(value, packet)
if self._namespace is None:
unknown = enum.IntEnum('<unknown>', {
'<unassigned>': value,
}, module='pcapkit.const', qualname='pcapkit.const.<unknown>')
return getattr(unknown, '<unassigned>')
return self._namespace(value)