diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index d36b4008bc7..36bf9acccd6 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -193,6 +193,23 @@ jobs: with: token: ${{ secrets.CODECOV_TOKEN }} + + cbor2-interop: + name: cbor2 interoperability (Python 3.12) + runs-on: ubuntu-latest + needs: [commit, spdx] + steps: + - name: Checkout Scapy + uses: actions/checkout@v6 + - name: Setup Python + uses: actions/setup-python@v6 + with: + python-version: "3.12" + - name: Install tox + run: pip install tox + - name: Run cbor2 differential tests + run: tox -e cbor2 + cryptography: name: pyca/cryptography test runs-on: ubuntu-latest diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index dcec5d8ed5d..0089a3401b8 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -14,6 +14,7 @@ CBOR_BadTag_Decoding_Error, CBOR_Codecs, CBOR_MajorTypes, + CBOR_SimpleValue, CBOR_Object, CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER, @@ -21,6 +22,7 @@ CBOR_TEXT_STRING, CBOR_ARRAY, CBOR_MAP, + CBORMapData, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, CBOR_FALSE, @@ -47,6 +49,8 @@ from scapy.cbor.cborfields import ( CBORF_element, CBORF_field, + CBORF_ANY, + CBOR_ABSENT, CBORF_UNSIGNED_INTEGER, CBORF_NEGATIVE_INTEGER, CBORF_INTEGER, @@ -56,12 +60,17 @@ CBORF_NULL, CBORF_UNDEFINED, CBORF_FLOAT, + CBORF_ITEMS, + CBORF_REMAINDER_OF, CBORF_ARRAY, CBORF_ARRAY_OF, + CBORF_ARRAY_INDEFINITE, CBORF_MAP, CBORF_SEMANTIC_TAG, CBORF_optional, + CBORF_CONDITIONAL, CBORF_PACKET, + CBORF_BYTE_STRING_PACKET, ) __all__ = [ @@ -73,6 +82,7 @@ # Codecs "CBOR_Codecs", "CBOR_MajorTypes", + "CBOR_SimpleValue", # Objects "CBOR_Object", "CBOR_UNSIGNED_INTEGER", @@ -81,6 +91,7 @@ "CBOR_TEXT_STRING", "CBOR_ARRAY", "CBOR_MAP", + "CBORMapData", "CBOR_SEMANTIC_TAG", "CBOR_SIMPLE_VALUE", "CBOR_FALSE", @@ -104,6 +115,8 @@ # Field base classes "CBORF_element", "CBORF_field", + "CBORF_ANY", + "CBOR_ABSENT", # Scalar fields "CBORF_UNSIGNED_INTEGER", "CBORF_NEGATIVE_INTEGER", @@ -115,11 +128,16 @@ "CBORF_UNDEFINED", "CBORF_FLOAT", # Structured fields + "CBORF_ITEMS", + "CBORF_REMAINDER_OF", "CBORF_ARRAY", "CBORF_ARRAY_OF", + "CBORF_ARRAY_INDEFINITE", "CBORF_MAP", "CBORF_SEMANTIC_TAG", # Complex fields "CBORF_optional", + "CBORF_CONDITIONAL", "CBORF_PACKET", + "CBORF_BYTE_STRING_PACKET", ] diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 1dcff4943f1..65045c83564 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -7,13 +7,18 @@ Following the ASN.1 paradigm """ +import copy +import math import random +import struct from typing import ( Any, Dict, + FrozenSet, Generic, List, Optional, + Set, Tuple, Type, TypeVar, @@ -285,6 +290,81 @@ class CBOR_MajorTypes(metaclass=CBOR_MajorTypes_metaclass): SIMPLE_AND_FLOAT = cast(CBORTag, 7) +class CBOR_AdditionalInfo(metaclass=Enum_metaclass): + """CBOR additional-info codes used with argument encoding (RFC 8949).""" + name = "CBOR_ADDITIONAL_INFO" + ONE_BYTE = 24 + TWO_BYTES = 25 + FOUR_BYTES = 26 + EIGHT_BYTES = 27 + RESERVED_28 = 28 + RESERVED_29 = 29 + RESERVED_30 = 30 + INDEFINITE = 31 + + +class CBOR_SimpleValue(metaclass=Enum_metaclass): + """Well-known CBOR simple values encoded in major type 7.""" + name = "CBOR_SIMPLE_VALUE" + FALSE = 20 + TRUE = 21 + NULL = 22 + UNDEFINED = 23 + + +class CBOR_FloatAI(metaclass=Enum_metaclass): + """Float additional-info codes under major type 7 (RFC 8949 §3.3).""" + name = "CBOR_FLOAT_AI" + HALF = 25 # IEEE binary16 + SINGLE = 26 # IEEE binary32 + DOUBLE = 27 # IEEE binary64 + + +class CBOR_KeyKind(metaclass=Enum_metaclass): + """Discriminator for hashable RFC 8949 map-key norms. + + Norms are tuples ``(kind, ...)`` (not dicts) so they remain hashable for + ``frozenset`` map-key identity. ``kind`` is always a ``CBOR_KeyKind``. + """ + name = "CBOR_KEY_KIND" + BOOL = 1 + NULL = 2 + UNDEF = 3 + INT = 4 + BSTR = 5 + TSTR = 6 + ARRAY = 7 + MAP = 8 + TAG = 9 + SIMPLE = 10 + OBJ = 11 + OTHER = 12 + NAN = 13 + FINITE = 14 + + +# Recursive hashable RFC 8949 map-key norm. +# First element is a ``CBOR_KeyKind`` ``EnumElement`` (typed ``Any`` because +# ``Enum_metaclass`` members are ``int`` in the class body). +_CBORKeyNorm = Union[ + Tuple[Any, None], + Tuple[Any, bool], + Tuple[Any, int], + Tuple[Any, bytes], + Tuple[Any, str], + Tuple[Any, float], + Tuple[Any, int, int], + Tuple[Any, Tuple["_CBORKeyNorm", ...]], + Tuple[Any, FrozenSet[Tuple["_CBORKeyNorm", "_CBORKeyNorm"]]], + Tuple[Any, int, "_CBORKeyNorm"], + Tuple[Any, str, "_CBORKeyNorm"], + Tuple[Any, str, str], +] + + +CBOR_UINT64_MAX = (1 << 64) - 1 + + class CBOR_Object_metaclass(type): def __new__(cls, name, # type: str @@ -296,11 +376,12 @@ def __new__(cls, 'Type[CBOR_Object[Any]]', super(CBOR_Object_metaclass, cls).__new__(cls, name, bases, dct) ) - try: - c.tag.register_cbor_object(c) - except Exception: - # Some objects may not have tags yet - log_runtime.warning("Failed to register CBOR object %r" % c) + if c.tag is not None: + try: + c.tag.register_cbor_object(c) + except Exception: + # Some objects may not have tags yet + log_runtime.exception("Failed to register CBOR object %r" % c) return c @@ -346,7 +427,22 @@ def show(self, lvl=0): def __eq__(self, other): # type: (Any) -> bool - return bool(self.val == other) + if isinstance(other, CBOR_Object): + return ( + type(self) is type(other) + and self.val == other.val + ) + return NotImplemented + + def __ne__(self, other): + # type: (Any) -> bool + equal = self.__eq__(other) + if equal is NotImplemented: + return NotImplemented + return not equal + + # No __hash__: defining __eq__ without __hash__ makes instances unhashable. + # Immutable scalar subclasses may add semantic hashing later if needed. ####################### @@ -368,6 +464,11 @@ class CBOR_BYTE_STRING(CBOR_Object[bytes]): """CBOR byte string (major type 2)""" tag = CBOR_MajorTypes.BYTE_STRING + def __repr__(self): + # type: () -> str + hexval = self.val.hex() if self.val else '' + return "<%s[h'%s']>" % (self.__class__.__name__, hexval) + class CBOR_TEXT_STRING(CBOR_Object[str]): """CBOR text string (major type 3)""" @@ -389,14 +490,195 @@ def strshow(self, lvl=0): return s -class CBOR_MAP(CBOR_Object[Dict[Any, Any]]): - """CBOR map (major type 5)""" +class CBORMapData(object): + """Ordered CBOR map pairs with typed dict-like access for scalar keys. + + Storage preserves ordered ``(key, value)`` pairs so ``enc()`` can emit a + faithful CBOR map. Lookup (``__getitem__`` / ``__contains__``) uses + RFC 8949 map-key equivalence via :func:`_cbor_key_equivalent`, so values + that compare equal under Python ``==`` but differ as CBOR items (``1`` vs + ``True``, distinct NaN payloads, etc.) stay distinct. + + Arbitrary CBOR maps cannot always be represented as Python ``dict`` + objects; :meth:`as_dict` raises when equivalence or Python key collision + would lose distinctions. + """ + + __slots__ = ("_pairs",) + + def __init__(self, pairs=None): + # type: (Optional[List[Tuple[Any, Any]]]) -> None + self._pairs = list(pairs or []) + + def cbor_pairs(self): + # type: () -> List[Tuple[Any, Any]] + return list(self._pairs) + + def as_dict(self): + # type: () -> Dict[Any, Any] + """Convert to a Python dict, raising if CBOR key distinctions would be lost.""" + out = {} # type: Dict[Any, Any] + used_norms = set() # type: Set[_CBORKeyNorm] + for key, value in self._pairs: + norm = _cbor_key_norm(key) + if norm in used_norms: + raise ValueError( + "CBOR map keys are equivalent under RFC 8949; " + "cannot convert to dict without losing distinctions" + ) + py_key = key.val if isinstance(key, CBOR_Object) else key + try: + hash(py_key) + except TypeError: + raise ValueError( + "CBOR map key cannot be represented as a Python dict key" + ) + # Also reject Python-dict collisions (True vs 1, etc.). + if py_key in out: + raise ValueError( + "Converting CBOR map to dict would collapse distinct keys" + ) + used_norms.add(norm) + out[py_key] = value + return out + + def copy(self): + # type: () -> CBORMapData + return copy.deepcopy(self) + + def __copy__(self): + # type: () -> CBORMapData + return self.copy() + + def __deepcopy__(self, memo): + # type: (Dict[int, Any]) -> CBORMapData + return CBORMapData(copy.deepcopy(self._pairs, memo)) + + def __len__(self): + # type: () -> int + return len(self._pairs) + + def __iter__(self): + # type: () -> Any + return iter(self.keys()) + + def keys(self): + # type: () -> List[Any] + out = [] # type: List[Any] + for key, _value in self._pairs: + out.append(key.val if isinstance(key, CBOR_Object) else key) + return out + + def values(self): + # type: () -> List[Any] + return [value for _key, value in self._pairs] + + def items(self): + # type: () -> List[Tuple[Any, Any]] + return [ + (key.val if isinstance(key, CBOR_Object) else key, value) + for key, value in self._pairs + ] + + def __contains__(self, key): + # type: (Any) -> bool + try: + self[key] + return True + except KeyError: + return False + + def __getitem__(self, key): + # type: (Any) -> Any + matches = [] # type: List[Any] + for map_key, value in self._pairs: + if _cbor_key_equivalent(map_key, key): + matches.append(value) + if not matches: + raise KeyError(key) + if len(matches) > 1: + raise KeyError("Ambiguous CBOR map key %r" % (key,)) + return matches[0] + + def get(self, key, default=None): + # type: (Any, Any) -> Any + try: + return self[key] + except KeyError: + return default + + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, dict): + # Do not use dict(self.items()): Python collapses True/1 (and + # similar) as equal keys, which is not the CBOR data model. + other_items = list(other.items()) + elif isinstance(other, CBORMapData): + # RFC 8949 maps are unordered; pair order is not identity. + other_items = other._pairs + else: + return NotImplemented + if len(self._pairs) != len(other_items): + return False + used = [False] * len(other_items) + for map_key, value in self._pairs: + matched = False + for idx, (other_key, other_value) in enumerate(other_items): + if used[idx]: + continue + if not _cbor_key_equivalent(map_key, other_key): + continue + if value != other_value: + return False + used[idx] = True + matched = True + break + if not matched: + return False + return True + + def __repr__(self): + # type: () -> str + return "CBORMapData(%r)" % (self.items(),) + + +def _cbor_map_pairs(mapping): + # type: (Any) -> List[Tuple[Any, Any]] + """Return ordered ``(key, value)`` pairs from a CBOR map representation. + + Accepts :class:`CBOR_MAP`, :class:`CBORMapData`, ``dict``, or a sequence + of pairs. Used by encode, display, and key-normalization paths. + """ + if isinstance(mapping, CBOR_MAP): + mapping = mapping.val + if isinstance(mapping, CBORMapData): + return mapping.cbor_pairs() + if isinstance(mapping, dict): + return list(mapping.items()) + return list(mapping) + + +class CBOR_MAP(CBOR_Object[Any]): + """CBOR map (major type 5). + + Always stores :class:`CBORMapData`. Constructors accept ``CBORMapData``, + ``dict``, or a sequence of ``(key, value)`` pairs. + """ tag = CBOR_MajorTypes.MAP + def __init__(self, val): + # type: (Any) -> None + if isinstance(val, CBORMapData): + super(CBOR_MAP, self).__init__(val) + elif isinstance(val, dict): + super(CBOR_MAP, self).__init__(CBORMapData(list(val.items()))) + else: + super(CBOR_MAP, self).__init__(CBORMapData(list(val))) + def strshow(self, lvl=0): # type: (int) -> str s = (" " * lvl) + ("# CBOR_MAP:") + "\n" - for k, v in self.val.items(): + for k, v in _cbor_map_pairs(self.val): s += (" " * (lvl + 1)) + "Key: " if hasattr(k, 'strshow'): s += k.strshow(0).strip() + "\n" @@ -448,18 +730,224 @@ def __init__(self): class CBOR_UNDEFINED(CBOR_Object[None]): - """CBOR undefined value""" + """CBOR undefined value (singleton).""" tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + _instance = None # type: Optional["CBOR_UNDEFINED"] + + def __new__(cls): + # type: () -> CBOR_UNDEFINED + if cls._instance is None: + cls._instance = CBOR_Object.__new__(cls) + return cls._instance def __init__(self): # type: () -> None - super(CBOR_UNDEFINED, self).__init__(None) + if not hasattr(self, "val"): + super(CBOR_UNDEFINED, self).__init__(None) + + def __bool__(self): + # type: () -> bool + return False + + def __copy__(self): + # type: () -> CBOR_UNDEFINED + return self + + def __deepcopy__(self, memo): + # type: (dict) -> CBOR_UNDEFINED + return self + + +class _CBORNoItem(object): + """Structural sentinel: sequence ended without consuming input.""" + + def __repr__(self): + # type: () -> str + return "CBOR_NO_ITEM" + + def __copy__(self): + # type: () -> _CBORNoItem + return self + + def __deepcopy__(self, memo): + # type: (dict) -> _CBORNoItem + return self + + +CBOR_NO_ITEM = _CBORNoItem() class CBOR_FLOAT(CBOR_Object[float]): """CBOR floating-point number (major type 7)""" tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + def __init__(self, val, encoded=None): + # type: (float, Optional[bytes]) -> None + CBOR_Object.__init__(self, val) + # Exact received float encoding when known; preferred width when None. + self._encoded = encoded + + def __setattr__(self, name, value): + # type: (str, Any) -> None + # After construction, assigning val invalidates the wire cache even + # when the new semantic value compares equal to the old one. + if name == "val" and hasattr(self, "_encoded"): + object.__setattr__(self, "_encoded", None) + super(CBOR_FLOAT, self).__setattr__(name, value) + + def enc(self, codec=None): + # type: (Any) -> bytes + if self._encoded is not None: + return self._encoded + return super(CBOR_FLOAT, self).enc(codec) + + +def _cbor_float_wire_parts(encoded): + # type: (bytes) -> Tuple[int, int] + """Return ``(ai, bits)`` for a definite CBOR float encoding.""" + wire = bytes(encoded) + if not wire: + raise ValueError("empty CBOR float encoding") + ai = wire[0] & 0x1f + if ai == CBOR_FloatAI.HALF: + if len(wire) < 3: + raise ValueError("truncated half float") + return ai, struct.unpack(">H", wire[1:3])[0] + if ai == CBOR_FloatAI.SINGLE: + if len(wire) < 5: + raise ValueError("truncated single float") + return ai, struct.unpack(">I", wire[1:5])[0] + if ai == CBOR_FloatAI.DOUBLE: + if len(wire) < 9: + raise ValueError("truncated double float") + return ai, struct.unpack(">Q", wire[1:9])[0] + raise ValueError("not a CBOR float encoding: ai=%d" % ai) + + +def _cbor_float_key_identity(value, encoded=None): + # type: (float, Optional[bytes]) -> _CBORKeyNorm + """Return RFC 8949 floating-point map-key identity for *value*. + + Finite ``+0.0`` / ``-0.0`` collapse. NaNs compare by sign and + significand after zero-extension to a 52-bit binary64 significand. + When *encoded* is a CBOR float item, prefer that bit pattern so payload + and sign survive Python's NaN canonicalization. + """ + if encoded is not None: + from scapy.cbor.cborcodec import ( + _cbor_float_from_bits, + _cbor_nan_components, + ) + ai, bits = _cbor_float_wire_parts(encoded) + comps = _cbor_nan_components(ai, bits) + if comps is not None: + sign, significand52 = comps + return (CBOR_KeyKind.NAN, sign, significand52) + return _cbor_float_key_identity(_cbor_float_from_bits(ai, bits)) + + fval = float(value) + if math.isnan(fval): + bits = struct.unpack(">Q", struct.pack(">d", fval))[0] + sign = (bits >> 63) & 0x1 + significand = bits & ((1 << 52) - 1) + return (CBOR_KeyKind.NAN, sign, significand) + if fval == 0.0: + return (CBOR_KeyKind.FINITE, 0.0) + return (CBOR_KeyKind.FINITE, fval) + + +def _cbor_key_norm(value): + # type: (Any) -> _CBORKeyNorm + """Return a hashable RFC 8949 map-key equivalence form for *value*. + + Integers and floats remain distinct groups. Floating ``+0.0`` and + ``-0.0`` collapse. NaNs are equivalent only when sign and normalized + significand match across widths. Arrays compare order-sensitively; + maps compare as unordered pairs of norms. Semantic tags require the + same tag number and an equivalent tagged value. + + Each norm is a tuple starting with :class:`CBOR_KeyKind` (not a dict), + so norms stay hashable for map ``frozenset`` identity. + """ + if isinstance(value, CBOR_Object): + if isinstance(value, (CBOR_TRUE, CBOR_FALSE)): + return (CBOR_KeyKind.BOOL, bool(value.val)) + if isinstance(value, CBOR_NULL): + return (CBOR_KeyKind.NULL, None) + if isinstance(value, CBOR_UNDEFINED): + return (CBOR_KeyKind.UNDEF, None) + if isinstance(value, (CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER)): + return (CBOR_KeyKind.INT, int(value.val)) + if isinstance(value, CBOR_BYTE_STRING): + return (CBOR_KeyKind.BSTR, bytes(value.val)) + if isinstance(value, CBOR_TEXT_STRING): + return (CBOR_KeyKind.TSTR, str(value.val)) + if isinstance(value, CBOR_FLOAT): + return _cbor_float_key_identity( + value.val, getattr(value, "_encoded", None) + ) + if isinstance(value, CBOR_ARRAY): + return ( + CBOR_KeyKind.ARRAY, + tuple(_cbor_key_norm(v) for v in value.val), + ) + if isinstance(value, CBOR_MAP): + return ( + CBOR_KeyKind.MAP, + frozenset( + (_cbor_key_norm(k), _cbor_key_norm(v)) + for k, v in _cbor_map_pairs(value) + ), + ) + if isinstance(value, CBOR_SEMANTIC_TAG): + tag_num, inner = value.val + return (CBOR_KeyKind.TAG, int(tag_num), _cbor_key_norm(inner)) + if isinstance(value, CBOR_SIMPLE_VALUE): + return (CBOR_KeyKind.SIMPLE, int(value.val)) + return ( + CBOR_KeyKind.OBJ, + type(value).__name__, + _cbor_key_norm(value.val), + ) + if isinstance(value, CBORMapData): + return ( + CBOR_KeyKind.MAP, + frozenset( + (_cbor_key_norm(k), _cbor_key_norm(v)) + for k, v in value.cbor_pairs() + ), + ) + if isinstance(value, dict): + return ( + CBOR_KeyKind.MAP, + frozenset( + (_cbor_key_norm(k), _cbor_key_norm(v)) + for k, v in value.items() + ), + ) + if isinstance(value, bool): + return (CBOR_KeyKind.BOOL, value) + if isinstance(value, int): + return (CBOR_KeyKind.INT, value) + if isinstance(value, float): + return _cbor_float_key_identity(value) + if isinstance(value, bytes): + return (CBOR_KeyKind.BSTR, value) + if isinstance(value, str): + return (CBOR_KeyKind.TSTR, value) + if isinstance(value, list): + return (CBOR_KeyKind.ARRAY, tuple(_cbor_key_norm(v) for v in value)) + if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], int): + # Bare semantic-tag tuple (tag_num, inner), as stored on CBOR_SEMANTIC_TAG. + return (CBOR_KeyKind.TAG, int(value[0]), _cbor_key_norm(value[1])) + return (CBOR_KeyKind.OTHER, type(value).__name__, repr(value)) + + +def _cbor_key_equivalent(a, b): + # type: (Any, Any) -> bool + """Return True when *a* and *b* are equivalent CBOR map keys (RFC 8949).""" + return _cbor_key_norm(a) == _cbor_key_norm(b) + class _CBOR_ERROR(CBOR_Object[Union[bytes, CBOR_Object[Any]]]): """CBOR decoding error wrapper""" diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index fe89fd3abd9..a86e02bf82c 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -12,8 +12,10 @@ Any, Dict, Generic, + Iterable, List, Optional, + Set, Tuple, Type, TypeVar, @@ -21,20 +23,35 @@ cast, ) +from scapy.utils import EnumElement from scapy.cbor.cbor import ( + CBOR_AdditionalInfo, CBOR_Codecs, CBOR_DECODING_ERROR, CBOR_Decoding_Error, CBOR_Encoding_Error, CBOR_Error, + CBOR_FloatAI, CBOR_MajorTypes, CBOR_Object, + CBOR_SimpleValue, + CBOR_UINT64_MAX, _CBOR_ERROR, + _CBORKeyNorm, ) from scapy.compat import chb from scapy.error import log_runtime +MAX_CBOR_NESTING = 128 + +# Wire buffers accepted by decode helpers (slicing preserves the concrete type). +_CBORBuf = Union[bytes, bytearray, memoryview] +_CBORBufT = TypeVar("_CBORBufT", bytes, bytearray, memoryview) +# Major-type / AI arguments: plain ints or Scapy ``EnumElement`` members. +_CBORHeadInt = Union[int, EnumElement] + + ################## # CBOR encoding # ################## @@ -44,6 +61,10 @@ class CBOR_Exception(Exception): pass +class CBOR_INDEFINITE(object): + """Marker returned by :func:`CBOR_decode_head` for indefinite-length items.""" + + class CBOR_Codec_Encoding_Error(CBOR_Encoding_Error): def __init__(self, msg, # type: str @@ -68,75 +89,385 @@ def __init__(self, self.decoded = decoded +def CBOR_encode_initial(major_type, additional_info): + # type: (_CBORHeadInt, _CBORHeadInt) -> bytes + """Encode a CBOR initial byte (3-bit major type + 5-bit additional info).""" + return chb((int(major_type) << 5) | int(additional_info)) + + def CBOR_encode_head(major_type, value): - # type: (int, int) -> bytes + # type: (_CBORHeadInt, int) -> bytes """ - Encode CBOR initial byte and additional info. + Encode CBOR initial byte and additional info for a definite argument. Format: 3 bits major type + 5 bits additional info """ + if value is None: + raise CBOR_Codec_Encoding_Error( + "Indefinite length requires CBOR_encode_initial(..., " + "CBOR_AdditionalInfo.INDEFINITE)" + ) + if not isinstance(value, int) or isinstance(value, bool): + raise CBOR_Codec_Encoding_Error( + "CBOR head value must be an integer, got %r" % (value,)) + if value < 0 or value > CBOR_UINT64_MAX: + raise CBOR_Codec_Encoding_Error( + "CBOR head value out of uint64 range: %r" % (value,)) if value < 24: # Value fits in 5 bits - return chb((major_type << 5) | value) + return CBOR_encode_initial(major_type, value) elif value < 256: # 1-byte value follows - return chb((major_type << 5) | 24) + chb(value) + return ( + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.ONE_BYTE) + + chb(value) + ) elif value < 65536: # 2-byte value follows - return chb((major_type << 5) | 25) + struct.pack(">H", value) + return ( + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.TWO_BYTES) + + struct.pack(">H", value) + ) elif value < 4294967296: # 4-byte value follows - return chb((major_type << 5) | 26) + struct.pack(">I", value) + return ( + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.FOUR_BYTES) + + struct.pack(">I", value) + ) else: # 8-byte value follows - return chb((major_type << 5) | 27) + struct.pack(">Q", value) + return ( + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.EIGHT_BYTES) + + struct.pack(">Q", value) + ) + + +CBOR_BREAK_BYTE = 0xFF + + +def _cbor_buf_bytes(buf): + # type: (_CBORBuf) -> bytes + """Materialize a bytes/memoryview slice as ``bytes``.""" + if isinstance(buf, memoryview): + return buf.tobytes() + return bytes(buf) + + +def cbor_is_break(s): + # type: (_CBORBuf) -> bool + """Return whether *s* begins with a CBOR break byte.""" + return bool(s) and s[0] == CBOR_BREAK_BYTE + + +def cbor_consume_break(s): + # type: (_CBORBufT) -> _CBORBufT + """Consume a leading CBOR break byte from *s*.""" + if not cbor_is_break(s): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=_cbor_buf_bytes(s)) + return s[1:] + + +def _cbor_skip_item(s, depth=0): + # type: (_CBORBufT, int) -> _CBORBufT + """Advance past one well-formed CBOR item without building objects.""" + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=_cbor_buf_bytes(s)) + major_type, value, rem = CBOR_decode_head(s) + if major_type in ( + CBOR_MajorTypes.UNSIGNED_INTEGER, + CBOR_MajorTypes.NEGATIVE_INTEGER, + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + ): + return rem + if major_type in ( + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, + ): + if value is CBOR_INDEFINITE: + expected_type = major_type + while rem and not cbor_is_break(rem): + chunk_type, chunk_len, rem = CBOR_decode_head(rem) + if chunk_type != expected_type: + raise CBOR_Codec_Decoding_Error( + "Indefinite string chunk must be major type %d, " + "got %d" % (expected_type, chunk_type), + remaining=_cbor_buf_bytes(rem)) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite string", + remaining=_cbor_buf_bytes(rem)) + length = int(chunk_len) + if len(rem) < length: + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string chunk", + remaining=_cbor_buf_bytes(rem)) + rem = rem[length:] + return cbor_consume_break(rem) + length = int(value) + if len(rem) < length: + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string", + remaining=_cbor_buf_bytes(rem)) + return rem[length:] + if major_type == CBOR_MajorTypes.ARRAY: + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _cbor_skip_item(rem, depth + 1) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = _cbor_skip_item(rem, depth + 1) + return rem + if major_type == CBOR_MajorTypes.MAP: + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _cbor_skip_item(rem, depth + 1) + rem = _cbor_skip_item(rem, depth + 1) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = _cbor_skip_item(rem, depth + 1) + rem = _cbor_skip_item(rem, depth + 1) + return rem + if major_type == CBOR_MajorTypes.TAG: + return _cbor_skip_item(rem, depth + 1) + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(rem), + ) + + +def cbor_count_items(s, max_count=None, until_break=False): + # type: (_CBORBuf, Optional[int], bool) -> int + """Count top-level CBOR items without building object trees. + + When *until_break* is true, stop at a break byte without consuming it. + When *max_count* is set, stop after that many items even if more remain. + """ + rem = s if isinstance(s, memoryview) else memoryview(s) + count = 0 + while rem and not (until_break and cbor_is_break(rem)): + if max_count is not None and count >= max_count: + break + rem = _cbor_skip_item(rem) + count += 1 + return count + + +def cbor_item_span(s): + # type: (_CBORBuf) -> Tuple[bytes, bytes] + """Split *s* into the first structural CBOR item and the remainder. + + Uses :func:`_cbor_skip_item` for boundary finding only. Callers that must + reject semantically invalid CBOR (invalid UTF-8, duplicate map keys, …) + should decode with :meth:`CBORcodec_Object.decode_cbor_item` instead. + """ + rem = s if isinstance(s, memoryview) else memoryview(s) + after = _cbor_skip_item(rem) + n = len(s) - len(after) + return bytes(s[:n]), bytes(s[n:]) def CBOR_decode_head(s): - # type: (bytes) -> Tuple[int, int, bytes] + # type: (_CBORBuf) -> Tuple[int, Union[int, CBOR_INDEFINITE], _CBORBuf] """ Decode CBOR initial byte and additional info. Returns: (major_type, value, remaining_bytes) """ if not s: - raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) + raise CBOR_Codec_Decoding_Error( + "Empty CBOR data", remaining=_cbor_buf_bytes(s)) initial_byte = s[0] major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f + # 0-23: argument is the additional-info nibble itself + # (CBOR_AdditionalInfo.ONE_BYTE == 24). if additional_info < 24: - # Value is in the additional info return major_type, additional_info, s[1:] - elif additional_info == 24: + elif additional_info == CBOR_AdditionalInfo.ONE_BYTE: # 1-byte value follows if len(s) < 2: raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 1-byte value", remaining=s) + "Not enough bytes for 1-byte value", + remaining=_cbor_buf_bytes(s)) return major_type, s[1], s[2:] - elif additional_info == 25: + elif additional_info == CBOR_AdditionalInfo.TWO_BYTES: # 2-byte value follows if len(s) < 3: raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 2-byte value", remaining=s) + "Not enough bytes for 2-byte value", + remaining=_cbor_buf_bytes(s)) value = struct.unpack(">H", s[1:3])[0] return major_type, value, s[3:] - elif additional_info == 26: + elif additional_info == CBOR_AdditionalInfo.FOUR_BYTES: # 4-byte value follows if len(s) < 5: raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 4-byte value", remaining=s) + "Not enough bytes for 4-byte value", + remaining=_cbor_buf_bytes(s)) value = struct.unpack(">I", s[1:5])[0] return major_type, value, s[5:] - elif additional_info == 27: + elif additional_info == CBOR_AdditionalInfo.EIGHT_BYTES: # 8-byte value follows if len(s) < 9: raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 8-byte value", remaining=s) + "Not enough bytes for 8-byte value", + remaining=_cbor_buf_bytes(s)) value = struct.unpack(">Q", s[1:9])[0] return major_type, value, s[9:] + elif additional_info == CBOR_AdditionalInfo.INDEFINITE: + if major_type in ( + CBOR_MajorTypes.UNSIGNED_INTEGER, + CBOR_MajorTypes.NEGATIVE_INTEGER, + CBOR_MajorTypes.TAG, + ): + raise CBOR_Codec_Decoding_Error( + "Indefinite length not allowed for major type %d" % + major_type, remaining=_cbor_buf_bytes(s)) + if major_type in ( + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, + CBOR_MajorTypes.ARRAY, + CBOR_MajorTypes.MAP, + ): + return major_type, CBOR_INDEFINITE, s[1:] + raise CBOR_Codec_Decoding_Error( + "Indefinite length not allowed for major type %d" % + major_type, remaining=_cbor_buf_bytes(s)) + elif additional_info in ( + CBOR_AdditionalInfo.RESERVED_28, + CBOR_AdditionalInfo.RESERVED_29, + CBOR_AdditionalInfo.RESERVED_30, + ): + raise CBOR_Codec_Decoding_Error( + "Reserved additional info: %d" % additional_info, + remaining=_cbor_buf_bytes(s)) else: raise CBOR_Codec_Decoding_Error( - "Invalid additional info: %d" % additional_info, remaining=s) + "Invalid additional info: %d" % additional_info, + remaining=_cbor_buf_bytes(s)) + + +def _cbor_float_from_bits(ai, bits): + # type: (int, int) -> float + if ai == CBOR_FloatAI.HALF: + return struct.unpack(">e", struct.pack(">H", bits & 0xffff))[0] + if ai == CBOR_FloatAI.SINGLE: + return struct.unpack(">f", struct.pack(">I", bits))[0] + return struct.unpack(">d", struct.pack(">Q", bits))[0] + + +def _cbor_float_to_half_bits(value): + # type: (float) -> Optional[int] + """Return IEEE binary16 bits when *value* round-trips exactly.""" + import math + if math.isnan(value): + # Callers that care about NaN payloads must use bit-pattern helpers. + return 0x7E00 + sign = 0x8000 if math.copysign(1.0, value) < 0 else 0 + if math.isinf(value): + return sign | 0x7C00 + try: + packed = struct.pack(">e", value) + except (OverflowError, ValueError): + return None + bits = struct.unpack(">H", packed)[0] + decoded = struct.unpack(">e", packed)[0] + if math.isinf(decoded) or decoded != value: + return None + if value == 0.0 and ( + math.copysign(1.0, decoded) != math.copysign(1.0, value) + ): + return None + return bits + + +def _cbor_nan_preferred_ai(ai, bits): + # type: (int, int) -> int + """Preferred float AI for a NaN, based on the original bit pattern. + + RFC 8949 prefers a shorter NaN only when zero-padding the shorter + significand reconstructs the original NaN payload. + """ + # Return plain ints: callers order preferred AI with ``<``. + if ai == CBOR_FloatAI.HALF: + return int(CBOR_FloatAI.HALF) + if ai == CBOR_FloatAI.SINGLE: + # binary32 NaN: 1+8+23. Prefer half when low 13 significand bits are 0. + mant = int(bits) & 0x7FFFFF + if mant and (mant & ((1 << 13) - 1)) == 0: + return int(CBOR_FloatAI.HALF) + return int(CBOR_FloatAI.SINGLE) + if ai == CBOR_FloatAI.DOUBLE: + # binary64 NaN: 1+11+52. + mant = int(bits) & ((1 << 52) - 1) + if mant == 0: + # Infinity, not NaN — caller should not use this helper. + return int(CBOR_FloatAI.DOUBLE) + # Prefer half when only the top 10 significand bits are used. + if (mant & ((1 << 42) - 1)) == 0: + return int(CBOR_FloatAI.HALF) + # Prefer single when only the top 23 significand bits are used. + if (mant & ((1 << 29) - 1)) == 0: + return int(CBOR_FloatAI.SINGLE) + return int(CBOR_FloatAI.DOUBLE) + return ai + + +def _cbor_nan_components(ai, bits): + # type: (int, int) -> Optional[Tuple[int, int]] + """Return ``(sign, significand52)`` for a NaN pattern, else ``None``. + + The significand is zero-extended to a binary64-width 52-bit field so + half / single / double representations of the same NaN share identity. + + Bit extraction is required here: ``struct`` float formats discard NaN + payloads, which RFC 8949 map-key identity must preserve. + """ + if ai == CBOR_FloatAI.HALF: + sign = (int(bits) >> 15) & 0x1 + exponent = (int(bits) >> 10) & 0x1f + fraction = int(bits) & 0x3ff + if exponent != 31 or not fraction: + return None + return sign, fraction << 42 + if ai == CBOR_FloatAI.SINGLE: + sign = (int(bits) >> 31) & 0x1 + exponent = (int(bits) >> 23) & 0xff + fraction = int(bits) & 0x7fffff + if exponent != 0xff or not fraction: + return None + return sign, fraction << 29 + if ai == CBOR_FloatAI.DOUBLE: + sign = (int(bits) >> 63) & 0x1 + exponent = (int(bits) >> 52) & 0x7ff + fraction = int(bits) & ((1 << 52) - 1) + if exponent != 0x7ff or not fraction: + return None + return sign, fraction + return None + + +def _cbor_preferred_float_ai(value): + # type: (float) -> int + """Return the preferred float AI for a numeric *value*.""" + import math + # Return plain ints: callers order preferred AI with ``<``. + if math.isnan(value): + # Without the original payload bits, only the quiet binary16 NaN is a + # safe generic preference. Encoded-width checks use bit patterns. + return int(CBOR_FloatAI.HALF) + if _cbor_float_to_half_bits(value) is not None: + return int(CBOR_FloatAI.HALF) + try: + single = struct.unpack(">f", struct.pack(">f", value))[0] + except (OverflowError, struct.error): + return int(CBOR_FloatAI.DOUBLE) + if single == value or (math.isinf(single) and math.isinf(value)): + return int(CBOR_FloatAI.SINGLE) + return int(CBOR_FloatAI.DOUBLE) # [ CBOR codec classes ] # @@ -189,7 +520,7 @@ def do_dec(cls, ): # type: (...) -> Tuple[CBOR_Object[Any], bytes] """Decode CBOR data using automatic dispatch based on major type.""" - return _decode_cbor_item(s, safe=safe) + return CBORcodec_Object.decode_cbor_item(s, depth=_depth) @classmethod def dec(cls, @@ -199,10 +530,11 @@ def dec(cls, _depth=0, # type: int ): # type: (...) -> Tuple[Union[_CBOR_ERROR, CBOR_Object[_K]], bytes] + # Nested decoding must raise so safedec only wraps the outermost item. if not safe: - return cls.do_dec(s, context, safe, _depth=_depth) + return cls.do_dec(s, context, False, _depth=_depth) try: - return cls.do_dec(s, context, safe, _depth=_depth) + return cls.do_dec(s, context, False, _depth=_depth) except CBOR_Codec_Decoding_Error as e: return CBOR_DECODING_ERROR(s, exc=e), b"" except CBOR_Error as e: @@ -222,6 +554,245 @@ def enc(cls, s): # type: (_K) -> bytes raise NotImplementedError("Subclasses must implement enc") + @staticmethod + def encode_cbor_item(item): + # type: (Any) -> bytes + """Encode a Python value to CBOR bytes""" + from scapy.cbor.cbor import ( + CBOR_Object, + CBORMapData, + ) + + if isinstance(item, CBOR_Object): + return item.enc() + elif isinstance(item, CBORMapData): + return CBORcodec_MAP.enc(item) + elif isinstance(item, bool): + # Must check bool before int (bool is subclass of int) + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif isinstance(item, int): + if item >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(item) + else: + return CBORcodec_NEGATIVE_INTEGER.enc(item) + elif isinstance(item, bytes): + return CBORcodec_BYTE_STRING.enc(item) + elif isinstance(item, str): + return CBORcodec_TEXT_STRING.enc(item) + elif isinstance(item, list): + return CBORcodec_ARRAY.enc(item) + elif isinstance(item, dict): + return CBORcodec_MAP.enc(item) + elif isinstance(item, float): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif item is None: + return CBORcodec_SIMPLE_AND_FLOAT.enc(None) + else: + raise CBOR_Codec_Encoding_Error( + "Cannot encode type: %s" % type(item)) + + @staticmethod + def _reject_duplicate_map_keys(pairs): + # type: (Iterable[Tuple[Any, Any]]) -> None + """Raise if *pairs* contain CBOR-equivalent duplicate keys.""" + from scapy.cbor.cbor import _cbor_key_norm + seen_norms = set() # type: Set[_CBORKeyNorm] + for key, _value in pairs: + norm = _cbor_key_norm(key) + if norm in seen_norms: + raise CBOR_Codec_Encoding_Error( + "Duplicate CBOR map key: %r" % (key,) + ) + seen_norms.add(norm) + + @staticmethod + def _encode_cbor_map_deterministic(pairs): + # type: (Iterable[Tuple[Any, Any]]) -> bytes + """Encode map pairs in RFC 8949 core-deterministic key order.""" + pairs = list(pairs) + CBORcodec_Object._reject_duplicate_map_keys(pairs) + encoded_pairs = [] # type: List[Tuple[bytes, bytes]] + for key, value in pairs: + key_bytes = CBORcodec_Object.encode_cbor_item_deterministic(key) + value_bytes = CBORcodec_Object.encode_cbor_item_deterministic( + value + ) + encoded_pairs.append((key_bytes, value_bytes)) + encoded_pairs.sort(key=lambda item: item[0]) + parts = [CBOR_encode_head(CBOR_MajorTypes.MAP, len(encoded_pairs))] + for key_bytes, value_bytes in encoded_pairs: + parts.append(key_bytes) + parts.append(value_bytes) + return b"".join(parts) + + @staticmethod + def encode_cbor_item_deterministic(item): + # type: (Any) -> bytes + """Encode a Python value using RFC 8949 core-deterministic rules. + + Unlike :meth:`encode_cbor_item`, map keys at every nesting level are + sorted by their deterministic encoded bytes. Intended for schema-driven + rebuild paths such as preserved unknown ``CBORF_MAP`` members. + + :class:`~scapy.cbor.cbor.CBOR_Object` instances are accepted and reduced + to native values (preferred float encoding, deterministic nested maps). + """ + import math + from scapy.cbor.cbor import ( + CBOR_Object, + CBOR_ARRAY, + CBOR_FLOAT, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_UNDEFINED, + CBORMapData, + _cbor_float_wire_parts, + _cbor_map_pairs, + ) + + if isinstance(item, CBOR_Object): + if isinstance(item, CBOR_UNDEFINED): + return CBOR_UNDEFINED().enc() + if isinstance(item, CBOR_FLOAT): + encoded = getattr(item, "_encoded", None) + if encoded is not None and math.isnan(float(item.val)): + try: + ai, bits = _cbor_float_wire_parts(encoded) + except ValueError as exc: + raise CBOR_Codec_Encoding_Error(str(exc)) + comps = _cbor_nan_components(ai, bits) + if comps is None: + raise CBOR_Codec_Encoding_Error( + "encoded float is not a NaN: %r" + % (bytes(encoded),) + ) + sign, significand52 = comps + preferred = _cbor_nan_preferred_ai(ai, bits) + if preferred == CBOR_FloatAI.HALF: + fraction = (significand52 >> 42) & 0x3ff + nan_bits = (sign << 15) | (0x1f << 10) | fraction + return ( + CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.HALF) + + struct.pack(">H", nan_bits) + ) + if preferred == CBOR_FloatAI.SINGLE: + fraction = (significand52 >> 29) & 0x7fffff + nan_bits = (sign << 31) | (0xff << 23) | fraction + return ( + CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.SINGLE) + + struct.pack(">I", nan_bits) + ) + if preferred == CBOR_FloatAI.DOUBLE: + nan_bits = ( + (sign << 63) | + (0x7ff << 52) | + (significand52 & ((1 << 52) - 1)) + ) + return ( + CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.DOUBLE) + + struct.pack(">Q", nan_bits) + ) + raise CBOR_Codec_Encoding_Error( + "Invalid NaN float AI: %d" % preferred) + # Finite floats ignore original width; rebuild preferred form. + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item.val)) + if isinstance(item, CBOR_ARRAY): + return CBORcodec_Object.encode_cbor_item_deterministic( + list(item.val) + ) + if isinstance(item, CBOR_MAP): + return CBORcodec_Object._encode_cbor_map_deterministic( + _cbor_map_pairs(item) + ) + if isinstance(item, CBOR_SEMANTIC_TAG): + tag_num, inner = item.val + return ( + CBOR_encode_head(CBOR_MajorTypes.TAG, tag_num) + + CBORcodec_Object.encode_cbor_item_deterministic(inner) + ) + if isinstance(item, CBOR_SIMPLE_VALUE): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + return CBORcodec_Object.encode_cbor_item_deterministic(item.val) + if isinstance(item, CBORMapData): + return CBORcodec_Object._encode_cbor_map_deterministic( + item.cbor_pairs() + ) + if isinstance(item, dict): + return CBORcodec_Object._encode_cbor_map_deterministic( + list(item.items()) + ) + if isinstance(item, list): + encoded_items = [ + CBORcodec_Object.encode_cbor_item_deterministic(element) + for element in item + ] + return ( + CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(encoded_items)) + + b"".join(encoded_items) + ) + return CBORcodec_Object.encode_cbor_item(item) + + @staticmethod + def decode_cbor_item(s, depth=0): + # type: (_CBORBuf, int) -> Tuple[CBOR_Object[Any], _CBORBuf] + """Decode CBOR bytes to a CBOR_Object. + + Top-level callers may pass ``bytes`` (or a subclass). Decoding then + works on a ``memoryview`` so unread suffixes are not recopied per item. + """ + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=_cbor_buf_bytes(s)) + if not isinstance(s, memoryview): + obj, rem = CBORcodec_Object.decode_cbor_item( + memoryview(s), depth=depth + ) + return ( + obj, + _cbor_buf_bytes(rem) if isinstance(rem, memoryview) else rem, + ) + if not s: + raise CBOR_Codec_Decoding_Error( + "Empty CBOR data", remaining=_cbor_buf_bytes(s)) + + if cbor_is_break(s): + raise CBOR_Codec_Decoding_Error( + "Standalone break byte (0xff)", + remaining=_cbor_buf_bytes(s)) + + initial_byte = s[0] + major_type = initial_byte >> 5 + + # Dispatch to appropriate codec based on major type + if major_type == CBOR_MajorTypes.UNSIGNED_INTEGER: + return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.NEGATIVE_INTEGER: + return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.BYTE_STRING: + return CBORcodec_BYTE_STRING.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.TEXT_STRING: + return CBORcodec_TEXT_STRING.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.ARRAY: + return CBORcodec_ARRAY.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.MAP: + return CBORcodec_MAP.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.TAG: + return CBORcodec_SEMANTIC_TAG.dec(s, safe=False, _depth=depth) + elif major_type == CBOR_MajorTypes.SIMPLE_AND_FLOAT: + return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=False, _depth=depth) + else: + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(s)) + CBOR_Codecs.CBOR.register_stem(CBORcodec_Object) @@ -244,7 +815,10 @@ def enc(cls, obj): raise CBOR_Codec_Encoding_Error( "Cannot encode negative value as unsigned integer. " "Use CBOR_NEGATIVE_INTEGER for negative values.") - return CBOR_encode_head(0, i) + if i > CBOR_UINT64_MAX: + raise CBOR_Codec_Encoding_Error( + "Unsigned integer exceeds uint64 range") + return CBOR_encode_head(CBOR_MajorTypes.UNSIGNED_INTEGER, i) @classmethod def do_dec(cls, @@ -256,7 +830,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[int], bytes] cls.check_string(s) major_type, value, remainder = CBOR_decode_head(s) - if major_type != 0: + if major_type != CBOR_MajorTypes.UNSIGNED_INTEGER: raise CBOR_Codec_Decoding_Error( "Expected major type 0 (unsigned integer), got %d" % major_type, remaining=s) @@ -276,8 +850,11 @@ def enc(cls, obj): raise CBOR_Codec_Encoding_Error( "Cannot encode non-negative value as negative integer. " "Use CBOR_UNSIGNED_INTEGER for non-negative values.") + if i < -(CBOR_UINT64_MAX + 1): + raise CBOR_Codec_Encoding_Error( + "Negative integer below CBOR int64 range") # CBOR negative integer: -1 - n - return CBOR_encode_head(1, -1 - i) + return CBOR_encode_head(CBOR_MajorTypes.NEGATIVE_INTEGER, -1 - i) @classmethod def do_dec(cls, @@ -289,7 +866,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[int], bytes] cls.check_string(s) major_type, value, remainder = CBOR_decode_head(s) - if major_type != 1: + if major_type != CBOR_MajorTypes.NEGATIVE_INTEGER: raise CBOR_Codec_Decoding_Error( "Expected major type 1 (negative integer), got %d" % major_type, remaining=s) @@ -308,7 +885,7 @@ def enc(cls, obj): data = obj.val if isinstance(obj, CBOR_Object) else obj if not isinstance(data, bytes): data = bytes(data) - return CBOR_encode_head(2, len(data)) + data + return CBOR_encode_head(CBOR_MajorTypes.BYTE_STRING, len(data)) + data @classmethod def do_dec(cls, @@ -320,15 +897,40 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[bytes], bytes] cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 2: + if major_type != CBOR_MajorTypes.BYTE_STRING: raise CBOR_Codec_Decoding_Error( "Expected major type 2 (byte string), got %d" % major_type, remaining=s) + if length is CBOR_INDEFINITE: + chunks = [] # type: List[bytes] + while True: + if cbor_is_break(remainder): + remainder = cbor_consume_break(remainder) + break + chunk_mt, chunk_len, remainder = CBOR_decode_head(remainder) + if chunk_mt != CBOR_MajorTypes.BYTE_STRING: + raise CBOR_Codec_Decoding_Error( + "Indefinite byte string chunk must be major type 2", + remaining=remainder) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite byte string", remaining=remainder) + if len(remainder) < chunk_len: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for byte string chunk: " + "expected %d, got %d" % + (chunk_len, len(remainder)), remaining=remainder) + chunks.append(_cbor_buf_bytes(remainder[:chunk_len])) + remainder = remainder[chunk_len:] + return cls.cbor_object(b"".join(chunks)), remainder if len(remainder) < length: raise CBOR_Codec_Decoding_Error( "Not enough bytes for byte string: expected %d, got %d" % - (length, len(remainder)), remaining=s) - return cls.cbor_object(remainder[:length]), remainder[length:] + (length, len(remainder)), remaining=_cbor_buf_bytes(s)) + return ( + cls.cbor_object(_cbor_buf_bytes(remainder[:length])), + remainder[length:], + ) class CBORcodec_TEXT_STRING(CBORcodec_Object[str]): @@ -344,7 +946,10 @@ def enc(cls, obj): text_bytes = text.encode('utf-8') else: text_bytes = bytes(text) - return CBOR_encode_head(3, len(text_bytes)) + text_bytes + return ( + CBOR_encode_head(CBOR_MajorTypes.TEXT_STRING, len(text_bytes)) + + text_bytes + ) @classmethod def do_dec(cls, @@ -356,19 +961,48 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[str], bytes] cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 3: + if major_type != CBOR_MajorTypes.TEXT_STRING: raise CBOR_Codec_Decoding_Error( "Expected major type 3 (text string), got %d" % major_type, remaining=s) + if length is CBOR_INDEFINITE: + decoded_chunks = [] # type: List[str] + while True: + if cbor_is_break(remainder): + remainder = cbor_consume_break(remainder) + break + chunk_mt, chunk_len, remainder = CBOR_decode_head(remainder) + if chunk_mt != CBOR_MajorTypes.TEXT_STRING: + raise CBOR_Codec_Decoding_Error( + "Indefinite text string chunk must be major type 3", + remaining=remainder) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite text string", remaining=remainder) + if len(remainder) < chunk_len: + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for text string chunk: " + "expected %d, got %d" % + (chunk_len, len(remainder)), remaining=remainder) + chunk_bytes = _cbor_buf_bytes(remainder[:chunk_len]) + remainder = remainder[chunk_len:] + try: + decoded_chunks.append(chunk_bytes.decode('utf-8')) + except UnicodeDecodeError as e: + raise CBOR_Codec_Decoding_Error( + "Invalid UTF-8 in text string chunk: %s" % str(e), + remaining=_cbor_buf_bytes(s)) + return cls.cbor_object("".join(decoded_chunks)), remainder if len(remainder) < length: raise CBOR_Codec_Decoding_Error( "Not enough bytes for text string: expected %d, got %d" % - (length, len(remainder)), remaining=s) + (length, len(remainder)), remaining=_cbor_buf_bytes(s)) try: - text = remainder[:length].decode('utf-8') + text = _cbor_buf_bytes(remainder[:length]).decode('utf-8') except UnicodeDecodeError as e: raise CBOR_Codec_Decoding_Error( - "Invalid UTF-8 in text string: %s" % str(e), remaining=s) + "Invalid UTF-8 in text string: %s" % str(e), + remaining=_cbor_buf_bytes(s)) return cls.cbor_object(text), remainder[length:] @@ -381,10 +1015,12 @@ def enc(cls, obj): # type: (Union[List[Any], CBOR_Object[List[Any]]]) -> bytes from scapy.cbor.cbor import CBOR_Object array = obj.val if isinstance(obj, CBOR_Object) else obj - result = CBOR_encode_head(4, len(array)) - for item in array: - result += CBORcodec_Object.encode_cbor_item(item) - return result + parts = [CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(array))] + parts.extend( + CBORcodec_Object.encode_cbor_item(item) + for item in array + ) + return b"".join(parts) @classmethod def do_dec(cls, @@ -396,37 +1032,56 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[List[Any]], bytes] cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 4: + if major_type != CBOR_MajorTypes.ARRAY: raise CBOR_Codec_Decoding_Error( "Expected major type 4 (array), got %d" % major_type, remaining=s) items = [] - for _ in range(length): - if not remainder: - raise CBOR_Codec_Decoding_Error( - "Not enough items in array", remaining=s) - item, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=safe) - items.append(item) + if length is CBOR_INDEFINITE: + while True: + if cbor_is_break(remainder): + remainder = cbor_consume_break(remainder) + break + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough items in array", remaining=s) + item, remainder = CBORcodec_Object.decode_cbor_item( + remainder, depth=_depth + 1) + items.append(item) + else: + for _ in range(length): + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough items in array", remaining=s) + item, remainder = CBORcodec_Object.decode_cbor_item( + remainder, depth=_depth + 1) + items.append(item) return cls.cbor_object(items), remainder -class CBORcodec_MAP(CBORcodec_Object[Dict[Any, Any]]): - """CBOR map codec (major type 5)""" +class CBORcodec_MAP(CBORcodec_Object[Any]): + """CBOR map codec (major type 5). + + Maps are stored as an ordered list of ``(key, value)`` CBOR objects so + that unhashable keys and distinct CBOR items that collide under Python + equality (``1`` vs ``True``) round-trip faithfully. + """ tag = CBOR_MajorTypes.MAP @classmethod def enc(cls, obj): - # type: (Union[Dict[Any, Any], CBOR_Object[Dict[Any, Any]]]) -> bytes - from scapy.cbor.cbor import CBOR_Object + # type: (Any) -> bytes + from scapy.cbor.cbor import CBOR_Object, _cbor_map_pairs mapping = obj.val if isinstance(obj, CBOR_Object) else obj - result = CBOR_encode_head(5, len(mapping)) - for key, value in mapping.items(): - result += CBORcodec_Object.encode_cbor_item(key) - result += CBORcodec_Object.encode_cbor_item(value) - return result + pairs = _cbor_map_pairs(mapping) + CBORcodec_Object._reject_duplicate_map_keys(pairs) + parts = [CBOR_encode_head(CBOR_MajorTypes.MAP, len(pairs))] + for key, value in pairs: + parts.append(CBORcodec_Object.encode_cbor_item(key)) + parts.append(CBORcodec_Object.encode_cbor_item(value)) + return b"".join(parts) @classmethod def do_dec(cls, @@ -435,34 +1090,60 @@ def do_dec(cls, safe=False, # type: bool _depth=0, # type: int ): - # type: (...) -> Tuple[CBOR_Object[Dict[Any, Any]], bytes] + # type: (...) -> Tuple[CBOR_Object[Any], bytes] + from scapy.cbor.cbor import CBORMapData cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 5: + if major_type != CBOR_MajorTypes.MAP: raise CBOR_Codec_Decoding_Error( "Expected major type 5 (map), got %d" % major_type, remaining=s) - mapping = {} - for _ in range(length): - if not remainder: - raise CBOR_Codec_Decoding_Error( - "Not enough key-value pairs in map", remaining=s) - key, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=safe) - if not remainder: + pairs = [] # type: List[Tuple[Any, Any]] + seen_norms = set() # type: Set[_CBORKeyNorm] + + def _add_pair(key, value): + # type: (Any, Any) -> None + from scapy.cbor.cbor import _cbor_key_norm + norm = _cbor_key_norm(key) + if norm in seen_norms: raise CBOR_Codec_Decoding_Error( - "Map key without value", remaining=s) - value, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=safe) - # Convert key to hashable type if it's a CBOR object - if isinstance(key, CBOR_Object): - key_val = key.val - else: - key_val = key - mapping[key_val] = value + "Duplicate CBOR map key: %r" % (key,), + remaining=s) + seen_norms.add(norm) + pairs.append((key, value)) + + if length is CBOR_INDEFINITE: + while True: + if cbor_is_break(remainder): + remainder = cbor_consume_break(remainder) + break + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough key-value pairs in map", remaining=s) + key, remainder = CBORcodec_Object.decode_cbor_item( + remainder, depth=_depth + 1) + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Map key without value", remaining=s) + value, remainder = CBORcodec_Object.decode_cbor_item( + remainder, depth=_depth + 1) + _add_pair(key, value) + else: + for _ in range(length): + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Not enough key-value pairs in map", remaining=s) + key, remainder = CBORcodec_Object.decode_cbor_item( + remainder, depth=_depth + 1) + if not remainder: + raise CBOR_Codec_Decoding_Error( + "Map key without value", remaining=s) + value, remainder = CBORcodec_Object.decode_cbor_item( + remainder, depth=_depth + 1) + _add_pair(key, value) - return cls.cbor_object(mapping), remainder + return cls.cbor_object(CBORMapData(pairs)), remainder class CBORcodec_SEMANTIC_TAG(CBORcodec_Object[Tuple[int, Any]]): @@ -475,9 +1156,13 @@ def enc(cls, obj): from scapy.cbor.cbor import CBOR_Object tagged_item = obj.val if isinstance(obj, CBOR_Object) else obj tag_num, item = tagged_item - result = CBOR_encode_head(6, tag_num) - result += CBORcodec_Object.encode_cbor_item(item) - return result + if tag_num < 0 or tag_num > CBOR_UINT64_MAX: + raise CBOR_Codec_Encoding_Error( + "Semantic tag number out of uint64 range") + return ( + CBOR_encode_head(CBOR_MajorTypes.TAG, tag_num) + + CBORcodec_Object.encode_cbor_item(item) + ) @classmethod def do_dec(cls, @@ -489,7 +1174,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[Tuple[int, Any]], bytes] cls.check_string(s) major_type, tag_num, remainder = CBOR_decode_head(s) - if major_type != 6: + if major_type != CBOR_MajorTypes.TAG: raise CBOR_Codec_Decoding_Error( "Expected major type 6 (tag), got %d" % major_type, remaining=s) @@ -499,7 +1184,7 @@ def do_dec(cls, "Tag without following item", remaining=s) item, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=safe) + remainder, depth=_depth + 1) return cls.cbor_object((tag_num, item)), remainder @@ -516,13 +1201,17 @@ def enc(cls, obj): # Check if obj is a CBOR object instance (for special cases like UNDEFINED) if isinstance(obj, CBOR_UNDEFINED): - return chb(0xf7) # undefined + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.UNDEFINED) elif isinstance(obj, CBOR_NULL): - return chb(0xf6) # null + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.NULL) elif isinstance(obj, CBOR_TRUE): - return chb(0xf5) # true + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.TRUE) elif isinstance(obj, CBOR_FALSE): - return chb(0xf4) # false + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.FALSE) elif isinstance(obj, CBOR_Object): # For other CBOR objects, use their val attribute val = obj.val @@ -530,17 +1219,51 @@ def enc(cls, obj): val = obj if val is False: - return chb(0xf4) # false + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.FALSE) elif val is True: - return chb(0xf5) # true + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.TRUE) elif val is None: - return chb(0xf6) # null + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.NULL) elif isinstance(val, float): - # Encode as double precision (8 bytes) - return chb(0xfb) + struct.pack(">d", val) + # Preferred serialization (RFC 8949): shortest float that + # preserves the numeric value. Received non-preferred widths are + # preserved via packet raw caches, not by this encoder. + ai = _cbor_preferred_float_ai(val) + if ai == CBOR_FloatAI.HALF: + half = _cbor_float_to_half_bits(val) + if half is not None: + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.HALF) + + struct.pack(">H", half) + ) + ai = CBOR_FloatAI.SINGLE + if ai == CBOR_FloatAI.SINGLE: + try: + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.SINGLE) + + struct.pack(">f", val) + ) + except (OverflowError, struct.error): + pass + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.DOUBLE) + + struct.pack(">d", val) + ) elif isinstance(val, int) and 0 <= val <= 23: # Simple value 0-23 - return CBOR_encode_head(7, val) + return CBOR_encode_head(CBOR_MajorTypes.SIMPLE_AND_FLOAT, val) + elif isinstance(val, int) and 32 <= val <= 255: + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_AdditionalInfo.ONE_BYTE) + + chb(val) + ) else: raise CBOR_Codec_Encoding_Error( "Cannot encode value as simple/float: %r" % val) @@ -560,158 +1283,61 @@ def do_dec(cls, cls.check_string(s) - # For major type 7, we need special handling because additional_info - # encodes different things (simple values vs float sizes) + # For major type 7, additional_info encodes simple values vs float sizes. initial_byte = s[0] major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - if major_type != 7: + if major_type != CBOR_MajorTypes.SIMPLE_AND_FLOAT: raise CBOR_Codec_Decoding_Error( "Expected major type 7 (simple/float), got %d" % major_type, remaining=s) # Check for special simple values (encoded directly in additional_info) - if additional_info == 20: + if additional_info == CBOR_SimpleValue.FALSE: return CBOR_FALSE(), s[1:] - elif additional_info == 21: + elif additional_info == CBOR_SimpleValue.TRUE: return CBOR_TRUE(), s[1:] - elif additional_info == 22: + elif additional_info == CBOR_SimpleValue.NULL: return CBOR_NULL(), s[1:] - elif additional_info == 23: + elif additional_info == CBOR_SimpleValue.UNDEFINED: return CBOR_UNDEFINED(), s[1:] - elif additional_info == 25: - # Half precision float (2 bytes) - IEEE 754 binary16 - if len(s) < 3: - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for half float", remaining=s) - half_bytes = s[1:3] - remainder = s[3:] - # Convert IEEE 754 binary16 to binary64 (double) - half_int = struct.unpack(">H", half_bytes)[0] - sign = (half_int >> 15) & 0x1 - exponent = (half_int >> 10) & 0x1f - fraction = half_int & 0x3ff - - # Handle special cases - if exponent == 0: - if fraction == 0: - # Zero - float_val = -0.0 if sign else 0.0 - else: - # Subnormal number - float_val = ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) - elif exponent == 31: - if fraction == 0: - # Infinity - float_val = float('-inf') if sign else float('inf') - else: - # NaN - float_val = float('nan') - else: - # Normalized number - float_val = ( - ((-1) ** sign) * - (1 + fraction / 1024.0) * - (2 ** (exponent - 15))) - - return CBOR_FLOAT(float_val), remainder - elif additional_info == 26: - # Single precision float (4 bytes) - if len(s) < 5: - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for single float", remaining=s) - float_val = struct.unpack(">f", s[1:5])[0] - return CBOR_FLOAT(float_val), s[5:] - elif additional_info == 27: - # Double precision float (8 bytes) - if len(s) < 9: + elif additional_info in ( + CBOR_FloatAI.HALF, + CBOR_FloatAI.SINGLE, + CBOR_FloatAI.DOUBLE, + ): + width = { + CBOR_FloatAI.HALF: 2, + CBOR_FloatAI.SINGLE: 4, + CBOR_FloatAI.DOUBLE: 8, + }[additional_info] + if len(s) < 1 + width: raise CBOR_Codec_Decoding_Error( - "Not enough bytes for double float", remaining=s) - float_val = struct.unpack(">d", s[1:9])[0] - return CBOR_FLOAT(float_val), s[9:] + "Not enough bytes for float", remaining=s) + fmt = {2: ">H", 4: ">I", 8: ">Q"}[width] + bits = struct.unpack(fmt, s[1:1 + width])[0] + float_val = _cbor_float_from_bits(additional_info, bits) + encoded = _cbor_buf_bytes(s[:1 + width]) + return CBOR_FLOAT(float_val, encoded=encoded), s[1 + width:] elif additional_info < 24: - # Simple value 0-23 + # Simple value 0-23 (below CBOR_AdditionalInfo.ONE_BYTE) return CBOR_SIMPLE_VALUE(additional_info), s[1:] else: # additional_info 24 means 1-byte simple value follows - if additional_info == 24: + if additional_info == CBOR_AdditionalInfo.ONE_BYTE: if len(s) < 2: raise CBOR_Codec_Decoding_Error( "Not enough bytes for simple value", remaining=s) - return CBOR_SIMPLE_VALUE(s[1]), s[2:] + simple = s[1] + if simple < 32: + raise CBOR_Codec_Decoding_Error( + "Two-byte simple-value encoding below 32 " + "is not well-formed", + remaining=s) + return CBOR_SIMPLE_VALUE(simple), s[2:] else: raise CBOR_Codec_Decoding_Error( - "Invalid additional info for major type 7: %d" % additional_info, + "Invalid additional info for major type 7: %d" + % additional_info, remaining=s) - - -# Helper methods for encoding/decoding arbitrary CBOR items - - -def _encode_cbor_item(item): - # type: (Any) -> bytes - """Encode a Python value to CBOR bytes""" - from scapy.cbor.cbor import CBOR_Object - - if isinstance(item, CBOR_Object): - return item.enc() - elif isinstance(item, bool): - # Must check bool before int (bool is subclass of int) - return CBORcodec_SIMPLE_AND_FLOAT.enc(item) - elif isinstance(item, int): - if item >= 0: - return CBORcodec_UNSIGNED_INTEGER.enc(item) - else: - return CBORcodec_NEGATIVE_INTEGER.enc(item) - elif isinstance(item, bytes): - return CBORcodec_BYTE_STRING.enc(item) - elif isinstance(item, str): - return CBORcodec_TEXT_STRING.enc(item) - elif isinstance(item, list): - return CBORcodec_ARRAY.enc(item) - elif isinstance(item, dict): - return CBORcodec_MAP.enc(item) - elif isinstance(item, float): - return CBORcodec_SIMPLE_AND_FLOAT.enc(item) - elif item is None: - return CBORcodec_SIMPLE_AND_FLOAT.enc(None) - else: - raise CBOR_Codec_Encoding_Error( - "Cannot encode type: %s" % type(item)) - - -def _decode_cbor_item(s, safe=False): - # type: (bytes, bool) -> Tuple[CBOR_Object[Any], bytes] - """Decode CBOR bytes to a CBOR_Object""" - if not s: - raise CBOR_Codec_Decoding_Error("Empty CBOR data", remaining=s) - - initial_byte = s[0] - major_type = initial_byte >> 5 - - # Dispatch to appropriate codec based on major type - if major_type == 0: - return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=safe) - elif major_type == 1: - return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=safe) - elif major_type == 2: - return CBORcodec_BYTE_STRING.dec(s, safe=safe) - elif major_type == 3: - return CBORcodec_TEXT_STRING.dec(s, safe=safe) - elif major_type == 4: - return CBORcodec_ARRAY.dec(s, safe=safe) - elif major_type == 5: - return CBORcodec_MAP.dec(s, safe=safe) - elif major_type == 6: - return CBORcodec_SEMANTIC_TAG.dec(s, safe=safe) - elif major_type == 7: - return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=safe) - else: - raise CBOR_Codec_Decoding_Error( - "Invalid major type: %d" % major_type, remaining=s) - - -# Add helper methods to CBORcodec_Object -CBORcodec_Object.encode_cbor_item = staticmethod(_encode_cbor_item) -CBORcodec_Object.decode_cbor_item = staticmethod(_decode_cbor_item) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 536424728ec..c901d433414 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -5,32 +5,54 @@ """ Classes that implement CBOR (Concise Binary Object Representation) data structures as packet fields. Modelled after scapy/asn1fields.py. + +Public leaf/compound hooks follow Scapy/ASN.1 style (``any2i`` / ``i2m`` / +``m2i``, ``build`` / ``dissect``). Compounds additionally use +``_build_counted`` / ``_dissect_counted`` so unframed sequences and array +budgeting can return an item count; callers outside this module should +prefer ``build`` / ``dissect``. """ import copy +import math -from functools import reduce +from dataclasses import dataclass from scapy.cbor.cbor import ( + CBOR_AdditionalInfo, CBOR_Decoding_Error, - CBOR_Error, + CBOR_Encoding_Error, + CBOR_FloatAI, CBOR_MajorTypes, CBOR_Object, + CBOR_SimpleValue, + CBOR_UINT64_MAX, CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER, CBOR_BYTE_STRING, CBOR_TEXT_STRING, + CBOR_ARRAY, CBOR_SEMANTIC_TAG, CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, + CBOR_NO_ITEM, CBOR_FLOAT, + CBOR_MAP, + CBOR_SIMPLE_VALUE, ) from scapy.cbor.cborcodec import ( + CBOR_BREAK_BYTE, CBOR_Codec_Decoding_Error, + CBOR_INDEFINITE, CBOR_decode_head, CBOR_encode_head, + CBOR_encode_initial, + cbor_count_items, + cbor_item_span, + cbor_is_break, + cbor_consume_break, CBORcodec_Object, CBORcodec_UNSIGNED_INTEGER, CBORcodec_NEGATIVE_INTEGER, @@ -38,7 +60,8 @@ CBORcodec_TEXT_STRING, CBORcodec_SIMPLE_AND_FLOAT, ) -from scapy.base_classes import BasePacket +from scapy.packet import Packet +from scapy.utils import Enum_metaclass from scapy.volatile import ( RandChoice, RandFloat, @@ -47,10 +70,11 @@ RandField, ) -from scapy import packet +from scapy import packet, fields, config from typing import ( Any, + Callable, Dict, Generic, List, @@ -67,12 +91,110 @@ from scapy.cborpacket import CBOR_Packet # noqa: F401 -class CBORF_badsequence(Exception): - pass +class CBOR_Type_Mismatch(CBOR_Decoding_Error): + """Raised when a CBOR field encounters an unexpected major type.""" + + +@dataclass(frozen=True) +class _CBORBuildResult(object): + """Encoded CBOR bytes and how many top-level items they contain.""" + data: bytes = b"" + items: int = 0 + + +@dataclass(frozen=True) +class _CBORParseResult(object): + """Decoded value, unconsumed input, and items consumed.""" + value: Any = None + remaining: bytes = b"" + items: int = 0 + + +# Sentinel for an optional field that was not present on the wire. +# Distinct from Python ``None``, which encodes CBOR null for CBORF_ANY. +# Identity must survive copy/deepcopy used by Packet default caches. + + +class _CBORAbsent(object): + def __repr__(self): + # type: () -> str + return "CBOR_ABSENT" + + def __copy__(self): + # type: () -> _CBORAbsent + return self + + def __deepcopy__(self, memo): + # type: (dict) -> _CBORAbsent + return self + + +CBOR_ABSENT = _CBORAbsent() + + +def _encode_exactly_one_cbor_item(val, context="value"): + # type: (Any, str) -> bytes + """Serialize *val* and require it to be exactly one well-formed CBOR item. + + Always goes through ``bytes(val)`` so Packet ``post_build`` / payload are + included, then fully decodes to prove single-item cardinality. + """ + data = bytes(val) + try: + _obj, remaining = CBORcodec_Object.decode_cbor_item(data) + except Exception as exc: + raise CBOR_Encoding_Error( + "%s did not encode a well-formed CBOR item: %s" + % (context, exc) + ) + if remaining: + raise CBOR_Encoding_Error( + "%s encoded more than one top-level CBOR item" + % context + ) + return data + + +def _cbor_attach_parent(parent, child): + # type: (Optional[Packet], Any) -> Any + """Attach *child* as a field-contained packet of *parent* (Scapy parent).""" + if child is not None and parent is not None and hasattr(child, "add_parent"): + child.add_parent(parent) + return child class CBORF_element(object): - pass + """Base class for CBOR packet field elements. + + Public API is ``build`` / ``dissect`` (bytes in, bytes out). Item + cardinality for compound budgeting lives in ``_build_counted`` / + ``_dissect_counted``. + """ + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + raise NotImplementedError + + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + raise NotImplementedError + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + return self._build_counted(pkt).data + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + return self._dissect_counted(pkt, s).remaining + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + """Upper bound independent of not-yet-dissected discriminators.""" + return 1 ########################## @@ -80,35 +202,28 @@ class CBORF_element(object): ########################## _I = TypeVar('_I') # Internal storage -_A = TypeVar('_A') # CBOR object -class CBORF_field(CBORF_element, Generic[_I, _A]): +class CBORF_field(CBORF_element, Generic[_I]): + """Base class for CBOR items in packet fields. + + Packet fields store native Python values (``int``, ``bytes``, ``str``, + ``bool``, ``float``, ``list``, ``dict``, ``None``). + """ holds_packets = 0 islist = 0 + ismutable = False CBOR_tag = None # type: Optional[Any] def __init__(self, name, # type: str - default, # type: Optional[_A] + default, # type: Optional[_I] ): # type: (...) -> None self.name = name - if default is None: - self.default = default # type: Optional[_A] - else: - self.default = self._wrap(default) self.owners = [] # type: List[Type[CBOR_Packet]] - - def _wrap(self, val): - # type: (Any) -> _A - """Return a CBOR object wrapping *val*. - - The base implementation is a pass-through cast; subclasses override - this to convert a raw Python value to the appropriate CBOR object - type (e.g. :class:`~scapy.cbor.cbor.CBOR_UNSIGNED_INTEGER`). - """ - return cast(_A, val) + # Mirror Scapy Field: normalize defaults through any2i(). + self.default = self.any2i(None, default) def register_owner(self, cls): # type: (Type[CBOR_Packet]) -> None @@ -122,77 +237,169 @@ def i2h(self, pkt, x): # type: (CBOR_Packet, _I) -> Any return x + def h2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> _I + return cast(_I, x) + def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[_A, bytes] - raise NotImplementedError("Subclasses must implement m2i") + # type: (CBOR_Packet, bytes) -> Tuple[_I, bytes] + raise NotImplementedError( + "Subclasses must implement m2i for %s" % type(self)) def i2m(self, pkt, x): - # type: (CBOR_Packet, Union[bytes, _I, _A]) -> bytes - if x is None: - return b"" - if isinstance(x, CBOR_Object): - return x.enc() - return self._encode(x) - - def _encode(self, x): + # type: (CBOR_Packet, Any) -> bytes + """Convert internal value to CBOR wire bytes (Scapy build hook).""" + if isinstance(x, fields.RawVal): + data = bytes(x) + try: + _obj, remaining = CBORcodec_Object.decode_cbor_item(data) + except Exception as exc: + raise CBOR_Encoding_Error( + "RawVal for %r is not well-formed CBOR: %s" + % (self.name, exc) + ) + if remaining: + raise CBOR_Encoding_Error( + "RawVal for %r must contain exactly one CBOR item" + % self.name + ) + return data + # Do not special-case None here: for CBORF_ANY, None is CBOR null. + # Absent/optional skipping is handled in _build_counted(). + return self._encode_leaf(x) + + def _encode_leaf(self, x): # type: (Any) -> bytes - """Encode a raw Python value to CBOR bytes.""" - raise NotImplementedError("Subclasses must implement _encode") + """Encode a native Python value to CBOR bytes (leaf fields only).""" + raise NotImplementedError( + "Subclasses must implement _encode_leaf for %s" % type(self)) + + @staticmethod + def _object_to_python(obj): + # type: (Any) -> Any + """Convert a :class:`CBOR_Object` tree to native Python values. + + Prefer keeping :class:`CBOR_Object` for arbitrary CBOR (``CBORF_ANY``). + Tags, simples, and undefined stay as ``CBOR_Object`` instances. + """ + if not isinstance(obj, CBOR_Object): + return obj + if isinstance(obj, (CBOR_UNDEFINED, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE)): + return obj + if isinstance(obj, CBOR_ARRAY): + return [CBORF_field._object_to_python(item) for item in obj.val] + if isinstance(obj, CBOR_MAP): + from scapy.cbor.cbor import CBORMapData, _cbor_map_pairs + pairs = _cbor_map_pairs(obj) + return CBORMapData([ + (CBORF_field._object_to_python(k), + CBORF_field._object_to_python(v)) + for k, v in pairs + ]) + if isinstance(obj, CBOR_FLOAT): + return float(obj.val) + return obj.val def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> _I - return cast(_I, x) - - def extract_packet(self, - cls, # type: Type[CBOR_Packet] - s, # type: bytes - _underlayer=None, # type: Optional[CBOR_Packet] - ): - # type: (...) -> Tuple[CBOR_Packet, bytes] - try: - c = cls(s, _underlayer=_underlayer) - except CBORF_badsequence: - c = packet.Raw(s, _underlayer=_underlayer) # type: ignore - cpad = c.getlayer(packet.Raw) - s = b"" - if cpad is not None: - s = cpad.load - if cpad.underlayer: - del cpad.underlayer.payload - return c, s + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return cast(_I, x) + if isinstance(x, CBOR_UNDEFINED): + return cast(_I, x) + if isinstance(x, CBOR_Object): + x = self._object_to_python(x) + return self.h2i(pkt, x) def build(self, pkt): # type: (CBOR_Packet) -> bytes - return self.i2m(pkt, getattr(pkt, self.name)) + """Encode this field's value from *pkt* (ASN.1-style leaf build).""" + val = pkt.getfieldval(self.name) + if val is None: + raise CBOR_Encoding_Error( + "Required field %r is None" % self.name) + return self.i2m(pkt, val) def dissect(self, pkt, s): # type: (CBOR_Packet, bytes) -> bytes - v, s = self.m2i(pkt, s) - self.set_val(pkt, v) - return s + """Decode one item from *s* into *pkt* (ASN.1-style leaf dissect).""" + val, remain = self.m2i(pkt, s) + pkt.setfieldval(self.name, val) + return remain + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + return _CBORBuildResult(self.build(pkt), 1) + + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + remain = self.dissect(pkt, s) + return _CBORParseResult(remaining=remain, items=1) + + def _parse_value(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + """Decode a free value without assigning it onto *pkt*.""" + val, remain = self.m2i(pkt, s) + return _CBORParseResult(value=val, remaining=remain, items=1) + + def _build_value(self, pkt, value): + # type: (CBOR_Packet, Any) -> _CBORBuildResult + """Encode *value* without reading it from *pkt* fields.""" + return _CBORBuildResult( + data=self.i2m(pkt, self.any2i(pkt, value)), + items=1, + ) def do_copy(self, x): # type: (Any) -> Any - if isinstance(x, list): - x = x[:] - for i in range(len(x)): - if isinstance(x[i], BasePacket): - x[i] = x[i].copy() + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return x + if isinstance(x, CBOR_UNDEFINED): return x + if isinstance(x, list): + return [self.do_copy(item) for item in x] + if isinstance(x, dict): + return {key: self.do_copy(value) for key, value in x.items()} if hasattr(x, "copy"): - return x.copy() - return x + try: + return x.copy() + except TypeError: + pass + return copy.deepcopy(x) - def set_val(self, pkt, val): - # type: (CBOR_Packet, Any) -> None - setattr(pkt, self.name, val) + def mark_absent(self, pkt): + # type: (CBOR_Packet) -> None + """Record that this field was not present on the wire. + + Assign ``CBOR_ABSENT`` without ``any2i`` so integer leaves that + reject non-int values still accept the presence sentinel. + """ + pkt.fields[self.name] = CBOR_ABSENT + pkt.explicit = 0 + pkt.raw_packet_cache = None + pkt.raw_packet_cache_fields = None + pkt.wirelen = None def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return getattr(pkt, self.name) is None + val = pkt.getfieldval(self.name) + return val is None or val is CBOR_ABSENT + + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + """Return True if the next CBOR item matches this field's outer type.""" + if not s or cbor_is_break(s): + return False + try: + major_type, _info, _rem = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error: + return False + tag = self.CBOR_tag + if tag is None: + return True + return major_type == int(tag) def get_fields_list(self): - # type: () -> List[CBORF_field[Any, Any]] + # type: () -> List[CBORF_field[Any]] return [self] def __str__(self): @@ -204,190 +411,547 @@ def randval(self): return cast(RandField[_I], RandNum(0, 2 ** 32)) def copy(self): - # type: () -> CBORF_field[_I, _A] + # type: () -> CBORF_field[_I] return copy.copy(self) +class _CBORFingerprintKind(metaclass=Enum_metaclass): + """Private discriminator for ``CBORF_ANY`` raw-cache fingerprints.""" + name = "CBOR_FINGERPRINT_KIND" + UNDEF = 3 + ARRAY = 7 + MAP = 8 + TAG = 9 + OBJ = 11 + NAN = 13 + FINITE = 14 + SENTINEL = 15 + FLOAT = 16 + INF = 17 + ZERO = 18 + MAPDATA = 19 + LIST = 20 + DICT = 21 + PY = 22 + + +_CBORFingerprint = Tuple[Any, ...] + + +class CBORF_ANY(CBORF_field[Any]): + """Represent any well-formed CBOR value as a lossless ``CBOR_Object``.""" + ismutable = True + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + # Python None / CBOR null is a real value; only CBOR_ABSENT means absent. + return pkt.getfieldval(self.name) is CBOR_ABSENT + + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + try: + CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error: + return False + return True + + def do_copy(self, x): # type: ignore[override] + # type: (Any) -> Any + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return x + if isinstance(x, CBOR_UNDEFINED): + return x + return copy.deepcopy(x) + + @staticmethod + def python_to_cbor_object(value): + # type: (Any) -> Any + """Convert native Python values into a :class:`CBOR_Object` tree.""" + from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_BYTE_STRING, + CBOR_FALSE, + CBOR_FLOAT, + CBOR_MAP, + CBOR_NEGATIVE_INTEGER, + CBOR_NULL, + CBOR_TEXT_STRING, + CBOR_TRUE, + CBOR_UNSIGNED_INTEGER, + CBORMapData, + ) + convert = CBORF_ANY.python_to_cbor_object + if isinstance(value, CBOR_Object): + return value + if isinstance(value, CBORMapData): + return CBOR_MAP(CBORMapData([ + (convert(k), convert(v)) + for k, v in value.cbor_pairs() + ])) + if isinstance(value, bool): + return CBOR_TRUE() if value else CBOR_FALSE() + if value is None: + return CBOR_NULL() + if isinstance(value, int): + if value >= 0: + return CBOR_UNSIGNED_INTEGER(value) + return CBOR_NEGATIVE_INTEGER(value) + if isinstance(value, float): + return CBOR_FLOAT(value) + if isinstance(value, bytes): + return CBOR_BYTE_STRING(value) + if isinstance(value, str): + return CBOR_TEXT_STRING(value) + if isinstance(value, list): + return CBOR_ARRAY([convert(item) for item in value]) + if isinstance(value, dict): + return CBOR_MAP(CBORMapData([ + (convert(k), convert(v)) + for k, v in value.items() + ])) + raise TypeError("Cannot convert %r to CBOR_Object" % (type(value),)) + + @staticmethod + def _cache_fingerprint(obj): + # type: (Any) -> _CBORFingerprint + """Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` values.""" + from scapy.cbor.cbor import CBORMapData + fingerprint = CBORF_ANY._cache_fingerprint + Kind = _CBORFingerprintKind + if obj is CBOR_ABSENT or obj is CBOR_NO_ITEM: + return (Kind.SENTINEL, obj) + if isinstance(obj, CBOR_UNDEFINED): + return (Kind.UNDEF,) + if isinstance(obj, CBOR_FLOAT): + fval = float(obj.val) + if math.isnan(fval): + token = (Kind.NAN,) # type: Tuple[Any, ...] + elif math.isinf(fval): + token = (Kind.INF, math.copysign(1.0, fval)) + elif fval == 0.0: + token = (Kind.ZERO, math.copysign(1.0, fval)) + else: + token = (Kind.FINITE, fval) + encoded = getattr(obj, "_encoded", None) + return (Kind.FLOAT, token, encoded) + if isinstance(obj, CBOR_ARRAY): + return ( + Kind.ARRAY, + tuple(fingerprint(item) for item in obj.val), + ) + if isinstance(obj, CBOR_MAP): + from scapy.cbor.cbor import _cbor_map_pairs + pairs = _cbor_map_pairs(obj) + return ( + Kind.MAP, + tuple( + (fingerprint(key), fingerprint(value)) + for key, value in pairs + ), + ) + if isinstance(obj, CBORMapData): + return ( + Kind.MAPDATA, + tuple( + (fingerprint(key), fingerprint(value)) + for key, value in obj.cbor_pairs() + ), + ) + if isinstance(obj, CBOR_SEMANTIC_TAG): + tag_num, inner = obj.val + return (Kind.TAG, int(tag_num), fingerprint(inner)) + if isinstance(obj, CBOR_Object): + return (Kind.OBJ, type(obj).__name__, fingerprint(obj.val)) + if isinstance(obj, list): + return (Kind.LIST, tuple(fingerprint(item) for item in obj)) + if isinstance(obj, dict): + return ( + Kind.DICT, + tuple( + (fingerprint(key), fingerprint(value)) + for key, value in obj.items() + ), + ) + return (Kind.PY, type(obj).__name__, obj) + + def cache_fingerprint(self, x): + # type: (Any) -> _CBORFingerprint + """Snapshot for Scapy mutable raw-cache comparison. + + Includes ``CBOR_FLOAT._encoded`` so explicit ``.val`` assignment that + clears the wire cache is visible even when the semantic float is + unchanged. + """ + return self._cache_fingerprint(x) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> Any + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return x + if isinstance(x, CBOR_UNDEFINED): + return x + return self.python_to_cbor_object(x) + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + val = pkt.getfieldval(self.name) + if val is CBOR_ABSENT: + return b"" + return self.i2m(pkt, val) + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + data = self.build(pkt) + if not data: + return _CBORBuildResult(b"", 0) + return _CBORBuildResult(data, 1) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + return CBORcodec_Object.decode_cbor_item(s) + + def _encode_leaf(self, x): + # type: (Any) -> bytes + if x is CBOR_ABSENT: + return b"" + return CBORcodec_Object.encode_cbor_item(x) + + ############################# # Simple CBOR Fields # ############################# -class CBORF_UNSIGNED_INTEGER(CBORF_field[int, CBOR_UNSIGNED_INTEGER]): +class CBORF_UNSIGNED_INTEGER(CBORF_field[int]): """CBOR unsigned integer field (major type 0).""" CBOR_tag = CBOR_MajorTypes.UNSIGNED_INTEGER - def _wrap(self, val): - # type: (Any) -> CBOR_UNSIGNED_INTEGER - if isinstance(val, CBOR_UNSIGNED_INTEGER): - return val - return CBOR_UNSIGNED_INTEGER(int(val)) + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> int + if isinstance(x, CBOR_Object): + x = x.val + if x is None: + return None # type: ignore + i = int(x) + if i < 0 or i > CBOR_UINT64_MAX: + raise CBOR_Encoding_Error( + "Unsigned integer out of CBOR range: %r" % (i,)) + return i def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_UNSIGNED_INTEGER, bytes] - return CBORcodec_UNSIGNED_INTEGER.dec(s) # type: ignore - - def _encode(self, x): + # type: (CBOR_Packet, bytes) -> Tuple[int, bytes] + obj, remain = CBORcodec_UNSIGNED_INTEGER.dec(s) + if not isinstance(obj, CBOR_UNSIGNED_INTEGER): + raise CBOR_Type_Mismatch( + "Expected unsigned integer, got %r" % obj) + return obj.val, remain + + def _encode_leaf(self, x): # type: (Any) -> bytes - return CBORcodec_UNSIGNED_INTEGER.enc( - x if isinstance(x, CBOR_Object) else CBOR_UNSIGNED_INTEGER(int(x)) - ) + return CBORcodec_UNSIGNED_INTEGER.enc(int(x)) def randval(self): # type: () -> RandNum return RandNum(0, 2 ** 64 - 1) -class CBORF_NEGATIVE_INTEGER(CBORF_field[int, CBOR_NEGATIVE_INTEGER]): +class CBORF_NEGATIVE_INTEGER(CBORF_field[int]): """CBOR negative integer field (major type 1).""" CBOR_tag = CBOR_MajorTypes.NEGATIVE_INTEGER - def _wrap(self, val): - # type: (Any) -> CBOR_NEGATIVE_INTEGER - if isinstance(val, CBOR_NEGATIVE_INTEGER): - return val - return CBOR_NEGATIVE_INTEGER(int(val)) + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> int + if isinstance(x, CBOR_Object): + x = x.val + if x is None: + return None # type: ignore + i = int(x) + if i >= 0 or i < -(CBOR_UINT64_MAX + 1): + raise CBOR_Encoding_Error( + "Negative integer out of CBOR range: %r" % (i,)) + return i def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_NEGATIVE_INTEGER, bytes] - return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore - - def _encode(self, x): + # type: (CBOR_Packet, bytes) -> Tuple[int, bytes] + obj, remain = CBORcodec_NEGATIVE_INTEGER.dec(s) + if not isinstance(obj, CBOR_NEGATIVE_INTEGER): + raise CBOR_Type_Mismatch( + "Expected negative integer, got %r" % obj) + return obj.val, remain + + def _encode_leaf(self, x): # type: (Any) -> bytes - return CBORcodec_NEGATIVE_INTEGER.enc( - x if isinstance(x, CBOR_Object) else CBOR_NEGATIVE_INTEGER(int(x)) - ) + return CBORcodec_NEGATIVE_INTEGER.enc(int(x)) def randval(self): # type: () -> RandNum return RandNum(-2 ** 64, -1) -class CBORF_INTEGER(CBORF_field[int, - Union[CBOR_UNSIGNED_INTEGER, - CBOR_NEGATIVE_INTEGER]]): +class CBORF_INTEGER(CBORF_field[int]): """CBOR integer field handling both positive and negative values.""" - def _wrap(self, val): - # type: (Any) -> Union[CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER] - if isinstance(val, (CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER)): - return val - i = int(val) - if i >= 0: - return CBOR_UNSIGNED_INTEGER(i) - return CBOR_NEGATIVE_INTEGER(i) + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + try: + major_type, _info, _rem = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error: + return False + return major_type in ( + CBOR_MajorTypes.UNSIGNED_INTEGER, + CBOR_MajorTypes.NEGATIVE_INTEGER, + ) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> int + if isinstance(x, CBOR_Object): + x = x.val + if x is None: + return None # type: ignore + i = int(x) + if i < -(CBOR_UINT64_MAX + 1) or i > CBOR_UINT64_MAX: + raise CBOR_Encoding_Error( + "Integer out of CBOR range: %r" % (i,)) + return i def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[Union[CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER], bytes] # noqa: E501 + # type: (CBOR_Packet, bytes) -> Tuple[int, bytes] if not s: raise CBOR_Decoding_Error("Empty CBOR data") major_type = (s[0] >> 5) & 0x7 - if major_type == 0: - return CBORcodec_UNSIGNED_INTEGER.dec(s) # type: ignore - elif major_type == 1: - return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore - raise CBOR_Decoding_Error( + if major_type == CBOR_MajorTypes.UNSIGNED_INTEGER: + obj, remain = CBORcodec_UNSIGNED_INTEGER.dec(s) + return obj.val, remain + elif major_type == CBOR_MajorTypes.NEGATIVE_INTEGER: + obj, remain = CBORcodec_NEGATIVE_INTEGER.dec(s) + return obj.val, remain + raise CBOR_Type_Mismatch( "Expected integer (major type 0 or 1), got %d" % major_type) - def i2m(self, pkt, x): - # type: (CBOR_Packet, Any) -> bytes - if x is None: - return b"" - if isinstance(x, CBOR_Object): - return x.enc() + def _encode_leaf(self, x): + # type: (Any) -> bytes i = int(x) if i >= 0: - return CBORcodec_UNSIGNED_INTEGER.enc(CBOR_UNSIGNED_INTEGER(i)) - return CBORcodec_NEGATIVE_INTEGER.enc(CBOR_NEGATIVE_INTEGER(i)) + return CBORcodec_UNSIGNED_INTEGER.enc(i) + return CBORcodec_NEGATIVE_INTEGER.enc(i) def randval(self): # type: () -> RandNum return RandNum(-2 ** 64, 2 ** 64 - 1) -class CBORF_BYTE_STRING(CBORF_field[bytes, CBOR_BYTE_STRING]): +def _cbor_decode_byte_string(s, definite_only=False): + # type: (bytes, bool) -> Tuple[bytes, bytes] + """Decode one CBOR byte string item; optionally reject indefinite form.""" + if definite_only: + try: + major_type, length, _rem = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if major_type != CBOR_MajorTypes.BYTE_STRING: + raise CBOR_Type_Mismatch( + "Expected byte string, got major type %d" % major_type) + if length is CBOR_INDEFINITE: + raise CBOR_Decoding_Error( + "Indefinite-length byte string not allowed here") + obj, remain = CBORcodec_BYTE_STRING.dec(s) + if not isinstance(obj, CBOR_BYTE_STRING): + raise CBOR_Type_Mismatch( + "Expected byte string, got %r" % obj) + return obj.val, remain + + +def _cbor_encode_byte_string(x): + # type: (Any) -> bytes + """Encode *x* as a definite CBOR byte string item.""" + return CBORcodec_BYTE_STRING.enc(bytes(x)) + + +class CBORF_BYTE_STRING(CBORF_field[bytes]): """CBOR byte string field (major type 2).""" CBOR_tag = CBOR_MajorTypes.BYTE_STRING - def _wrap(self, val): - # type: (Any) -> CBOR_BYTE_STRING - if isinstance(val, CBOR_BYTE_STRING): - return val - return CBOR_BYTE_STRING(bytes(val)) + def __init__(self, + name, # type: str + default, # type: Optional[bytes] + definite_only=False, # type: bool + ): + # type: (...) -> None + super(CBORF_BYTE_STRING, self).__init__(name, default) + self.definite_only = definite_only + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> bytes + if isinstance(x, CBOR_Object): + x = x.val + if x is None: + return None # type: ignore + return bytes(x) def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_BYTE_STRING, bytes] - return CBORcodec_BYTE_STRING.dec(s) # type: ignore + # type: (CBOR_Packet, bytes) -> Tuple[bytes, bytes] + return _cbor_decode_byte_string(s, definite_only=self.definite_only) - def _encode(self, x): + def _encode_leaf(self, x): # type: (Any) -> bytes - return CBORcodec_BYTE_STRING.enc( - x if isinstance(x, CBOR_Object) else CBOR_BYTE_STRING(bytes(x)) - ) + return _cbor_encode_byte_string(x) def randval(self): # type: () -> RandString return RandString(RandNum(0, 1000)) -class CBORF_TEXT_STRING(CBORF_field[str, CBOR_TEXT_STRING]): +class CBORF_BYTE_STRING_PACKET(CBORF_field[Packet]): + """CBOR byte string which wraps another packet field. + + The inner packet may or may not itself be CBOR or CBOR sequence data. + Shares byte-string wire helpers with :class:`CBORF_BYTE_STRING`. + """ + CBOR_tag = CBOR_MajorTypes.BYTE_STRING + holds_packets = 1 + + def __init__(self, + name, # type: str + default, # type: Optional[Packet] + pkt_cls=None, # type: Optional[Type[Packet]] + cls_cb=None, # type: Optional[Callable[[Packet, bytes], Optional[Type[Packet]]]] # noqa: E501 + definite_only=False, # type: bool + ): + # type: (...) -> None + if pkt_cls is None and cls_cb is None: + raise ValueError('Must give one of pkt_cls or cls_cb') + # any2i() needs these during default normalization in super().__init__. + self.pkt_cls = pkt_cls + self.cls_cb = cls_cb + self.definite_only = definite_only + super(CBORF_BYTE_STRING_PACKET, self).__init__(name, default) + + def _decode_packet_value(self, pkt, data): + # type: (CBOR_Packet, bytes) -> Packet + if self.pkt_cls is not None: + pkt_cls = self.pkt_cls + elif self.cls_cb is not None: + pkt_cls = self.cls_cb(pkt, data) + else: + pkt_cls = None + if pkt_cls is None: + return packet.Raw(data) + try: + return pkt_cls(data, _parent=pkt) # type: ignore + except CBOR_Decoding_Error: + raise + except Exception as exc: + if config.conf.debug_dissector: + raise + raise CBOR_Decoding_Error( + "Failed to decode byte-string packet content: %s" % exc + ) from exc + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> Packet + if isinstance(x, CBOR_BYTE_STRING): + x = x.val + if isinstance(x, (bytes, bytearray)): + return self._decode_packet_value(pkt, bytes(x)) + return _cbor_attach_parent(pkt, x) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Packet, bytes] + data, remain = _cbor_decode_byte_string( + s, definite_only=self.definite_only + ) + return self._decode_packet_value(pkt, data), remain + + def _encode_leaf(self, x): + # type: (Any) -> bytes + return _cbor_encode_byte_string(x) + + +class CBORF_TEXT_STRING(CBORF_field[str]): """CBOR text string field (major type 3).""" CBOR_tag = CBOR_MajorTypes.TEXT_STRING - def _wrap(self, val): - # type: (Any) -> CBOR_TEXT_STRING - if isinstance(val, CBOR_TEXT_STRING): - return val - return CBOR_TEXT_STRING(str(val)) + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + if isinstance(x, CBOR_Object): + x = x.val + if x is None: + return None # type: ignore + # Reject bytes: str(b"hi") == "b'hi'", which silently corrupts the value. + if isinstance(x, (bytes, bytearray, memoryview)): + raise TypeError( + "CBOR text string field %r requires str, got %s" + % (self.name, type(x).__name__) + ) + return str(x) def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_TEXT_STRING, bytes] - return CBORcodec_TEXT_STRING.dec(s) # type: ignore - - def _encode(self, x): + # type: (CBOR_Packet, bytes) -> Tuple[str, bytes] + obj, remain = CBORcodec_TEXT_STRING.dec(s) + if not isinstance(obj, CBOR_TEXT_STRING): + raise CBOR_Type_Mismatch( + "Expected text string, got %r" % obj) + return obj.val, remain + + def _encode_leaf(self, x): # type: (Any) -> bytes - return CBORcodec_TEXT_STRING.enc( - x if isinstance(x, CBOR_Object) else CBOR_TEXT_STRING(str(x)) - ) + return CBORcodec_TEXT_STRING.enc(str(x)) def randval(self): # type: () -> RandString return RandString(RandNum(0, 1000)) -class CBORF_BOOLEAN(CBORF_field[bool, Union[CBOR_FALSE, CBOR_TRUE]]): +class CBORF_BOOLEAN(CBORF_field[bool]): """CBOR boolean field (major type 7, simple values 20/21).""" CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT - def _wrap(self, val): - # type: (Any) -> Union[CBOR_FALSE, CBOR_TRUE] - if isinstance(val, (CBOR_FALSE, CBOR_TRUE)): - return val - return CBOR_TRUE() if val else CBOR_FALSE() + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + ai = s[0] & 0x1f + return ( + ((s[0] >> 5) & 0x7) == CBOR_MajorTypes.SIMPLE_AND_FLOAT + and ai in ( + CBOR_SimpleValue.FALSE, + CBOR_SimpleValue.TRUE, + ) + ) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> bool + if x is CBOR_ABSENT: + return CBOR_ABSENT # type: ignore + if x is None: + return None # type: ignore + if isinstance(x, (CBOR_FALSE, CBOR_TRUE)): + return x.val + if isinstance(x, CBOR_Object): + return bool(x.val) + return bool(x) def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[Union[CBOR_FALSE, CBOR_TRUE], bytes] + # type: (CBOR_Packet, bytes) -> Tuple[bool, bytes] obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) if not isinstance(obj, (CBOR_FALSE, CBOR_TRUE)): - raise CBOR_Decoding_Error( + raise CBOR_Type_Mismatch( "Expected boolean (CBOR_FALSE or CBOR_TRUE), got %r" % obj) - return obj, remain # type: ignore + return obj.val, remain - def i2m(self, pkt, x): - # type: (CBOR_Packet, Any) -> bytes - if x is None: - return b"" - if isinstance(x, (CBOR_FALSE, CBOR_TRUE)): - return x.enc() - return CBORcodec_SIMPLE_AND_FLOAT.enc( - CBOR_TRUE() if x else CBOR_FALSE() - ) + def _encode_leaf(self, x): + # type: (Any) -> bytes + return CBORcodec_SIMPLE_AND_FLOAT.enc(bool(x)) def randval(self): # type: () -> RandChoice return RandChoice(True, False) -class CBORF_NULL(CBORF_field[None, CBOR_NULL]): +class CBORF_NULL(CBORF_field[None]): """CBOR null field (major type 7, simple value 22).""" CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT @@ -398,28 +962,51 @@ def __init__(self, # type: (...) -> None super(CBORF_NULL, self).__init__(name, None) - def _wrap(self, val): - # type: (Any) -> CBOR_NULL - return CBOR_NULL() + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + return s[0] == CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.NULL + )[0] + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> None + if x is CBOR_ABSENT: + return CBOR_ABSENT # type: ignore + return None def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_NULL, bytes] + # type: (CBOR_Packet, bytes) -> Tuple[None, bytes] obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) if not isinstance(obj, CBOR_NULL): - raise CBOR_Decoding_Error( + raise CBOR_Type_Mismatch( "Expected null, got %r" % obj) - return obj, remain # type: ignore + return None, remain - def i2m(self, pkt, x): - # type: (CBOR_Packet, Any) -> bytes + def _encode_leaf(self, x): + # type: (Any) -> bytes return CBOR_NULL().enc() + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + if pkt.getfieldval(self.name) is CBOR_ABSENT: + return b"" + return self._encode_leaf(None) + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + data = self.build(pkt) + if not data: + return _CBORBuildResult(b"", 0) + return _CBORBuildResult(data, 1) + def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return False + return pkt.getfieldval(self.name) is CBOR_ABSENT -class CBORF_UNDEFINED(CBORF_field[None, CBOR_UNDEFINED]): +class CBORF_UNDEFINED(CBORF_field[None]): """CBOR undefined field (major type 7, simple value 23).""" CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT @@ -430,52 +1017,104 @@ def __init__(self, # type: (...) -> None super(CBORF_UNDEFINED, self).__init__(name, None) - def _wrap(self, val): - # type: (Any) -> CBOR_UNDEFINED - return CBOR_UNDEFINED() + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + return s[0] == CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.UNDEFINED + )[0] + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> None + if x is CBOR_ABSENT: + return CBOR_ABSENT # type: ignore + return None def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_UNDEFINED, bytes] + # type: (CBOR_Packet, bytes) -> Tuple[None, bytes] obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) if not isinstance(obj, CBOR_UNDEFINED): - raise CBOR_Decoding_Error( + raise CBOR_Type_Mismatch( "Expected undefined, got %r" % obj) - return obj, remain # type: ignore + return None, remain - def i2m(self, pkt, x): - # type: (CBOR_Packet, Any) -> bytes + def _encode_leaf(self, x): + # type: (Any) -> bytes return CBOR_UNDEFINED().enc() + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + if pkt.getfieldval(self.name) is CBOR_ABSENT: + return b"" + return self._encode_leaf(None) + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + data = self.build(pkt) + if not data: + return _CBORBuildResult(b"", 0) + return _CBORBuildResult(data, 1) + def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return False + return pkt.getfieldval(self.name) is CBOR_ABSENT -class CBORF_FLOAT(CBORF_field[float, CBOR_FLOAT]): - """CBOR float field (major type 7, double precision).""" +class CBORF_FLOAT(CBORF_field[float]): + """CBOR float field (major type 7). + + Stores a plain Python ``float``. Exact received encodings are preserved + only while the packet ``raw_packet_cache`` remains valid; after semantic + rebuild, preferred (shortest exact) encoding is used. + """ CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT - def _wrap(self, val): - # type: (Any) -> CBOR_FLOAT - if isinstance(val, CBOR_FLOAT): - return val - return CBOR_FLOAT(float(val)) + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + ai = s[0] & 0x1f + return ( + ((s[0] >> 5) & 0x7) == CBOR_MajorTypes.SIMPLE_AND_FLOAT + and ai in ( + CBOR_FloatAI.HALF, + CBOR_FloatAI.SINGLE, + CBOR_FloatAI.DOUBLE, + ) + ) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> float + if x is CBOR_ABSENT: + return CBOR_ABSENT # type: ignore + if x is None: + return None # type: ignore + if isinstance(x, CBOR_FLOAT): + return float(x.val) + if isinstance(x, CBOR_Object): + return float(self._object_to_python(x)) + return float(x) def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_FLOAT, bytes] + # type: (CBOR_Packet, bytes) -> Tuple[float, bytes] obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) if not isinstance(obj, CBOR_FLOAT): - raise CBOR_Decoding_Error( + raise CBOR_Type_Mismatch( "Expected float, got %r" % obj) - return obj, remain # type: ignore + return float(obj.val), remain - def i2m(self, pkt, x): - # type: (CBOR_Packet, Any) -> bytes - if x is None: - return b"" - if isinstance(x, CBOR_FLOAT): - return x.enc() - return CBORcodec_SIMPLE_AND_FLOAT.enc(CBOR_FLOAT(float(x))) + def _encode_leaf(self, x): + # type: (Any) -> bytes + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(x)) + + def i2h(self, pkt, x): + # type: (CBOR_Packet, Any) -> Any + return x + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + return repr(x) def randval(self): # type: () -> RandFloat @@ -486,33 +1125,14 @@ def randval(self): # Structured CBOR Fields # ############################## -class CBORF_ARRAY(CBORF_field[List[Any], List[Any]]): - """ - CBOR array with a fixed sequence of named, typed fields (major type 4). - Analogous to ASN1F_SEQUENCE: each positional element corresponds to a - specific CBORF_field. The CBOR array count must match the number of - declared fields. - - Example:: - class MyCBOR(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("version", 1), - CBORF_TEXT_STRING("name", ""), - ) - """ - CBOR_tag = CBOR_MajorTypes.ARRAY +class _CBORF_compound(CBORF_element): + """Shared helpers for sequence-like CBOR field containers.""" + CBOR_tag = None holds_packets = 1 def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - # The array itself is a structural field without its own named slot on - # the packet; a placeholder name is used so the base class __init__ - # stays happy. Individual element fields are the ones that carry names. - name = "_cbor_array" - default = [field.default for field in seq] - super(CBORF_ARRAY, self).__init__(name, None) - self.default = default self.seq = seq self.islist = len(seq) > 1 @@ -525,94 +1145,534 @@ def is_empty(self, pkt): return all(f.is_empty(pkt) for f in self.seq) def get_fields_list(self): - # type: () -> List[CBORF_field[Any, Any]] - return reduce(lambda x, y: x + y.get_fields_list(), - self.seq, []) + # type: () -> List[CBORF_field[Any]] + fields_list = [ + child + for field in self.seq + for child in field.get_fields_list() + ] + names = [f.name for f in fields_list] + if len(names) != len(set(names)): + dupes = sorted({n for n in names if names.count(n) > 1}) + raise ValueError( + "Duplicate CBOR field name(s) %s; for multiple maps use " + "distinct unknown_field= values" % (dupes,) + ) + return fields_list + + def _build_children(self, pkt): + # type: (CBOR_Packet) -> Tuple[bytes, int] + parts = [] # type: List[bytes] + total_items = 0 + for field in self.seq: + result = field._build_counted(pkt) + parts.append(result.data) + total_items += result.items + return b"".join(parts), total_items + + def _dissect_field(self, pkt, field, remaining, max_items=None): + # type: (CBOR_Packet, Any, bytes, Optional[int]) -> _CBORParseResult + if isinstance(field, CBORF_REMAINDER_OF): + return field._dissect_counted( + pkt, remaining, max_items=max_items + ) + return field._dissect_counted(pkt, remaining) - def m2i(self, pkt, s): - # type: (Any, bytes) -> Tuple[Any, bytes] - """ - Decode a CBOR array. Each element is decoded by its corresponding - field in ``self.seq``. The decoded values are set directly on the - packet by each field's ``dissect`` call, so this method returns an - empty list (which is discarded by ``dissect``). + def _reject_nonterminal_remainder_of(self, allow_terminal=True): + # type: (bool) -> None + """Reject ``CBORF_REMAINDER_OF`` that is not a direct final child of *self*. + + Nested unframed ``CBORF_ITEMS`` share this framing context, so they + recurse with ``allow_terminal=False``. Framed ``CBORF_ARRAY`` + compounds establish their own item budget and are not walked. """ + for i, field in enumerate(self.seq): + is_last = i == len(self.seq) - 1 + if isinstance(field, CBORF_REMAINDER_OF): + if not (allow_terminal and is_last): + raise ValueError( + "CBORF_REMAINDER_OF must be the last field " + "in the sequence" + ) + elif isinstance(field, CBORF_ITEMS): + # Only unframed ITEMS share this framing context; framed + # ARRAY establishes its own count boundary. + field._reject_nonterminal_remainder_of(allow_terminal=False) + + +class CBORF_ITEMS(_CBORF_compound): + """ + Unframed fixed sequence of named, typed fields (no CBOR array head). + + Unlike :class:`CBORF_ARRAY`, this emits/consumes a stream of top-level + CBOR items with greedy left-to-right parsing and no suffix lookahead. + Use it when a schema is a field list without a major-type-4 envelope + (ASN.1 SEQUENCE analogy belongs on :class:`CBORF_ARRAY`). + + Same-type optional-then-required schemas are ambiguous on the wire; + prefer :class:`CBORF_ARRAY` (item budget) or :class:`CBORF_CONDITIONAL` + with a previously decoded discriminator. + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + super(CBORF_ITEMS, self).__init__(*seq, **kwargs) + self._reject_nonterminal_remainder_of() + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + data, total_items = self._build_children(pkt) + return _CBORBuildResult(data, total_items) + + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + # Stream schema fields greedily left-to-right; leave trailing bytes + # for the parent without requiring them to be well-formed CBOR. + remaining = s + total_items = 0 + for field in self.seq: + result = self._dissect_field(pkt, field, remaining) + remaining = result.remaining + total_items += result.items + return _CBORParseResult(remaining=remaining, items=total_items) + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return sum(f.min_items(pkt) for f in self.seq) + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return sum(f.structural_max_items(pkt) for f in self.seq) + + +class CBORF_ARRAY(_CBORF_compound): + """ + CBOR array with a fixed sequence of named, typed fields (major type 4). + + Analogous to ASN1F_SEQUENCE: each positional element is a + :class:`CBORF_field`, wrapped in one definite (or indefinite) CBOR array. + Prefer this over :class:`CBORF_ITEMS` when the wire form is a single + array item. + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + CBOR_tag = CBOR_MajorTypes.ARRAY + + encode_indefinite = False + """Set to true to encode using indefinite length.""" + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + super(CBORF_ARRAY, self).__init__(*seq, **kwargs) + self._reject_nonterminal_remainder_of() + + def _dissect_children_budgeted(self, pkt, s, count): + # type: (CBOR_Packet, bytes, int) -> bytes + remaining = s + items_left = count + for index, field in enumerate(self.seq): + reserved = sum( + f.min_items(pkt) for f in self.seq[index + 1:] + ) + available = items_left - reserved + needed = field.min_items(pkt) + if available < 0 or available < needed: + raise CBOR_Decoding_Error("CBOR item count mismatch") + if available == 0: + if isinstance(field, CBORF_optional): + field._field.mark_absent(pkt) + continue + result = self._dissect_field( + pkt, field, remaining, max_items=available + ) + if result.items > items_left: + raise CBOR_Decoding_Error( + "CBOR field consumed more items than remaining" + ) + remaining = result.remaining + items_left -= result.items + if items_left != 0: + raise CBOR_Decoding_Error("CBOR item count mismatch") + return remaining + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + items_data, total_items = self._build_children(pkt) + if self.encode_indefinite: + data = ( + CBOR_encode_initial( + CBOR_MajorTypes.ARRAY, CBOR_AdditionalInfo.INDEFINITE + ) + + items_data + + bytes([CBOR_BREAK_BYTE]) + ) + else: + data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, total_items) + data += items_data + return _CBORBuildResult(data, 1) + + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult try: - major_type, count, s = CBOR_decode_head(s) + major_type, count, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) - if major_type != 4: - raise CBOR_Decoding_Error( + if major_type != CBOR_MajorTypes.ARRAY: + raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) - if count != len(self.seq): - raise CBOR_Decoding_Error( - "Array length mismatch: expected %d, got %d" % - (len(self.seq), count)) - for obj in self.seq: + if count is CBOR_INDEFINITE: + # Lightweight head/span walk — avoid building CBOR_Object trees + # just to learn the item budget before the schema pass. + child_max = sum( + f.structural_max_items(pkt) for f in self.seq + ) try: - s = obj.dissect(pkt, s) - except CBORF_badsequence: - break - return [], s + item_count = cbor_count_items( + remaining, + max_count=child_max + 1, + until_break=True, + ) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if item_count > child_max: + raise CBOR_Decoding_Error("CBOR item count mismatch") + remaining = self._dissect_children_budgeted( + pkt, remaining, item_count + ) + try: + remaining = cbor_consume_break(remaining) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + else: + remaining = self._dissect_children_budgeted( + pkt, remaining, count + ) + return _CBORParseResult(remaining=remaining, items=1) - def dissect(self, pkt, s): - # type: (Any, bytes) -> bytes - _, x = self.m2i(pkt, s) - return x - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - items = b"".join(obj.build(pkt) for obj in self.seq) - return CBOR_encode_head(4, len(self.seq)) + items +class CBORF_ARRAY_INDEFINITE(CBORF_ARRAY): + """A field to act as an array but to always encode to indefinite-length.""" + + encode_indefinite = True _ARRAY_T = Union[ - 'CBOR_Packet', - Type[CBORF_field[Any, Any]], + Type[Packet], + Type['CBORF_field[Any]'], 'CBORF_PACKET', - CBORF_field[Any, Any], + 'CBORF_field[Any]', ] -class CBORF_ARRAY_OF(CBORF_field[List[_ARRAY_T], List[CBOR_Object[Any]]]): +class _CBORF_HOMOGENEOUS(CBORF_field[List[Any]]): + """Shared machinery for homogeneous CBOR collections.""" + islist = 1 + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + # Collections are not leaf encoders; use counted compound build. + return self._build_counted(pkt).data + + def __init__(self, + name, # type: str + default, # type: Any + pkt_cls=None, # type: _ARRAY_T + next_cls_cb=None, # type: Optional[Callable[..., Optional[Type[Packet]]]] # noqa: E501 + max_count=None, # type: Optional[int] + ): + # type: (...) -> None + self.cls = None + self.item_field = None + self.holds_packets = 0 + self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] + self.max_count = max_count + if next_cls_cb is not None: + if pkt_cls is not None: + raise ValueError( + "Pass only next_cls_cb, or only pkt_cls" + ) + self.next_cls_cb = next_cls_cb + self.holds_packets = 1 + elif pkt_cls is None: + raise ValueError("Provide pkt_cls or next_cls_cb") + elif ( + (isinstance(pkt_cls, type) and issubclass(pkt_cls, CBORF_field)) + or isinstance(pkt_cls, CBORF_field) + ): + if isinstance(pkt_cls, type): + self.item_field = pkt_cls("_item", None) # type: ignore + else: + self.item_field = pkt_cls + # Packet-valued element fields must register as packet storage + # even though decode/encode still go through item_field. + self.holds_packets = 1 if getattr( + self.item_field, "holds_packets", False + ) else 0 + else: + self.cls = self._require_packet_cls(pkt_cls) + self.holds_packets = 1 + super(_CBORF_HOMOGENEOUS, self).__init__(name, default) + + def cache_fingerprint(self, x): + # type: (Any) -> Any + """Compose item fingerprints when the element field provides them. + + Packet-valued collections return ``None`` so the parent packet uses + nested ``_raw_packet_cache_field_value`` composition. + """ + if self.holds_packets or self.item_field is None or x is None: + return None + item_fp = getattr(self.item_field, "cache_fingerprint", None) + if item_fp is None: + return None + return tuple(item_fp(item) for item in x) + + @staticmethod + def _require_packet_cls(pkt_cls): + # type: (Any) -> Type[CBOR_Packet] + """Validate a CBOR_Packet subclass with a non-None CBOR_root.""" + from scapy.cborpacket import CBOR_Packet + if ( + isinstance(pkt_cls, type) + and issubclass(pkt_cls, CBOR_Packet) + and getattr(pkt_cls, "CBOR_root", None) is not None + ): + return cast("Type[CBOR_Packet]", pkt_cls) + raise ValueError( + "pkt_cls must be a CBOR_Packet subclass with CBOR_root" + ) + + def _list_limit(self): + # type: () -> int + if self.max_count is not None: + return self.max_count + return config.conf.max_list_count + + def _check_list_limit(self, consumed): + # type: (int) -> None + limit = self._list_limit() + if consumed >= limit: + raise CBOR_Decoding_Error( + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, limit) + ) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> List[Any] + if x is None: + return None # type: ignore + if self.item_field is not None: + return [self.item_field.any2i(pkt, item) for item in x] + items = list(x) + for item in items: + _cbor_attach_parent(pkt, item) + return items + + def _decode_element(self, pkt, s, values=None): + # type: (CBOR_Packet, bytes, Optional[List[Any]]) -> Tuple[Any, bytes] + if self.item_field is not None: + result = self.item_field._parse_value(pkt, s) + if result.items != 1: + raise CBOR_Decoding_Error( + "%s element must consume exactly one item" + % self.__class__.__name__ + ) + return result.value, result.remaining + pkt_cls = self.cls + if self.next_cls_cb is not None: + values = values if values is not None else [] + pkt_cls = self.next_cls_cb( + pkt, + values, + values[-1] if values else None, + s, + ) + if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: + return CBOR_NO_ITEM, s + pkt_cls = self._require_packet_cls(pkt_cls) + item_bytes, remaining = cbor_item_span(s) + try: + child = pkt_cls(item_bytes, _parent=pkt) # type: ignore + except CBOR_Decoding_Error: + raise + except Exception as exc: + if config.conf.debug_dissector: + raise + raise CBOR_Decoding_Error(str(exc)) + return child, remaining + + def _encode_element(self, pkt, item): + # type: (CBOR_Packet, Any) -> bytes + if self.item_field is not None: + result = self.item_field._build_value(pkt, item) + if result.items != 1: + raise CBOR_Encoding_Error( + "%s element must emit exactly one item" + % self.__class__.__name__ + ) + return result.data + return _encode_exactly_one_cbor_item( + item, context="%s element" % self.__class__.__name__ + ) + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + if self.item_field is None: + return repr(x) + if x is None: + return self._empty_repr + return self._open_repr + ", ".join( + self.item_field.i2repr(pkt, item) for item in x + ) + self._close_repr + + def __repr__(self): + # type: () -> str + return "<%s %s>" % (self.__class__.__name__, self.name) + + +class CBORF_REMAINDER_OF(_CBORF_HOMOGENEOUS): + """ + Unframed sequence of homogeneous elements (no CBOR array head). + + Preferred constructors (ASN1F_SEQUENCE_OF / PacketListField style):: + + CBORF_REMAINDER_OF("items", [], pkt_cls=MyPacket) + CBORF_REMAINDER_OF("items", [], pkt_cls=CBORF_UNSIGNED_INTEGER) + CBORF_REMAINDER_OF("items", [], next_cls_cb=choose_next) + + ``pkt_cls`` may be a :class:`CBOR_Packet` subclass or a + :class:`CBORF_field` class/instance. Do not use a ``cls=`` keyword: + :class:`~typing.Generic` reserves that name on Python 3.7. + Pass only one of ``pkt_cls`` / ``next_cls_cb``. + + ``CBORF_REMAINDER_OF`` represents an unframed greedy tail of zero or more + CBOR items. Because it consumes the remaining item budget/input, it must + be a direct final child of a positional ``CBORF_ITEMS`` or + ``CBORF_ARRAY``. + + It must not be wrapped in ``CBORF_optional``, ``CBORF_CONDITIONAL``, or + ``CBORF_SEMANTIC_TAG``. Use ``max_count`` to cap decoding (defaults to + ``conf.max_list_count``). + """ + CBOR_tag = None + _empty_repr = "()" + _open_repr = "(" + _close_repr = ")" + + def __init__(self, + name, # type: str + default, # type: Any + pkt_cls=None, # type: _ARRAY_T + next_cls_cb=None, # type: Optional[Callable[..., Optional[Type[Packet]]]] # noqa: E501 + max_count=None, # type: Optional[int] + ): + # type: (...) -> None + super(CBORF_REMAINDER_OF, self).__init__( + name, + default, + pkt_cls=pkt_cls, + next_cls_cb=next_cls_cb, + max_count=max_count, + ) + + def _decode_items(self, pkt, data, max_items=None): + # type: (CBOR_Packet, bytes, Optional[int]) -> Tuple[List[Any], bytes, int] + """Decode zero or more immediate CBOR items; do not consume break.""" + values = [] # type: List[Any] + remaining = data + consumed = 0 + while remaining and not cbor_is_break(remaining): + if max_items is not None and consumed >= max_items: + break + self._check_list_limit(consumed) + before_len = len(remaining) + item, next_remaining = self._decode_element( + pkt, remaining, values=values + ) + if item is CBOR_NO_ITEM: + break + if len(next_remaining) >= before_len: + raise CBOR_Decoding_Error( + "Sequence decoder did not consume input") + values.append(item) + consumed += 1 + remaining = next_remaining + return values, remaining, consumed + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[List[Any], bytes] + values, remaining, _consumed = self._decode_items(pkt, s) + return values, remaining + + def _dissect_counted(self, pkt, s, max_items=None): + # type: (CBOR_Packet, bytes, Optional[int]) -> _CBORParseResult + values, remaining, consumed = self._decode_items( + pkt, s, max_items=max_items + ) + pkt.setfieldval(self.name, values) + return _CBORParseResult(remaining=remaining, items=consumed) + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + val = pkt.getfieldval(self.name) + if val is None: + raise CBOR_Encoding_Error( + "Required collection field %r is None" % self.name) + parts = [self._encode_element(pkt, item) for item in val] + return _CBORBuildResult(b"".join(parts), len(val)) + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self._list_limit() + + +class CBORF_ARRAY_OF(_CBORF_HOMOGENEOUS): """ CBOR array of homogeneous elements (major type 4). - Analogous to ASN1F_SEQUENCE_OF: variable-length array where every - element shares the same type, specified by ``cls``. - ``cls`` may be a :class:`CBORF_field` class/instance (leaf type) or a - :class:`CBOR_Packet` subclass (structured type). + Preferred constructors:: + + CBORF_ARRAY_OF("items", [], pkt_cls=MyPacket) + CBORF_ARRAY_OF("items", [], pkt_cls=CBORF_UNSIGNED_INTEGER) + + ``pkt_cls`` may be a :class:`CBOR_Packet` subclass or a + :class:`CBORF_field` class/instance. Do not use a ``cls=`` keyword: + :class:`~typing.Generic` reserves that name on Python 3.7. + Use ``max_count`` to cap decoding (defaults to ``conf.max_list_count``). """ CBOR_tag = CBOR_MajorTypes.ARRAY - islist = 1 + _empty_repr = "[]" + _open_repr = "[" + _close_repr = "]" def __init__(self, name, # type: str default, # type: Any - cls, # type: _ARRAY_T + pkt_cls=None, # type: _ARRAY_T + max_count=None, # type: Optional[int] ): # type: (...) -> None - if isinstance(cls, type) and issubclass(cls, CBORF_field) or \ - isinstance(cls, CBORF_field): - if isinstance(cls, type): - self.fld = cls("_item", None) # type: ignore - else: - self.fld = cls - self._extract_item = lambda s, pkt: self.fld.m2i(pkt, s) - self.holds_packets = 0 - elif hasattr(cls, "CBOR_root") or callable(cls): - self.cls = cast("Type[CBOR_Packet]", cls) - self._extract_item = lambda s, pkt: self.extract_packet( - self.cls, s, _underlayer=pkt) - self.holds_packets = 1 - else: - raise ValueError("cls must be a CBORF_field or CBOR_Packet") - super(CBORF_ARRAY_OF, self).__init__(name, None) - self.default = default - - def is_empty(self, pkt): - # type: (CBOR_Packet) -> bool - return CBORF_field.is_empty(self, pkt) + super(CBORF_ARRAY_OF, self).__init__( + name, default, pkt_cls=pkt_cls, max_count=max_count + ) def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[List[Any], bytes] @@ -620,48 +1680,112 @@ def m2i(self, pkt, s): major_type, count, s = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) - if major_type != 4: - raise CBOR_Decoding_Error( + if major_type != CBOR_MajorTypes.ARRAY: + raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) - lst = [] - for _ in range(count): - c, s = self._extract_item(s, pkt) # type: ignore - if c is not None: - lst.append(c) + lst = [] # type: List[Any] + if count is CBOR_INDEFINITE: + while True: + if cbor_is_break(s): + s = cbor_consume_break(s) + break + self._check_list_limit(len(lst)) + item, s = self._decode_element(pkt, s) + lst.append(item) + else: + if count > self._list_limit(): + raise CBOR_Decoding_Error( + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, self._list_limit()) + ) + for _ in range(count): + item, s = self._decode_element(pkt, s) + lst.append(item) return lst, s - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - val = getattr(pkt, self.name) + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + val = pkt.getfieldval(self.name) if val is None: - val = [] - items = b"".join(bytes(item) for item in val) - return CBOR_encode_head(4, len(val)) + items + raise CBOR_Encoding_Error( + "Required collection field %r is None" % self.name) + parts = [self._encode_element(pkt, item) for item in val] + data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(val)) + data += b"".join(parts) + return _CBORBuildResult(data, 1) - def i2repr(self, pkt, x): - # type: (CBOR_Packet, Any) -> str - if self.holds_packets: - return repr(x) - elif x is None: - return "[]" - else: - return "[%s]" % ", ".join( - self.fld.i2repr(pkt, item) for item in x # type: ignore - ) - def __repr__(self): - # type: () -> str - return "<%s %s>" % (self.__class__.__name__, self.name) +class _CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): + """Per-map storage for unknown text-key extension pairs. + + Not a CBOR wire field by itself: owning :class:`CBORF_MAP` instances read + and write this packet field around known members. + """ + ismutable = True + islist = 1 + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> List[Tuple[str, Any]] + if x is None or x is CBOR_ABSENT: + return [] + return list(x) + + def do_copy(self, x): # type: ignore[override] + # type: (Any) -> Any + return copy.deepcopy(x) + + def cache_fingerprint(self, x): + # type: (Any) -> _CBORFingerprint + """Wire-sensitive fingerprint for unknown text-key extension pairs.""" + if not x: + return () + fingerprint = CBORF_ANY._cache_fingerprint + return tuple( + (fingerprint(key), fingerprint(value)) + for key, value in x + ) + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return not pkt.getfieldval(self.name) + + def _encode_leaf(self, x): + # type: (Any) -> bytes + raise CBOR_Encoding_Error( + "_CBORF_MAP_UNKNOWN is not encoded as a standalone CBOR item" + ) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + raise CBOR_Decoding_Error( + "_CBORF_MAP_UNKNOWN is not decoded as a standalone CBOR item" + ) -class CBORF_MAP(CBORF_field[Dict[str, Any], Dict[str, Any]]): + +class CBORF_MAP(CBORF_element): """ CBOR map with a fixed set of named, typed fields (major type 5). + This is a **JSON-like named-field** schema helper, not a general CBOR map + codec: keys must be CBOR text strings (the field ``name``, or unknown + extension names). Integer / byte-string / other key types are rejected. + Protocols that need arbitrary CBOR map keys should use :class:`CBORF_ANY` + or a dedicated field. + Each field in ``seq`` represents one key-value pair. The key is the field's ``name`` encoded as a CBOR text string. The value is encoded and decoded by the corresponding :class:`CBORF_field`. + On encode, pairs are emitted in RFC 8949 core-deterministic order + (sorted by encoded key bytes), independent of declaration order. + + Unknown received key/value pairs are retained in a dedicated packet field + (``unknown_field``, default ``"_cbor_unknown"``) as ordered ``(key, value)`` + pairs. While the packet raw cache is valid the exact received bytes are + preserved; after any mutation unknown members are re-encoded using + core-deterministic CBOR together with known fields. Schemas with more than + one map must pass distinct ``unknown_field=`` names. + Example:: class MyCBOR(CBOR_Packet): @@ -672,19 +1796,35 @@ class MyCBOR(CBOR_Packet): """ CBOR_tag = CBOR_MajorTypes.MAP holds_packets = 1 + islist = 1 def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - # The map itself is a structural field without its own named slot on - # the packet; a placeholder name is used so the base class __init__ - # stays happy. Individual value fields are the ones that carry names - # (which also serve as the CBOR text-string keys in the wire encoding). - name = "_cbor_map" - default = {field.name: field.default for field in seq} - super(CBORF_MAP, self).__init__(name, None) - self.default = default + unknown_field = kwargs.pop("unknown_field", "_cbor_unknown") + if kwargs: + raise TypeError( + "CBORF_MAP() got unexpected keyword arguments: %s" + % ", ".join(sorted(kwargs)) + ) self.seq = seq - self.islist = 1 + field_by_name = {} # type: Dict[str, Any] + encoded_keys = {} # type: Dict[str, bytes] + for fld in seq: + name = fld.name + if name in field_by_name: + raise ValueError( + "Duplicate CBOR map field name: %r" % (name,) + ) + field_by_name[name] = fld + encoded_keys[name] = CBORcodec_TEXT_STRING.enc(name) + self._field_by_name = field_by_name + self._encoded_keys = encoded_keys + if unknown_field in field_by_name: + raise ValueError( + "CBORF_MAP unknown_field %r collides with a known member" + % (unknown_field,) + ) + self._unknown_field = _CBORF_MAP_UNKNOWN(unknown_field, []) def __repr__(self): # type: () -> str @@ -695,119 +1835,290 @@ def is_empty(self, pkt): return all(f.is_empty(pkt) for f in self.seq) def get_fields_list(self): - # type: () -> List[CBORF_field[Any, Any]] - return reduce(lambda x, y: x + y.get_fields_list(), - self.seq, []) - - def m2i(self, pkt, s): - # type: (Any, bytes) -> Tuple[Any, bytes] - """ - Decode a CBOR map. Keys are decoded as CBOR items and matched to - fields by name. Values are decoded by the matching field. Unknown - keys are silently skipped. - """ + # type: () -> List[CBORF_field[Any]] + return [ + child + for field in self.seq + for child in field.get_fields_list() + ] + [self._unknown_field] + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + # Emit pairs sorted by encoded key bytes (RFC 8949 core deterministic). + pairs = [] # type: List[Tuple[bytes, bytes]] + for fld in self.seq: + value_result = fld._build_counted(pkt) + if value_result.items == 0: + continue + if value_result.items != 1: + raise CBOR_Encoding_Error( + "CBOR map value for %r must emit exactly one item" + % fld.name + ) + pairs.append((self._encoded_keys[fld.name], value_result.data)) + known_names = set(self._field_by_name) + seen_unknown = set() # type: set[str] + unknown = pkt.getfieldval(self._unknown_field.name) or [] + for key, value in unknown: + if not isinstance(key, str): + raise CBOR_Encoding_Error( + "CBOR map unknown key must be a text string, got %r" + % (key,) + ) + if key in known_names or key in seen_unknown: + raise CBOR_Encoding_Error( + "Duplicate CBOR map key: %r" % (key,) + ) + seen_unknown.add(key) + key_bytes = CBORcodec_TEXT_STRING.enc(key) + value_bytes = CBORcodec_Object.encode_cbor_item_deterministic(value) + pairs.append((key_bytes, value_bytes)) + pairs.sort(key=lambda item: item[0]) + parts = [] # type: List[bytes] + for key_bytes, value_bytes in pairs: + parts.append(key_bytes) + parts.append(value_bytes) + data = CBOR_encode_head(CBOR_MajorTypes.MAP, len(pairs)) + b"".join(parts) + return _CBORBuildResult(data, 1) + + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult try: - major_type, count, s = CBOR_decode_head(s) + major_type, count, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) - if major_type != 5: - raise CBOR_Decoding_Error( + if major_type != CBOR_MajorTypes.MAP: + raise CBOR_Type_Mismatch( "Expected major type 5 (map), got %d" % major_type) - # Build a lookup from field name to field object. - field_map = {f.name: f for f in self.seq} - for _ in range(count): - # Decode the key (any CBOR type; convert to str for lookup). - key_obj, s = CBORcodec_Object.decode_cbor_item(s) - if isinstance(key_obj, CBOR_Object): - key = str(key_obj.val) + + field_map = self._field_by_name + seen_keys = set() # type: set[str] + seen_fields = set() # type: set[str] + pair_values = {} # type: Dict[str, bytes] + unknown_pairs = [] # type: List[Tuple[str, Any]] + + def _collect_pair(): + # type: () -> None + nonlocal remaining + try: + key_obj, after_key = CBORcodec_Object.decode_cbor_item(remaining) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if not isinstance(key_obj, CBOR_TEXT_STRING): + raise CBOR_Decoding_Error( + "CBOR map field key must be a text string, got %r" + % (key_obj,) + ) + key = key_obj.val + if key in seen_keys: + raise CBOR_Decoding_Error( + "Duplicate CBOR map field name: %r" % (key,) + ) + seen_keys.add(key) + if key in field_map: + try: + val_bytes, remaining = cbor_item_span(after_key) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + pair_values[key] = val_bytes else: - key = str(key_obj) - fld = field_map.get(key) - if fld is not None: - s = fld.dissect(pkt, s) + try: + val_obj, remaining = CBORcodec_Object.decode_cbor_item( + after_key + ) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + unknown_pairs.append((key, val_obj)) + + limit = config.conf.max_list_count + if count is CBOR_INDEFINITE: + while True: + if cbor_is_break(remaining): + remaining = cbor_consume_break(remaining) + break + if len(seen_keys) >= limit: + raise CBOR_Decoding_Error( + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, limit) + ) + _collect_pair() + else: + if count > limit: + raise CBOR_Decoding_Error( + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, limit) + ) + for _ in range(count): + _collect_pair() + + def _dissect_value_bytes(fld, val_bytes): + # type: (Any, bytes) -> None + if isinstance(fld, CBORF_optional): + value_fld = fld._field + elif isinstance(fld, CBORF_CONDITIONAL): + value_fld = fld.fld else: - # Skip unknown value. - _unknown, s = CBORcodec_Object.decode_cbor_item(s) - return [], s - - def dissect(self, pkt, s): - # type: (Any, bytes) -> bytes - _, x = self.m2i(pkt, s) - return x + value_fld = fld + result = value_fld._dissect_counted(pkt, val_bytes) + if result.items != 1 or result.remaining: + raise CBOR_Decoding_Error( + "Map value for %r must contain exactly one item" + % getattr(value_fld, "name", value_fld) + ) + seen_fields.add(value_fld.name) + + # Phase 1: unconditional members (order-independent). + for fld in self.seq: + if isinstance(fld, CBORF_CONDITIONAL): + continue + name = fld.name + if name not in pair_values: + if isinstance(fld, CBORF_optional): + fld._field.mark_absent(pkt) + continue + _dissect_value_bytes(fld, pair_values[name]) + + # Phase 2: conditionals after discriminators are populated. + for fld in self.seq: + if not isinstance(fld, CBORF_CONDITIONAL): + continue + name = fld.fld.name + if name not in pair_values: + continue + if not fld._evalcond(pkt): + raise CBOR_Decoding_Error( + "Map field %r present but condition is false" % name + ) + _dissect_value_bytes(fld, pair_values[name]) - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - result = CBOR_encode_head(5, len(self.seq)) for fld in self.seq: - # Encode key as a CBOR text string. - result += CBORcodec_TEXT_STRING.enc(CBOR_TEXT_STRING(fld.name)) - result += fld.build(pkt) - return result + if fld.min_items(pkt) > 0 and fld.name not in seen_fields: + raise CBOR_Decoding_Error( + "Required map field %r is missing" % fld.name + ) + pkt.setfieldval(self._unknown_field.name, unknown_pairs) + return _CBORParseResult(remaining=remaining, items=1) -class CBORF_SEMANTIC_TAG(CBORF_field[Tuple[int, Any], - CBOR_SEMANTIC_TAG]): +class CBORF_SEMANTIC_TAG(CBORF_element): """ - CBOR semantic tag field (major type 6). + CBOR semantic tag wrapper (major type 6). - Wraps an ``inner_field`` with the given numeric ``tag_num``. The inner - field handles encoding and decoding of the tagged value. The outer field - (named ``name``) stores the :class:`~scapy.cbor.cbor.CBOR_SEMANTIC_TAG` - wrapper (tag number + ``None`` placeholder), while the inner field stores - its value under its own name on the packet. + Wraps an ``inner_field`` with the given numeric ``tag_num``. The tag + number is schema metadata only: it is not stored as editable packet + field state. The inner field stores its value under its own name. Example:: class TimestampPkt(CBOR_Packet): CBOR_root = CBORF_SEMANTIC_TAG( - "tag_info", None, 1, CBORF_INTEGER("ts", 0) + 1, CBORF_INTEGER("ts", 0) ) """ CBOR_tag = CBOR_MajorTypes.TAG + holds_packets = 0 def __init__(self, - name, # type: str - default, # type: Any tag_num, # type: int - inner_field, # type: CBORF_field[Any, Any] + inner_field, # type: CBORF_field[Any] ): # type: (...) -> None + if tag_num < 0 or tag_num > CBOR_UINT64_MAX: + raise CBOR_Encoding_Error( + "Semantic tag number out of uint64 range") + if isinstance(inner_field, CBORF_REMAINDER_OF): + raise ValueError( + "CBORF_REMAINDER_OF cannot be wrapped; " + "place it directly as the final positional field" + ) self.tag_num = tag_num self.inner_field = inner_field - super(CBORF_SEMANTIC_TAG, self).__init__(name, default) - def _wrap(self, val): - # type: (Any) -> CBOR_SEMANTIC_TAG - if isinstance(val, CBOR_SEMANTIC_TAG): - return val - return CBOR_SEMANTIC_TAG((self.tag_num, val)) + @property + def name(self): + # type: () -> str + """Map/schema key identity comes from the tagged value field.""" + return self.inner_field.name - def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_SEMANTIC_TAG, bytes] + def _parse_tag_head(self, s): + # type: (bytes) -> bytes try: - major_type, tag_num, s = CBOR_decode_head(s) + major_type, tag_num, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) - if major_type != 6: - raise CBOR_Decoding_Error( + if major_type != CBOR_MajorTypes.TAG: + raise CBOR_Type_Mismatch( "Expected major type 6 (semantic tag), got %d" % major_type) - return CBOR_SEMANTIC_TAG((tag_num, None)), s # type: ignore + if tag_num != self.tag_num: + raise CBOR_Type_Mismatch( + "Expected tag %d, got %d" % (self.tag_num, tag_num)) + return remaining + + def _encode_tagged(self, inner_data): + # type: (bytes) -> bytes + return CBOR_encode_head(CBOR_MajorTypes.TAG, self.tag_num) + inner_data + + def matches_next_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bool + if not s or cbor_is_break(s): + return False + try: + major_type, tag_num, _rem = CBOR_decode_head(s) + except CBOR_Codec_Decoding_Error: + return False + return ( + major_type == CBOR_MajorTypes.TAG + and tag_num == self.tag_num + ) - def dissect(self, pkt, s): - # type: (CBOR_Packet, bytes) -> bytes - tag_obj, s = self.m2i(pkt, s) - self.set_val(pkt, tag_obj) - # Dissect the tagged content using the inner field. - return self.inner_field.dissect(pkt, s) + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + remaining = self._parse_tag_head(s) + inner = self.inner_field._dissect_counted(pkt, remaining) + if inner.items != 1: + raise CBOR_Decoding_Error( + "Semantic tag content must be exactly one CBOR item") + return _CBORParseResult(remaining=inner.remaining, items=1) + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + inner = self.inner_field._build_counted(pkt) + if inner.items != 1: + raise CBOR_Encoding_Error( + "Semantic tag content must be exactly one CBOR item") + return _CBORBuildResult(self._encode_tagged(inner.data), 1) + + def _parse_value(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + remaining = self._parse_tag_head(s) + inner = self.inner_field._parse_value(pkt, remaining) + if inner.items != 1: + raise CBOR_Decoding_Error( + "Semantic tag content must be exactly one CBOR item") + return _CBORParseResult( + value=inner.value, remaining=inner.remaining, items=1 + ) - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - inner_bytes = self.inner_field.build(pkt) - return CBOR_encode_head(6, self.tag_num) + inner_bytes + def _build_value(self, pkt, value): + # type: (CBOR_Packet, Any) -> _CBORBuildResult + inner = self.inner_field._build_value(pkt, value) + if inner.items != 1: + raise CBOR_Encoding_Error( + "Semantic tag content must be exactly one CBOR item") + return _CBORBuildResult(data=self._encode_tagged(inner.data), items=1) def get_fields_list(self): - # type: () -> List[CBORF_field[Any, Any]] - return [self] + self.inner_field.get_fields_list() + # type: () -> List[CBORF_field[Any]] + # Tag number is schema metadata; only the tagged value is packet state. + return self.inner_field.get_fields_list() + + def mark_absent(self, pkt): + # type: (CBOR_Packet) -> None + self.inner_field.mark_absent(pkt) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return self.inner_field.is_empty(pkt) ############################## @@ -816,88 +2127,149 @@ def get_fields_list(self): class CBORF_optional(CBORF_element): """ - Wrapper making a :class:`CBORF_field` optional. + Wrapper making a CBOR field or semantic-tag field optional. - During decoding, if the next CBOR item does not match the expected major - type, the field value is set to ``None`` and the stream is left unchanged. + Accepts :class:`CBORF_field` or :class:`CBORF_SEMANTIC_TAG` (presence + methods required). Absence is recorded as ``CBOR_ABSENT`` on every path + (lookahead mismatch, exhausted parent array, missing map key). If the + next item matches but decoding fails, the error propagates. """ def __init__(self, field): - # type: (CBORF_field[Any, Any]) -> None + # type: (Union[CBORF_field[Any], CBORF_SEMANTIC_TAG]) -> None + if not isinstance(field, (CBORF_field, CBORF_SEMANTIC_TAG)): + raise TypeError( + "CBORF_optional requires CBORF_field or CBORF_SEMANTIC_TAG; " + "got %r" % (type(field).__name__,) + ) + if isinstance(field, CBORF_REMAINDER_OF): + raise ValueError( + "CBORF_REMAINDER_OF cannot be wrapped; " + "place it directly as the final positional field" + ) self._field = field def __getattr__(self, attr): - # type: (str) -> Optional[Any] + # type: (str) -> Any return getattr(self._field, attr) - def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] - try: - return self._field.m2i(pkt, s) - except (CBOR_Error, CBORF_badsequence, - CBOR_Codec_Decoding_Error): - return None, s + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + if self._field.is_empty(pkt): + return _CBORBuildResult(b"", 0) + return self._field._build_counted(pkt) - def dissect(self, pkt, s): - # type: (CBOR_Packet, bytes) -> bytes - try: - return self._field.dissect(pkt, s) - except (CBOR_Error, CBORF_badsequence, - CBOR_Codec_Decoding_Error): - self._field.set_val(pkt, None) - return s + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + if not self._field.matches_next_item(pkt, s): + self._field.mark_absent(pkt) + return _CBORParseResult(remaining=s, items=0) + return self._field._dissect_counted(pkt, s) - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - if self._field.is_empty(pkt): - return b"" - return self._field.build(pkt) + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 - def any2i(self, pkt, x): - # type: (CBOR_Packet, Any) -> Any - return self._field.any2i(pkt, x) + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self._field.structural_max_items(pkt) - def i2repr(self, pkt, x): - # type: (CBOR_Packet, Any) -> str - return self._field.i2repr(pkt, x) +class CBORF_CONDITIONAL(CBORF_element, fields.ConditionalField): + """ + Wrapper making a :class:`CBORF_field` conditional on some other packet + state. + """ + + def __init__(self, + fld, # type: CBORF_field[Any] + cond, # type: Callable[[Packet], bool] + ): + # type: (...) -> None + if isinstance(fld, CBORF_REMAINDER_OF): + raise ValueError( + "CBORF_REMAINDER_OF cannot be wrapped; " + "place it directly as the final positional field" + ) + fields.ConditionalField.__init__(self, fld, cond) -class CBORF_PACKET(CBORF_field['CBOR_Packet', Optional['CBOR_Packet']]): + def __repr__(self): + # type: () -> str + return "<%s%r>" % (self.__class__.__name__, self.fld) + + @property + def owners(self): + return self.fld.owners + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + if self._evalcond(pkt): + return self.fld._build_counted(pkt) + return _CBORBuildResult(b"", 0) + + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + if self._evalcond(pkt): + return self.fld._dissect_counted(pkt, s) + return _CBORParseResult(remaining=s, items=0) + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + if self._evalcond(pkt): + return self.fld.min_items(pkt) + return 0 + + def structural_max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self.fld.structural_max_items(pkt) + + +class CBORF_PACKET(CBORF_field['CBOR_Packet']): """ CBOR field that encapsulates a nested :class:`CBOR_Packet`. The nested packet is encoded as-is (its ``CBOR_root.build()`` output) - and decoded by instantiating ``cls`` from the current byte stream. + and decoded by instantiating ``pkt_cls`` from the current byte stream. + + Use ``pkt_cls=`` (or a positional third argument). A ``cls=`` keyword + conflicts with :class:`~typing.Generic` on Python 3.7. """ holds_packets = 1 def __init__(self, name, # type: str default, # type: Optional[CBOR_Packet] - cls, # type: Type[CBOR_Packet] + pkt_cls, # type: Type[CBOR_Packet] ): # type: (...) -> None - self.cls = cls - super(CBORF_PACKET, self).__init__(name, None) - self.default = default + self.cls = _CBORF_HOMOGENEOUS._require_packet_cls(pkt_cls) + super(CBORF_PACKET, self).__init__(name, default) def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] - return self.extract_packet(self.cls, s, _underlayer=pkt) + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_Packet, bytes] + item_bytes, remain = cbor_item_span(s) + try: + child = self.cls(item_bytes, _parent=pkt) # type: ignore + except CBOR_Decoding_Error: + raise + except Exception as exc: + if config.conf.debug_dissector: + raise + raise CBOR_Decoding_Error(str(exc)) + return child, remain def i2m(self, pkt, x): # type: (CBOR_Packet, Any) -> bytes if x is None: - return b"" - if isinstance(x, bytes): - return x - return bytes(x) + raise CBOR_Encoding_Error( + "Required field %r is None" % self.name) + return _encode_exactly_one_cbor_item( + x, context="field %r" % self.name + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> CBOR_Packet - if hasattr(x, "add_underlayer"): - x.add_underlayer(pkt) - return super(CBORF_PACKET, self).any2i(pkt, x) # type: ignore + return cast('CBOR_Packet', _cbor_attach_parent(pkt, x)) def randval(self): # type: ignore # type: () -> CBOR_Packet diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index eb12bedaea9..52ed1f514e0 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -6,7 +6,8 @@ CBOR Packet Packet holding data encoded in Concise Binary Object Representation (CBOR). -Modelled after scapy/asn1packet.py. +Modelled after scapy/asn1packet.py, with CBOR-specific raw-cache integration +for sentinels (``CBOR_ABSENT``) and mutable ANY values. """ from scapy.base_classes import Packet_metaclass @@ -17,13 +18,10 @@ Dict, Tuple, Type, + Optional, cast, - TYPE_CHECKING, ) -if TYPE_CHECKING: - from scapy.cbor.cborfields import CBORF_field # noqa: F401 - class CBORPacket_metaclass(Packet_metaclass): def __new__(cls, @@ -41,25 +39,133 @@ def __new__(cls, class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): - CBOR_root = cast('CBORF_field[Any, Any]', None) + """CBOR packet with root-schema build/dissect and cache integration. + + Field flags (``islist`` / ``ismutable`` / ``holds_packets``) drive + Scapy's mutation detection and per-instance default copying. This class + re-parents nested packet defaults for exact-wire rebuilds. + """ + + CBOR_root = None # type: Optional[Any] + + def do_init_cached_fields(self, for_dissect_only=False): + # type: (bool) -> None + super(CBOR_Packet, self).do_init_cached_fields( + for_dissect_only=for_dissect_only + ) + if for_dissect_only: + return + # Packet isolates list/Packet defaults into fields; re-parent those. + for f in self.packetfields: + if f.name in self.fields: + self.fields[f.name] = f.any2i(self, self.fields[f.name]) + # Isolate CBOR ismutable defaults (e.g. CBORF_ANY objects) without + # promoting into fields (bind overloads stay Packet-global). + need = [ + f.name for f in self.fields_desc + if getattr(f, "ismutable", False) + and f.name in self.default_fields + and f.name not in self.fields + ] + if need: + self.default_fields = dict(self.default_fields) + for name in need: + fld = self.fieldtype[name] + self.default_fields[name] = fld.do_copy( + self.default_fields[name] + ) + + def _raw_packet_cache_field_value(self, fld, val, copy=False): + # type: (Any, Any, bool) -> Optional[Any] + # Field-local fingerprints (e.g. CBORF_ANY) include wire-cache state + # that semantic CBOR_Object equality ignores. + cache_fingerprint = getattr(fld, "cache_fingerprint", None) + if cache_fingerprint is not None: + fingerprint = cache_fingerprint(val) + if fingerprint is not None: + return fingerprint + if fld.holds_packets: + # Compose nested CBOR field fingerprints instead of shallow-copying + # child.fields (which aliases mutable CBOR_Object trees). + def _child_fp(child): + # type: (Packet) -> Tuple[Any, Any] + child_fields = {} # type: Dict[str, Any] + if isinstance(child, CBOR_Packet): + from scapy.cbor.cborfields import CBOR_ABSENT + for cf in child.fields_desc: + if cf.name not in child.fields: + continue + cval = child.fields[cf.name] + if cval is CBOR_ABSENT: + child_fields[cf.name] = CBOR_ABSENT + continue + if cval is None and getattr(cf, "isconditional", False): + continue + if ( + cf.islist + or cf.holds_packets + or getattr(cf, "ismutable", False) + ) and cval is not None: + child_fields[cf.name] = ( + child._raw_packet_cache_field_value( + cf, cval, copy=copy + ) + ) + else: + child_fields[cf.name] = cval + else: + child_fields = ( + fld.do_copy(child.fields) if copy else child.fields + ) + return (child_fields, child.payload.raw_packet_cache) + + if fld.islist: + return [_child_fp(item) for item in val] + return _child_fp(val) + return super(CBOR_Packet, self)._raw_packet_cache_field_value( + fld, val, copy + ) def self_build(self): # type: () -> bytes - """Build this CBOR packet to wire bytes using CBOR_root. - - Returns the raw packet cache when already built, otherwise delegates - to CBOR_root.build() which encodes all fields according to the CBOR - schema defined for this packet. - """ - if self.raw_packet_cache is not None: + if self._raw_packet_cache_is_valid(): return self.raw_packet_cache return self.CBOR_root.build(self) - def do_dissect(self, x): + def do_dissect(self, s): # type: (bytes) -> bytes - """Dissect CBOR-encoded bytes into packet fields. + from scapy.cbor.cborfields import CBOR_ABSENT + result = self.CBOR_root._dissect_counted(self, s) + remain = result.remaining + self.raw_packet_cache = s[:-len(remain)] if remain else s + self.raw_packet_cache_fields = {} + for f in self.fields_desc: + if f.name not in self.fields: + continue + fval = self.fields[f.name] + # Absent scalars are not mutable fingerprints; storing CBOR_ABSENT + # here would fail validation against the generic None fingerprint. + if fval is CBOR_ABSENT: + continue + if getattr(f, "isconditional", False) and fval is None: + continue + if (f.islist or f.holds_packets or getattr(f, "ismutable", False)) \ + and fval is not None: + self.raw_packet_cache_fields[f.name] = \ + self._raw_packet_cache_field_value(f, fval, copy=True) + self.explicit = 1 + return remain + + def copy(self): + # type: () -> Packet + """Deep-copy this packet and re-parent embedded CBOR children. - Delegates to CBOR_root.dissect() which reads CBOR items from *x*, - populates each field on the packet, and returns any unconsumed bytes. + Generic ``Packet.copy()`` copies packet-valued fields but leaves each + child's ``.parent`` pointing at the original owner. CBOR fields rely on + ``parent`` for ownership, so reattach after the clone is built. """ - return self.CBOR_root.dissect(self, x) + clone = super(CBOR_Packet, self).copy() + for f in clone.fields_desc: + if f.holds_packets and f.name in clone.fields: + clone.fields[f.name] = f.any2i(clone, clone.fields[f.name]) + return clone diff --git a/scapy/packet.py b/scapy/packet.py index 8afb483c94b..f5983ac1c99 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -367,14 +367,11 @@ def do_init_cached_fields(self, for_dissect_only=False): if for_dissect_only: return - # Deepcopy default references + # Deepcopy default references into fields (list/dict/Packet/…) for fname in Packet.class_default_fields_ref[cls_name]: value = self.default_fields[fname] - try: - self.fields[fname] = value.copy() - except AttributeError: - # Python 2.7 - list only - self.fields[fname] = value[:] + fld = self.fieldtype[fname] + self.fields[fname] = fld.do_copy(value) def prepare_cached_fields(self, flist): # type: (Sequence[AnyField]) -> None @@ -406,7 +403,7 @@ def prepare_cached_fields(self, flist): if f.holds_packets: class_packetfields.append(f) - # Remember references + # list/dict/set/Packet/RandField: promote copies into fields if isinstance(f.default, (list, dict, set, RandField, Packet)): class_default_fields_ref.append(f.name) @@ -766,22 +763,30 @@ def clear_cache(self): fsubval.clear_cache() self.payload.clear_cache() + def _raw_packet_cache_is_valid(self): + # type: () -> bool + """Return True if ``raw_packet_cache`` still matches nested field state. + + On mismatch, clear the cache fingerprints and ``wirelen``. + """ + if self.raw_packet_cache is None or self.raw_packet_cache_fields is None: + return False + for fname, fval in self.raw_packet_cache_fields.items(): + fld, val = self.getfield_and_val(fname) + if self._raw_packet_cache_field_value(fld, val) != fval: + self.raw_packet_cache = None + self.raw_packet_cache_fields = None + self.wirelen = None + return False + return True + def self_build(self): # type: () -> bytes """ Create the default layer regarding fields_desc dict """ - if self.raw_packet_cache is not None and \ - self.raw_packet_cache_fields is not None: - for fname, fval in self.raw_packet_cache_fields.items(): - fld, val = self.getfield_and_val(fname) - if self._raw_packet_cache_field_value(fld, val) != fval: - self.raw_packet_cache = None - self.raw_packet_cache_fields = None - self.wirelen = None - break - if self.raw_packet_cache is not None: - return self.raw_packet_cache + if self._raw_packet_cache_is_valid(): + return cast(bytes, self.raw_packet_cache) p = b"" for f in self.fields_desc: val = self.getfieldval(f.name) diff --git a/scapy/utils.py b/scapy/utils.py index 2e0e0479075..ba9e439d7e3 100644 --- a/scapy/utils.py +++ b/scapy/utils.py @@ -1186,6 +1186,14 @@ def __neq__(self, other): # type: (Any) -> bool return not self.__eq__(other) + def __copy__(self): + # type: () -> EnumElement + return self + + def __deepcopy__(self, memo): + # type: (Dict[Any, Any]) -> EnumElement + return self + class Enum_metaclass(type): element_class = EnumElement diff --git a/test/configs/bsd.utsc b/test/configs/bsd.utsc index 194466f989f..4e2a79f4aaf 100644 --- a/test/configs/bsd.utsc +++ b/test/configs/bsd.utsc @@ -20,7 +20,8 @@ "test/contrib/automotive/gm/gmlanutils.uts", "test/contrib/isotp_packet.uts", "test/contrib/isotpscan.uts", - "test/contrib/isotp_soft_socket.uts" + "test/contrib/isotp_soft_socket.uts", + "test/scapy/layers/cbor_cbor2_interop.uts" ], "onlyfailed": true, "extensions": ["scapy-rpc"], @@ -36,6 +37,7 @@ "ipv6", "vcan_socket", "tun", - "tap" + "tap", + "external_cbor2" ] } diff --git a/test/configs/linux.utsc b/test/configs/linux.utsc index b26e9166c85..63564ba5d6f 100644 --- a/test/configs/linux.utsc +++ b/test/configs/linux.utsc @@ -16,7 +16,8 @@ ], "remove_testfiles": [ "test/windows.uts", - "test/bpf.uts" + "test/bpf.uts", + "test/scapy/layers/cbor_cbor2_interop.uts" ], "breakfailed": true, "onlyfailed": true, @@ -28,6 +29,7 @@ "kw_ko": [ "osx", "windows", - "ipv6" + "ipv6", + "external_cbor2" ] } diff --git a/test/configs/solaris.utsc b/test/configs/solaris.utsc index 85c3c570f0b..101513a57e3 100644 --- a/test/configs/solaris.utsc +++ b/test/configs/solaris.utsc @@ -19,7 +19,8 @@ "test/windows.uts", "test/contrib/automotive/ecu_am.uts", "test/contrib/automotive/gm/gmlanutils.uts", - "test/contrib/isotpscan.uts" + "test/contrib/isotpscan.uts", + "test/scapy/layers/cbor_cbor2_interop.uts" ], "onlyfailed": true, "extensions": ["scapy-rpc"], @@ -35,6 +36,7 @@ "ipv6", "tap", "tun", - "vcan_socket" + "vcan_socket", + "external_cbor2" ] } diff --git a/test/configs/windows.utsc b/test/configs/windows.utsc index a38f065e8ca..fdb18762293 100644 --- a/test/configs/windows.utsc +++ b/test/configs/windows.utsc @@ -15,7 +15,8 @@ ], "remove_testfiles": [ "test\\bpf.uts", - "test\\linux.uts" + "test\\linux.uts", + "test\\scapy\\layers\\cbor_cbor2_interop.uts" ], "breakfailed": true, "onlyfailed": true, @@ -38,6 +39,7 @@ "tap", "tun", "vcan_socket", - "zstd" + "zstd", + "external_cbor2" ] } diff --git a/test/configs/windows2.utsc b/test/configs/windows2.utsc index 8d284880dd0..4703c9b5e39 100644 --- a/test/configs/windows2.utsc +++ b/test/configs/windows2.utsc @@ -13,7 +13,8 @@ ], "remove_testfiles": [ "bpf.uts", - "linux.uts" + "linux.uts", + "scapy\\layers\\cbor_cbor2_interop.uts" ], "breakfailed": true, "onlyfailed": true, @@ -37,6 +38,7 @@ "tcpdump", "tap", "tun", - "tshark" + "tshark", + "external_cbor2" ] } diff --git a/test/fields.uts b/test/fields.uts index e2d1132d414..2b208f4c27e 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -2357,3 +2357,36 @@ p assert p.indent == 0xf assert p.pcount == 4 assert [p.x for p in p.plist] == [0x41, 0x42, 0x43, 0x44] + +############ +############ ++ EnumElement and mutable Field default isolation + += EnumElement copy and deepcopy return the same singleton +~ core field +from copy import copy, deepcopy +from scapy.utils import Enum_metaclass + +class _EnumCopyProbe(metaclass=Enum_metaclass): + name = "ENUM_COPY_PROBE" + A = 1 + B = 2 + +assert copy(_EnumCopyProbe.A) is _EnumCopyProbe.A +assert deepcopy(_EnumCopyProbe.B) is _EnumCopyProbe.B + += Mutable PacketListField defaults isolate nested Packet members via Field.do_copy +~ core field +from scapy.layers.inet import IP + +class _MutPktListDefault(Packet): + fields_desc = [ + PacketListField("lst", [IP(dst="1.2.3.4")], IP), + ] + +a = _MutPktListDefault() +b = _MutPktListDefault() +assert a.lst is not b.lst +assert a.lst[0] is not b.lst[0] +a.lst[0].dst = "9.9.9.9" +assert b.lst[0].dst == "1.2.3.4" diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index f65c75d89ce..3d8b0ff91f2 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -4,9 +4,9 @@ # Try me with: # bash test/run_tests -t test/scapy/layers/cbor.uts -F # -# NOTE: Interoperability tests require cbor2 (test-only dependency): -# pip install cbor2 -# cbor2 is used ONLY in tests, NOT in the scapy CBOR implementation +# Interoperability / cbor2 differential tests live in: +# test/scapy/layers/cbor_cbor2_interop.uts +# (requires: pip install -r test/scapy/layers/requirements-cbor2.txt) ########### CBOR Basic Types ####################################### @@ -143,9 +143,24 @@ isinstance(obj, CBOR_UNDEFINED) and remainder == b'' + CBOR Float -= Encode double precision float += Encode preferred (shortest) float for exact half-precision values obj = CBOR_FLOAT(1.5) -bytes(obj) == b'\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00' +bytes(obj) == b'\xf9\x3e\x00' + += Encode double precision when shorter widths cannot preserve the value +obj = CBOR_FLOAT(1.0e300) +bytes(obj) == b'\xfb\x7e\x37\xe4\x3c\x88\x00\x75\x9c' + += Decoded floats preserve their exact received encoding on rebuild +obj, rem = CBOR_Codecs.CBOR.dec(b'\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00') +abs(obj.val - 1.5) < 0.0001 and rem == b'' and bytes(obj) == b'\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00' + += CBOR_Object equality compares type and value +from scapy.cbor import CBOR_UNSIGNED_INTEGER, CBOR_TRUE +assert CBOR_UNSIGNED_INTEGER(1) == CBOR_UNSIGNED_INTEGER(1) +assert CBOR_UNSIGNED_INTEGER(1) != CBOR_UNSIGNED_INTEGER(2) +assert CBOR_UNSIGNED_INTEGER(1) != CBOR_TRUE() +assert CBOR_UNSIGNED_INTEGER(1) != 1 = Decode double precision float obj, remainder = CBOR_Codecs.CBOR.dec(b'\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00') @@ -268,6 +283,10 @@ isinstance(obj, CBOR_MAP) and remainder == b'' obj, remainder = CBOR_Codecs.CBOR.safedec(b'\xff\xff\xff') isinstance(obj, CBOR_DECODING_ERROR) += Safe decode of a truncated nested array wraps only the outer error +obj, remainder = CBOR_Codecs.CBOR.safedec(b'\x82\x01') +isinstance(obj, CBOR_DECODING_ERROR) and remainder == b'' + = Decode with insufficient bytes for length try: obj, remainder = CBOR_Codecs.CBOR.dec(b'\x18') @@ -282,3670 +301,4683 @@ try: except: True -########### CBOR Interoperability Tests with cbor2 ################# -# These tests verify interoperability between scapy's CBOR implementation -# and the standard cbor2 library. cbor2 is ONLY used in tests, not in -# the scapy implementation. -# -# NOTE: These tests require cbor2 to be installed: pip install cbor2 - -+ CBOR Interoperability - Basic Types (Scapy encode, cbor2 decode) ++ CBORF_REMAINDER_OF packet item cardinality -= Check cbor2 availability -try: - import cbor2 - cbor2_available = True -except ImportError: - cbor2_available = False += CBORF_REMAINDER_OF rejects a packet element that consumes two top-level items +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -cbor2_available +class TwoItemSequenceDecodeElement(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_UNSIGNED_INTEGER("first", 0), + CBORF_UNSIGNED_INTEGER("second", 0), + ) -= Interop: Scapy encode unsigned integer, cbor2 decode -import cbor2 -obj = CBOR_UNSIGNED_INTEGER(42) -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded == 42 +class PacketSequenceDecode(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF( + "elements", + [], + TwoItemSequenceDecodeElement, + ) -= Interop: Scapy encode negative integer, cbor2 decode -obj = CBOR_NEGATIVE_INTEGER(-100) -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded == -100 - -= Interop: Scapy encode text string, cbor2 decode -obj = CBOR_TEXT_STRING("Hello, World!") -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded == "Hello, World!" - -= Interop: Scapy encode UTF-8 text string, cbor2 decode -obj = CBOR_TEXT_STRING("Café ☕") -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded == "Café ☕" - -= Interop: Scapy encode byte string, cbor2 decode -obj = CBOR_BYTE_STRING(b'\x01\x02\x03\x04\x05') -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded == b'\x01\x02\x03\x04\x05' - -= Interop: Scapy encode true, cbor2 decode -obj = CBOR_TRUE() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded is True +try: + PacketSequenceDecode(b"\x01\x02") + assert False, "REMAINDER_OF accepted two CBOR items as one packet element" +except CBOR_Decoding_Error: + pass + ++ Optional lookahead distinguishes absence from malformed presence + += An outer major-type mismatch means that an optional semantic tag is absent +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet -= Interop: Scapy encode false, cbor2 decode -obj = CBOR_FALSE() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded is False +class OptionalTaggedThenFallback(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) + ), + CBORF_ANY("fallback", None), + ) -= Interop: Scapy encode null, cbor2 decode -obj = CBOR_NULL() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded is None +pkt = OptionalTaggedThenFallback(b"\x81\x07") +from scapy.cbor.cborfields import CBOR_ABSENT +assert pkt.tagged_value is CBOR_ABSENT +assert pkt.fallback.val == 7 + += A matching optional semantic tag with the wrong inner type is malformed +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_ITEMS, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet -= Interop: Scapy encode undefined, cbor2 decode -obj = CBOR_UNDEFINED() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -from cbor2 import undefined -decoded is undefined +class OptionalTaggedUnsigned(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional( + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) + ) + ) -= Interop: Scapy encode float, cbor2 decode -obj = CBOR_FLOAT(3.14159) -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -abs(decoded - 3.14159) < 0.0001 +pkt = OptionalTaggedUnsigned() +try: + OptionalTaggedUnsigned.CBOR_root._dissect_counted( + pkt, + b"\xc1\x61x", + ) + assert False, "A present tag with malformed content was treated as absent" +except CBOR_Decoding_Error: + pass + += A matching optional semantic tag with truncated content is malformed +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_ITEMS, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet -+ CBOR Interoperability - Collections (Scapy encode, cbor2 decode) +class OptionalTruncatedTaggedUnsigned(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional( + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) + ) + ) -= Interop: Scapy encode array, cbor2 decode -from scapy.cbor.cborcodec import CBORcodec_ARRAY -encoded = CBORcodec_ARRAY.enc([1, 2, 3, 4, 5]) -decoded = cbor2.loads(encoded) -decoded == [1, 2, 3, 4, 5] +pkt = OptionalTruncatedTaggedUnsigned() +try: + OptionalTruncatedTaggedUnsigned.CBOR_root._dissect_counted(pkt, b"\xc1") + assert False, "A truncated present tag was treated as an absent field" +except CBOR_Decoding_Error: + pass + += Zero-budget optional stays absent so a trailing CBORF_ANY can consume the item +# When the optional has available==0 because a required trailing field +# reserved the only item, mark the optional absent and let the trailing +# field consume the item (even if outer types match). +from scapy.cbor.cborfields import ( + CBOR_ABSENT, + CBORF_ANY, + CBORF_ARRAY, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet -= Interop: Scapy encode nested array, cbor2 decode -encoded = CBORcodec_ARRAY.enc([1, [2, 3], [4, [5, 6]]]) -decoded = cbor2.loads(encoded) -decoded == [1, [2, 3], [4, [5, 6]]] +class OptionalTaggedBeforeAny(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) + ), + CBORF_ANY("fallback", None), + ) -= Interop: Scapy encode map, cbor2 decode -from scapy.cbor.cborcodec import CBORcodec_MAP -encoded = CBORcodec_MAP.enc({"a": 1, "b": 2, "c": 3}) -decoded = cbor2.loads(encoded) -decoded == {"a": 1, "b": 2, "c": 3} - -= Interop: Scapy encode complex map, cbor2 decode -data = {"name": "Alice", "age": 30, "active": True, "tags": ["user", "admin"]} -encoded = CBORcodec_MAP.enc(data) -decoded = cbor2.loads(encoded) -decoded == data - -= Interop: Scapy encode mixed array, cbor2 decode -encoded = CBORcodec_ARRAY.enc([42, "hello", True, None, 3.14, [1, 2]]) -decoded = cbor2.loads(encoded) -len(decoded) == 6 and decoded[0] == 42 and decoded[1] == "hello" - -+ CBOR Interoperability - Basic Types (cbor2 encode, Scapy decode) - -= Interop: cbor2 encode unsigned integer, Scapy decode -encoded = cbor2.dumps(42) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val == 42 and isinstance(obj, CBOR_UNSIGNED_INTEGER) - -= Interop: cbor2 encode negative integer, Scapy decode -encoded = cbor2.dumps(-100) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val == -100 and isinstance(obj, CBOR_NEGATIVE_INTEGER) - -= Interop: cbor2 encode text string, Scapy decode -encoded = cbor2.dumps("Hello, World!") -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val == "Hello, World!" and isinstance(obj, CBOR_TEXT_STRING) - -= Interop: cbor2 encode UTF-8 text string, Scapy decode -encoded = cbor2.dumps("Café ☕") -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val == "Café ☕" and isinstance(obj, CBOR_TEXT_STRING) - -= Interop: cbor2 encode byte string, Scapy decode -encoded = cbor2.dumps(b'\x01\x02\x03\x04\x05') -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val == b'\x01\x02\x03\x04\x05' and isinstance(obj, CBOR_BYTE_STRING) - -= Interop: cbor2 encode true, Scapy decode -encoded = cbor2.dumps(True) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val is True and isinstance(obj, CBOR_TRUE) - -= Interop: cbor2 encode false, Scapy decode -encoded = cbor2.dumps(False) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val is False and isinstance(obj, CBOR_FALSE) - -= Interop: cbor2 encode null, Scapy decode -encoded = cbor2.dumps(None) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -obj.val is None and isinstance(obj, CBOR_NULL) - -= Interop: cbor2 encode undefined, Scapy decode -from cbor2 import CBORSimpleValue, undefined -encoded = cbor2.dumps(undefined) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_UNDEFINED) - -= Interop: cbor2 encode float, Scapy decode -encoded = cbor2.dumps(3.14159) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -abs(obj.val - 3.14159) < 0.0001 and isinstance(obj, CBOR_FLOAT) - -+ CBOR Interoperability - Collections (cbor2 encode, Scapy decode) - -= Interop: cbor2 encode array, Scapy decode -encoded = cbor2.dumps([1, 2, 3, 4, 5]) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 5 - -= Interop: cbor2 encode nested array, Scapy decode -encoded = cbor2.dumps([1, [2, 3], [4, [5, 6]]]) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 +# Tag 2 does not match optional tag 1 → absent; ANY consumes the item. +pkt = OptionalTaggedBeforeAny(b"\x81\xc2\x01") +assert pkt.getfieldval("tagged_value") is CBOR_ABSENT +assert pkt.getfieldval("fallback") is not None +assert pkt.getfieldval("fallback") is not CBOR_ABSENT + ++ Optional CBOR null presence + += Optional CBORF_ANY preserves a present null after another field is mutated +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet -= Interop: cbor2 encode map, Scapy decode -encoded = cbor2.dumps({"a": 1, "b": 2, "c": 3}) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_MAP) and len(obj.val) == 3 - -= Interop: cbor2 encode complex map, Scapy decode -data = {"name": "Alice", "age": 30, "active": True} -encoded = cbor2.dumps(data) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_MAP) and "name" in obj.val - -= Interop: cbor2 encode mixed array, Scapy decode -encoded = cbor2.dumps([42, "hello", True, None, 3.14]) -obj, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 5 - -+ CBOR Interoperability - Roundtrip Tests - -= Interop roundtrip: integer through cbor2 -original_val = 12345 -scapy_obj = CBOR_UNSIGNED_INTEGER(original_val) -scapy_encoded = bytes(scapy_obj) -cbor2_decoded = cbor2.loads(scapy_encoded) -cbor2_encoded = cbor2.dumps(cbor2_decoded) -scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) -scapy_decoded.val == original_val - -= Interop roundtrip: string through cbor2 -original_val = "Test String 测试" -scapy_obj = CBOR_TEXT_STRING(original_val) -scapy_encoded = bytes(scapy_obj) -cbor2_decoded = cbor2.loads(scapy_encoded) -cbor2_encoded = cbor2.dumps(cbor2_decoded) -scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) -scapy_decoded.val == original_val - -= Interop roundtrip: array through cbor2 -original_val = [1, "two", 3.0, True, None] -scapy_encoded = CBORcodec_ARRAY.enc(original_val) -cbor2_decoded = cbor2.loads(scapy_encoded) -cbor2_encoded = cbor2.dumps(cbor2_decoded) -scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) -isinstance(scapy_decoded, CBOR_ARRAY) and len(scapy_decoded.val) == 5 - -= Interop roundtrip: map through cbor2 -original_val = {"int": 42, "str": "value", "bool": True, "null": None} -scapy_encoded = CBORcodec_MAP.enc(original_val) -cbor2_decoded = cbor2.loads(scapy_encoded) -cbor2_encoded = cbor2.dumps(cbor2_decoded) -scapy_decoded, _ = CBOR_Codecs.CBOR.dec(cbor2_encoded) -isinstance(scapy_decoded, CBOR_MAP) and len(scapy_decoded.val) == 4 - -+ CBOR Interoperability - Edge Cases - -= Interop: Large unsigned integer -large_int = 18446744073709551615 # 2^64 - 1 -encoded = cbor2.dumps(large_int) -obj, _ = CBOR_Codecs.CBOR.dec(encoded) -obj.val == large_int - -= Interop: Very negative integer -neg_int = -18446744073709551616 # -(2^64) -encoded = cbor2.dumps(neg_int) -obj, _ = CBOR_Codecs.CBOR.dec(encoded) -obj.val == neg_int - -= Interop: Empty collections -empty_array = cbor2.dumps([]) -obj1, _ = CBOR_Codecs.CBOR.dec(empty_array) -empty_map = cbor2.dumps({}) -obj2, _ = CBOR_Codecs.CBOR.dec(empty_map) -isinstance(obj1, CBOR_ARRAY) and len(obj1.val) == 0 and isinstance(obj2, CBOR_MAP) and len(obj2.val) == 0 - -= Interop: Deeply nested structure -deep = {"level1": {"level2": {"level3": {"level4": [1, 2, 3]}}}} -encoded = cbor2.dumps(deep) -obj, _ = CBOR_Codecs.CBOR.dec(encoded) -isinstance(obj, CBOR_MAP) - -= Interop: Special float values (infinity) -import math -pos_inf_encoded = cbor2.dumps(math.inf) -pos_inf_obj, _ = CBOR_Codecs.CBOR.dec(pos_inf_encoded) -neg_inf_encoded = cbor2.dumps(-math.inf) -neg_inf_obj, _ = CBOR_Codecs.CBOR.dec(neg_inf_encoded) -math.isinf(pos_inf_obj.val) and math.isinf(neg_inf_obj.val) - -= Interop: Special float value (NaN) -nan_encoded = cbor2.dumps(math.nan) -nan_obj, _ = CBOR_Codecs.CBOR.dec(nan_encoded) -math.isnan(nan_obj.val) - -= Interop: Zero values -zero_int = cbor2.dumps(0) -zero_float = cbor2.dumps(0.0) -obj1, _ = CBOR_Codecs.CBOR.dec(zero_int) -obj2, _ = CBOR_Codecs.CBOR.dec(zero_float) -obj1.val == 0 and obj2.val == 0.0 - -########### Additional Tests Adapted from PR #4875 ################### -# These tests verify specific encoding sizes and edge cases - -+ CBOR Encoding Sizes - Unsigned Integers - -= uint encoding size 0 (argument in initial byte) -obj = CBOR_UNSIGNED_INTEGER(0x12) -data = bytes(obj) -data == bytes.fromhex('12') - -= uint encoding size 1 (1-byte argument follows) -obj = CBOR_UNSIGNED_INTEGER(0x34) -data = bytes(obj) -data == bytes.fromhex('1834') - -= uint encoding size 2 (2-byte argument follows) -obj = CBOR_UNSIGNED_INTEGER(0x1234) -data = bytes(obj) -data == bytes.fromhex('191234') - -= uint encoding size 4 (4-byte argument follows) -obj = CBOR_UNSIGNED_INTEGER(0x12345678) -data = bytes(obj) -data == bytes.fromhex('1a12345678') - -= uint encoding size 8 (8-byte argument follows) -obj = CBOR_UNSIGNED_INTEGER(0x1234567812345678) -data = bytes(obj) -data == bytes.fromhex('1b1234567812345678') - -= uint decoding size 0 -data = bytes.fromhex('12') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 18 and remainder == b'' - -= uint decoding size 1 -data = bytes.fromhex('1834') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 0x34 and remainder == b'' - -= uint decoding size 2 -data = bytes.fromhex('191234') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 0x1234 and remainder == b'' - -= uint decoding size 4 -data = bytes.fromhex('1a12345678') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 0x12345678 and remainder == b'' - -= uint decoding size 8 -data = bytes.fromhex('1b1234567812345678') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 0x1234567812345678 and remainder == b'' - -+ CBOR Encoding Sizes - Negative Integers - -= nint encoding size 0 -obj = CBOR_NEGATIVE_INTEGER(-0x13) -data = bytes(obj) -data == bytes.fromhex('32') - -= nint decoding size 0 -data = bytes.fromhex('32') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == -0x13 and isinstance(obj, CBOR_NEGATIVE_INTEGER) and remainder == b'' - -= nint decoding size 2 -data = bytes.fromhex('391234') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == (-0x1234 - 1) and isinstance(obj, CBOR_NEGATIVE_INTEGER) and remainder == b'' - -+ CBOR Byte String Edge Cases - -= bstr encoding with specific content -obj = CBOR_BYTE_STRING(b'hi') -data = bytes(obj) -data == bytes.fromhex('426869') - -= bstr decoding with specific content -data = bytes.fromhex('426869') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == b'hi' and isinstance(obj, CBOR_BYTE_STRING) and remainder == b'' - -= bstr longer content (24 bytes) -content = b'longlonglonglonglonglong' -obj = CBOR_BYTE_STRING(content) -data = bytes(obj) -# Should use 1-byte length encoding (0x58 = major type 2, additional info 24) -data[:2] == bytes.fromhex('5818') and data[2:] == content - -= bstr decoding longer content -data = bytes.fromhex('58186c6f6e676c6f6e676c6f6e676c6f6e676c6f6e676c6f6e67') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == b'longlonglonglonglonglong' and remainder == b'' - -+ CBOR Text String Edge Cases - -= tstr encoding with specific content -obj = CBOR_TEXT_STRING('hi') -data = bytes(obj) -data == bytes.fromhex('626869') - -= tstr decoding with specific content -data = bytes.fromhex('626869') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 'hi' and isinstance(obj, CBOR_TEXT_STRING) and remainder == b'' - -= tstr longer content (24 chars) -content = 'longlonglonglonglonglong' -obj = CBOR_TEXT_STRING(content) -data = bytes(obj) -# Should use 1-byte length encoding (0x78 = major type 3, additional info 24) -data[:2] == bytes.fromhex('7818') and data[2:] == content.encode('utf8') - -= tstr decoding longer content -data = bytes.fromhex('78186c6f6e676c6f6e676c6f6e676c6f6e676c6f6e676c6f6e67') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -obj.val == 'longlonglonglonglonglong' and remainder == b'' - -+ CBOR Array Specific Encodings - -= array encoding with mixed integer types -from scapy.cbor.cborcodec import CBORcodec_ARRAY -# Array with positive 10 and negative 20 -encoded = CBORcodec_ARRAY.enc([10, -20]) -decoded, _ = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_ARRAY) and len(decoded.val) == 2 +class OptionalAnyWithTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", None)), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= array decoding specific encoding -data = bytes.fromhex('820A33') # array(2): [10, -20] -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 and remainder == b'' +pkt = OptionalAnyWithTail(b"\x82\xf6\x01") +from scapy.cbor.cbor import CBOR_NULL +assert isinstance(pkt.value, CBOR_NULL) +assert pkt.tail == 1 + +# Mutating another field invalidates Scapy's raw-packet cache. The rebuilt +# packet must still contain the explicitly present CBOR null item. +pkt.tail = 2 +assert bytes(pkt) == b"\x82\xf6\x02" + ++ Nested CBOR packet dissection lifecycle + += CBORF_PACKET runs child dissection hooks and retains the exact child bytes +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -+ CBOR Map Specific Encodings +class LifecycleDirectChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY(CBORF_UNSIGNED_INTEGER("value", 0)) + events = [] + def pre_dissect(self, data): + type(self).events.append("pre") + return data + def do_dissect(self, data): + type(self).events.append("do") + return super().do_dissect(data) + def post_dissect(self, data): + type(self).events.append("post") + return data + +class LifecycleDirectParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, LifecycleDirectChild) + +LifecycleDirectChild.events[:] = [] +pkt = LifecycleDirectParent(b"\x81\x01\xff") +child = pkt.child +assert LifecycleDirectChild.events == ["pre", "do", "post"] +assert child.original == b"\x81\x01" +assert child.raw_packet_cache == b"\x81\x01" + += Packet-valued CBORF_ARRAY_OF runs child hooks and retains each item span +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -= map encoding with integer keys -from scapy.cbor.cborcodec import CBORcodec_MAP -encoded = CBORcodec_MAP.enc({10: -20}) -decoded, _ = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_MAP) and len(decoded.val) == 1 +class LifecycleArrayChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY(CBORF_UNSIGNED_INTEGER("value", 0)) + events = [] + def pre_dissect(self, data): + type(self).events.append("pre") + return data + def do_dissect(self, data): + type(self).events.append("do") + return super().do_dissect(data) + def post_dissect(self, data): + type(self).events.append("post") + return data + +class LifecycleArrayParent(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("items", [], LifecycleArrayChild) + +LifecycleArrayChild.events[:] = [] +pkt = LifecycleArrayParent(b"\x81\x81\x01") +child = pkt.items[0] +assert LifecycleArrayChild.events == ["pre", "do", "post"] +assert child.original == b"\x81\x01" +assert child.raw_packet_cache == b"\x81\x01" + += Packet-valued CBORF_REMAINDER_OF runs child hooks and retains each item span +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -= map decoding specific encoding -data = bytes.fromhex('A10A33') # map(1): {10: -20} -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_MAP) and len(obj.val) == 1 and remainder == b'' +class LifecycleSequenceChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY(CBORF_UNSIGNED_INTEGER("value", 0)) + events = [] + def pre_dissect(self, data): + type(self).events.append("pre") + return data + def do_dissect(self, data): + type(self).events.append("do") + return super().do_dissect(data) + def post_dissect(self, data): + type(self).events.append("post") + return data + +class LifecycleSequenceParent(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF("items", [], LifecycleSequenceChild) + +LifecycleSequenceChild.events[:] = [] +pkt = LifecycleSequenceParent(b"\x81\x01") +child = pkt.items[0] +assert LifecycleSequenceChild.events == ["pre", "do", "post"] +assert child.original == b"\x81\x01" +assert child.raw_packet_cache == b"\x81\x01" + += Nested field edits invalidate parent raw_packet_cache on rebuild +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_PACKET +from scapy.cborpacket import CBOR_Packet -+ CBOR Float Specific Encodings +class CacheChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY(CBORF_INTEGER("val", 0)) + +class CacheParent(CBOR_Packet): + CBOR_root = CBORF_ARRAY(CBORF_PACKET("child", None, CacheChild)) + +raw = bytes(CacheParent(child=CacheChild(val=7))) +pkt = CacheParent(raw) +assert pkt.raw_packet_cache is not None +assert pkt.child.val == 7 +pkt.child.val = 9 +assert pkt.child.raw_packet_cache is None +rebuilt = bytes(pkt) +assert rebuilt != raw +assert CacheParent(rebuilt).child.val == 9 + ++ Fixed-map conditional field ordering + += A fixed map decodes conditional members independently of wire key order +from scapy.cbor.cborfields import ( + CBORF_CONDITIONAL, + CBORF_MAP, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -= float64 encoding specific value -obj = CBOR_FLOAT(1.5e20) -data = bytes(obj) -data == bytes.fromhex('FB442043561A882930') +class ConditionalMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("kind", 0), + CBORF_CONDITIONAL( + CBORF_TEXT_STRING("name", None), + lambda pkt: pkt.getfieldval("kind") == 1, + ), + ) -= float64 decoding specific value -data = bytes.fromhex('FB442043561A882930') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 1.5e20 and remainder == b'' +kind_first = b"\xa2\x64kind\x01\x64name\x61x" +name_first = b"\xa2\x64name\x61x\x64kind\x01" -+ CBOR Multiple Item Decoding +first = ConditionalMap(kind_first) +second = ConditionalMap(name_first) +assert first.kind == second.kind == 1 +assert first.getfieldval("name") == second.getfieldval("name") == "x" -= decode multiple items in sequence -data = bytes.fromhex('010203') # Three unsigned integers: 1, 2, 3 -obj1, remainder1 = CBOR_Codecs.CBOR.dec(data) -obj2, remainder2 = CBOR_Codecs.CBOR.dec(remainder1) -obj3, remainder3 = CBOR_Codecs.CBOR.dec(remainder2) -obj1.val == 1 and obj2.val == 2 and obj3.val == 3 and remainder3 == b'' ++ Generic CBOR map key identity -= decode nested array with specific encoding -data = bytes.fromhex('8201820203') # array(2): [1, array(2): [2, 3]] -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 and remainder == b'' and isinstance(obj.val[1], CBOR_ARRAY) += Generic CBOR maps preserve integer 1 and boolean true as distinct keys +from scapy.cbor import CBOR_Codecs -+ CBOR Boundary Value Tests +wire = b"\xa2\x01\x61a\xf5\x61b" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +assert obj.enc() == wire -= encode maximum value that fits in each size -# Maximum for size 0 (0-23) -obj = CBOR_UNSIGNED_INTEGER(23) -bytes(obj) == bytes.fromhex('17') += Generic CBOR maps round-trip a map-valued key +from scapy.cbor import CBOR_Codecs -= encode minimum value needing size 1 -obj = CBOR_UNSIGNED_INTEGER(24) -bytes(obj) == bytes.fromhex('1818') +wire = b"\xa1\xa1\x01\x02\x03" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +assert obj.enc() == wire -= encode maximum value for size 1 -obj = CBOR_UNSIGNED_INTEGER(255) -bytes(obj) == bytes.fromhex('18ff') += Generic CBOR maps still reject duplicate data-item keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error -= encode minimum value needing size 2 -obj = CBOR_UNSIGNED_INTEGER(256) -bytes(obj) == bytes.fromhex('190100') +try: + CBOR_Codecs.CBOR.dec(b"\xa2\x01\x00\x01\x01") + assert False, "A generic map accepted a duplicate integer key" +except CBOR_Codec_Decoding_Error: + pass -= negative integer boundary at -24 -obj = CBOR_NEGATIVE_INTEGER(-24) -bytes(obj) == bytes.fromhex('37') ++ CBORF_ANY map identity and mutability -= negative integer boundary at -25 -obj = CBOR_NEGATIVE_INTEGER(-25) -bytes(obj) == bytes.fromhex('3818') += Empty CBORF_ANY map survives sibling mutation as a map +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBOR_ABSENT +from scapy.cbor.cbor import CBOR_MAP, CBORMapData +from scapy.cborpacket import CBOR_Packet -+ CBOR Empty Container Tests +class AnyMapPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("a", None), + CBORF_UNSIGNED_INTEGER("b", 0), + ) -= encode empty array -from scapy.cbor.cborcodec import CBORcodec_ARRAY -encoded = CBORcodec_ARRAY.enc([]) -decoded, _ = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_ARRAY) and len(decoded.val) == 0 +pkt = AnyMapPkt(b"\x82\xa0\x00") +assert isinstance(pkt.a, CBOR_MAP) +assert isinstance(pkt.a.val, CBORMapData) +assert len(pkt.a.val) == 0 +pkt.b = 1 +assert bytes(pkt) == b"\x82\xa0\x01" -= encode empty map -from scapy.cbor.cborcodec import CBORcodec_MAP -encoded = CBORcodec_MAP.enc({}) -decoded, _ = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_MAP) and len(decoded.val) == 0 += Non-empty CBORF_ANY map survives sibling mutation +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_MAP, CBORMapData +from scapy.cborpacket import CBOR_Packet -= encode empty byte string -obj = CBOR_BYTE_STRING(b'') -data = bytes(obj) -data == bytes.fromhex('40') +class AnyMapPkt2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("a", None), + CBORF_UNSIGNED_INTEGER("b", 0), + ) -= encode empty text string -obj = CBOR_TEXT_STRING('') -data = bytes(obj) -data == bytes.fromhex('60') - -########### CBOR Fuzzing / Random Object Tests #################### - -+ CBOR Random Object Generation - -= Create RandCBORObject -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -isinstance(rand, RandCBORObject) - -= Generate random CBOR unsigned integer -from scapy.cbor import RandCBORObject, CBOR_UNSIGNED_INTEGER -rand = RandCBORObject(objlist=[CBOR_UNSIGNED_INTEGER]) -obj = rand._fix() -isinstance(obj, CBOR_UNSIGNED_INTEGER) and isinstance(obj.val, int) and obj.val >= 0 - -= Generate random CBOR negative integer -from scapy.cbor import RandCBORObject, CBOR_NEGATIVE_INTEGER -rand = RandCBORObject(objlist=[CBOR_NEGATIVE_INTEGER]) -obj = rand._fix() -isinstance(obj, CBOR_NEGATIVE_INTEGER) and isinstance(obj.val, int) and obj.val < 0 - -= Generate random CBOR byte string -from scapy.cbor import RandCBORObject, CBOR_BYTE_STRING -rand = RandCBORObject(objlist=[CBOR_BYTE_STRING]) -obj = rand._fix() -isinstance(obj, CBOR_BYTE_STRING) and isinstance(obj.val, bytes) - -= Generate random CBOR text string -from scapy.cbor import RandCBORObject, CBOR_TEXT_STRING -rand = RandCBORObject(objlist=[CBOR_TEXT_STRING]) -obj = rand._fix() -isinstance(obj, CBOR_TEXT_STRING) and isinstance(obj.val, str) and len(obj.val) > 0 - -= Generate random CBOR array -from scapy.cbor import RandCBORObject, CBOR_ARRAY -rand = RandCBORObject(objlist=[CBOR_ARRAY]) -obj = rand._fix() -isinstance(obj, CBOR_ARRAY) and isinstance(obj.val, list) - -= Generate random CBOR map -from scapy.cbor import RandCBORObject, CBOR_MAP -rand = RandCBORObject(objlist=[CBOR_MAP]) -obj = rand._fix() -isinstance(obj, CBOR_MAP) and isinstance(obj.val, dict) - -= Generate random CBOR boolean (false) -from scapy.cbor import RandCBORObject, CBOR_FALSE -rand = RandCBORObject(objlist=[CBOR_FALSE]) -obj = rand._fix() -isinstance(obj, CBOR_FALSE) and obj.val == False - -= Generate random CBOR boolean (true) -from scapy.cbor import RandCBORObject, CBOR_TRUE -rand = RandCBORObject(objlist=[CBOR_TRUE]) -obj = rand._fix() -isinstance(obj, CBOR_TRUE) and obj.val == True - -= Generate random CBOR null -from scapy.cbor import RandCBORObject, CBOR_NULL -rand = RandCBORObject(objlist=[CBOR_NULL]) -obj = rand._fix() -isinstance(obj, CBOR_NULL) and obj.val is None - -= Generate random CBOR undefined -from scapy.cbor import RandCBORObject, CBOR_UNDEFINED -rand = RandCBORObject(objlist=[CBOR_UNDEFINED]) -obj = rand._fix() -isinstance(obj, CBOR_UNDEFINED) and obj.val is None - -= Generate random CBOR float -from scapy.cbor import RandCBORObject, CBOR_FLOAT -rand = RandCBORObject(objlist=[CBOR_FLOAT]) -obj = rand._fix() -isinstance(obj, CBOR_FLOAT) and isinstance(obj.val, float) - -+ CBOR Random Object Encoding/Decoding - -= Encode and decode random unsigned integer -from scapy.cbor import RandCBORObject, CBOR_UNSIGNED_INTEGER, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_UNSIGNED_INTEGER]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_UNSIGNED_INTEGER) and remainder == b'' and decoded.val == obj.val - -= Encode and decode random text string -from scapy.cbor import RandCBORObject, CBOR_TEXT_STRING, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_TEXT_STRING]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_TEXT_STRING) and remainder == b'' and decoded.val == obj.val - -= Encode and decode random byte string -from scapy.cbor import RandCBORObject, CBOR_BYTE_STRING, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_BYTE_STRING]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_BYTE_STRING) and remainder == b'' and decoded.val == obj.val - -= Encode and decode random array -from scapy.cbor import RandCBORObject, CBOR_ARRAY, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_ARRAY]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_ARRAY) and remainder == b'' and len(decoded.val) == len(obj.val) - -= Encode and decode random map -from scapy.cbor import RandCBORObject, CBOR_MAP, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_MAP]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_MAP) and remainder == b'' and len(decoded.val) == len(obj.val) - -= Encode and decode random float -from scapy.cbor import RandCBORObject, CBOR_FLOAT, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_FLOAT]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_FLOAT) and remainder == b'' - -+ CBOR Random Mixed Types - -= Generate multiple random objects of different types -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -objects = [rand._fix() for _ in range(10)] -len(objects) == 10 and all(hasattr(obj, 'val') for obj in objects) - -= Encode and decode multiple random objects -from scapy.cbor import RandCBORObject, CBOR_Codecs -rand = RandCBORObject() -success_count = 0 -for _ in range(20): - obj = rand._fix() - try: - encoded = bytes(obj) - decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) - if remainder == b'': - success_count += 1 - except: - pass +pkt = AnyMapPkt2(b"\x82\xa1\x01\x02\x00") +assert isinstance(pkt.a, CBOR_MAP) +assert isinstance(pkt.a.val, CBORMapData) +assert pkt.a.val[1].val == 2 +pkt.b = 1 +assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" -success_count >= 18 - -= Random nested arrays encode/decode correctly -from scapy.cbor import RandCBORObject, CBOR_ARRAY, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_ARRAY]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_ARRAY) and remainder == b'' - -= Random nested maps encode/decode correctly -from scapy.cbor import RandCBORObject, CBOR_MAP, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_MAP]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_MAP) and remainder == b'' - -+ CBOR Fuzzing Stress Tests - -= Generate 100 random objects without errors -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -objects = [] -for _ in range(100): - obj = None - try: - obj = rand._fix() - except: - pass - if obj is not None: - objects.append(obj) += In-place mutation of CBORF_ANY list invalidates raw cache +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet -len(objects) >= 95 +class AnyRoot(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= Encode 50 random objects without errors -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -encoded_count = 0 -for _ in range(50): - obj = rand._fix() - try: - encoded = bytes(obj) - if len(encoded) > 0: - encoded_count += 1 - except: - pass +pkt = AnyRoot(b"\x82\x01\x02") +assert pkt.raw_packet_cache == b"\x82\x01\x02" +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) +assert bytes(pkt) == b"\x83\x01\x02\x03" -encoded_count >= 45 += Nested packet in-place ANY mutation invalidates the parent raw cache +from scapy.cbor.cborfields import CBORF_ANY, CBORF_PACKET +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet -= Roundtrip 50 random objects -from scapy.cbor import RandCBORObject, CBOR_Codecs -rand = RandCBORObject() -roundtrip_count = 0 -for _ in range(50): - obj = rand._fix() - try: - encoded = bytes(obj) - decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) - if remainder == b'': - roundtrip_count += 1 - except: - pass +class NestedAnyChild(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -roundtrip_count >= 45 +class NestedAnyParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, NestedAnyChild) -########### CBOR Fields ########################################### +pkt = NestedAnyParent(bytes.fromhex("8101")) +pkt.child.value.val.append(CBOR_UNSIGNED_INTEGER(2)) +assert bytes(pkt.child) == bytes.fromhex("820102") +assert bytes(pkt) == bytes.fromhex("820102") -+ CBORF scalar fields - CBORF_UNSIGNED_INTEGER += ARRAY_OF(CBORF_ANY) same-value float mutation invalidates raw cache +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_ANY +from scapy.cborpacket import CBOR_Packet -= CBORF_UNSIGNED_INTEGER basic encode/decode -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +class AnyArrayFloatPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], CBORF_ANY) + +pkt = AnyArrayFloatPkt(bytes.fromhex("81f90000")) +pkt.values[0].val = -0.0 +assert bytes(pkt) == bytes.fromhex("81f98000") + += Packet-valued ARRAY_OF and REMAINDER_OF fingerprint copy and wire cache +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY_OF, + CBORF_PACKET, + CBORF_ITEMS, + CBORF_REMAINDER_OF, +) +from scapy.cbor.cbor import CBOR_FLOAT, CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class PktUInt(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER("value", 42) +class NestedAnyChild(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -pkt = PktUInt() -assert pkt.value.val == 42 -raw_data = bytes(pkt) -pkt2 = PktUInt(raw_data) -assert pkt2.value.val == 42 +# Packet-class constructor +class ArrayOfChildCls(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("items", [], NestedAnyChild) -= CBORF_UNSIGNED_INTEGER zero value -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER -from scapy.cborpacket import CBOR_Packet +# Packet-field-instance constructor +class ArrayOfChildFld(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "items", [], CBORF_PACKET("item", None, NestedAnyChild) + ) -class PktUIntZero(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) +class SeqOfChildCls(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_REMAINDER_OF("items", [], NestedAnyChild), + ) -pkt = PktUIntZero() -raw_data = bytes(pkt) -assert raw_data == b'\x00' -pkt2 = PktUIntZero(raw_data) -assert pkt2.value.val == 0 +class SeqOfChildFld(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_REMAINDER_OF( + "items", [], CBORF_PACKET("item", None, NestedAnyChild) + ), + ) -= CBORF_UNSIGNED_INTEGER large value roundtrip -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER +wire = bytes.fromhex("81f90000") +for cls in (ArrayOfChildCls, ArrayOfChildFld): + assert cls.CBOR_root.holds_packets == 1 + pkt = cls(wire) + assert bytes(pkt) == wire + pkt.items[0].value.val = -0.0 + assert bytes(pkt) == bytes.fromhex("81f98000") + +for cls in (ArrayOfChildCls, ArrayOfChildFld): + pkt = cls(wire) + pkt.items[0].value = CBOR_FLOAT(0.0) + assert getattr(pkt.items[0].value, "_encoded", None) is None + assert bytes(pkt) == bytes.fromhex("81f90000") + +for cls in (ArrayOfChildCls, ArrayOfChildFld): + pkt = cls(bytes.fromhex("81820102")) + pkt.items[0].value.val.append(CBOR_UNSIGNED_INTEGER(3)) + assert bytes(pkt) == bytes.fromhex("8183010203") + +for cls in (ArrayOfChildCls, ArrayOfChildFld): + pkt = cls(wire) + clone = pkt.copy() + assert clone.items[0].parent is clone + clone.items[0].value.val = -0.0 + assert bytes(pkt) == wire + assert bytes(clone) == bytes.fromhex("81f98000") + +seq_wire = bytes.fromhex("f90000") +for cls in (SeqOfChildCls, SeqOfChildFld): + assert cls.CBOR_root.seq[0].holds_packets == 1 + pkt = cls(seq_wire) + assert bytes(pkt) == seq_wire + pkt.items[0].value.val = -0.0 + assert bytes(pkt) == bytes.fromhex("f98000") + += Unknown map float mutation invalidates raw cache +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER, CBORF_optional from scapy.cborpacket import CBOR_Packet -class PktUIntLarge(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER("value", 1000000) - -pkt = PktUIntLarge() -raw_data = bytes(pkt) -pkt2 = PktUIntLarge(raw_data) -assert pkt2.value.val == 1000000 - -+ CBORF scalar fields - CBORF_NEGATIVE_INTEGER +class UnknownFloatMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_optional(CBORF_UNSIGNED_INTEGER("a", None)), + ) -= CBORF_NEGATIVE_INTEGER basic encode/decode -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +pkt = UnknownFloatMap(bytes.fromhex("a16178f90000")) +assert pkt._cbor_unknown[0][0] == "x" +pkt._cbor_unknown[0][1].val = -0.0 +assert bytes(pkt) == bytes.fromhex("a16178f98000") + += Typed map lookup distinguishes integer 1 from boolean True +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cbor import CBORMapData + +wire = b"\xa2\x01\x61a\xf5\x61b" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +assert isinstance(obj.val, CBORMapData) +assert obj.val[1].val == "a" +assert obj.val[True].val == "b" +assert obj.val[1] is not obj.val[True] + += CBORMapData equality with dict keeps True and 1 distinct +from scapy.cbor.cbor import CBORMapData, CBOR_TRUE, CBOR_UNSIGNED_INTEGER + +m = CBORMapData([(CBOR_TRUE(), "a"), (CBOR_UNSIGNED_INTEGER(1), "b")]) +# Python dict cannot hold both True and 1; equality must not collapse them. +assert m != {True: "b"} +assert m != {1: "b"} +assert m != {True: "a"} +assert m == CBORMapData([(CBOR_TRUE(), "a"), (CBOR_UNSIGNED_INTEGER(1), "b")]) +assert len(dict(m.items())) == 1 + ++ optional major-type-7 lookahead and absence + += Optional boolean leaves a required float for the next field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_FLOAT, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class PktNInt(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER("value", -1) - -pkt = PktNInt() -assert pkt.value.val == -1 -raw_data = bytes(pkt) -pkt2 = PktNInt(raw_data) -assert pkt2.value.val == -1 +class OptBoolFloat(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("flag", None)), + CBORF_FLOAT("num", 0.0), + ) -= CBORF_NEGATIVE_INTEGER -100 roundtrip -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +pkt = OptBoolFloat(b"\x81\xf9\x3e\x00") # [1.5] as float16 +assert pkt.flag is CBOR_ABSENT +assert abs(pkt.num - 1.5) < 1e-6 + += Optional boolean leaves a required null for the next field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_NULL, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class PktNInt100(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER("value", -100) +class OptBoolNull(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("flag", None)), + CBORF_NULL("nil"), + ) -pkt = PktNInt100() -raw_data = bytes(pkt) -pkt2 = PktNInt100(raw_data) -assert pkt2.value.val == -100 +pkt = OptBoolNull(b"\x81\xf6") +assert pkt.flag is CBOR_ABSENT +assert pkt.nil is None + += Optional null leaves a required boolean for the next field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_NULL, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -+ CBORF scalar fields - CBORF_INTEGER +class OptNullBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_NULL("nil")), + CBORF_BOOLEAN("flag", False), + ) -= CBORF_INTEGER positive value -from scapy.cbor.cborfields import CBORF_INTEGER +pkt = OptNullBool(b"\x81\xf5") +assert pkt.nil is CBOR_ABSENT +assert pkt.flag is True + += Optional undefined leaves a required float for the next field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_FLOAT, + CBORF_UNDEFINED, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class PktInt(CBOR_Packet): - CBOR_root = CBORF_INTEGER("value", 7) - -pkt = PktInt() -raw_data = bytes(pkt) -pkt2 = PktInt(raw_data) -assert pkt2.value.val == 7 +class OptUndefFloat(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNDEFINED("u")), + CBORF_FLOAT("num", 0.0), + ) -= CBORF_INTEGER negative value -from scapy.cbor.cborfields import CBORF_INTEGER +pkt = OptUndefFloat(b"\x81\xf9\x3e\x00") +assert pkt.u is CBOR_ABSENT +assert abs(pkt.num - 1.5) < 1e-6 + += Optional float leaves a required boolean for the next field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_FLOAT, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class PktIntNeg(CBOR_Packet): - CBOR_root = CBORF_INTEGER("value", -5) +class OptFloatBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_FLOAT("num", None)), + CBORF_BOOLEAN("flag", False), + ) -pkt = PktIntNeg() -raw_data = bytes(pkt) -pkt2 = PktIntNeg(raw_data) -assert pkt2.value.val == -5 +pkt = OptFloatBool(b"\x81\xf4") +assert pkt.num is CBOR_ABSENT +assert pkt.flag is False + += Absent optional ANY stays absent after cache invalidation +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -+ CBORF scalar fields - CBORF_BYTE_STRING +class OptAnyTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_optional(CBORF_ANY("extra", None)), + ) -= CBORF_BYTE_STRING basic encode/decode -from scapy.cbor.cborfields import CBORF_BYTE_STRING +pkt = OptAnyTail(b"\x81\x00") +assert pkt.extra is CBOR_ABSENT +pkt.n = 1 +assert bytes(pkt) == b"\x81\x01" + += Absent optional map members stay absent after rebuild +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_INTEGER, + CBORF_MAP, + CBORF_NULL, + CBORF_SEMANTIC_TAG, + CBORF_TEXT_STRING, + CBORF_UNDEFINED, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class PktBStr(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING("data", b"hello") - -pkt = PktBStr() -assert pkt.data.val == b"hello" -raw_data = bytes(pkt) -pkt2 = PktBStr(raw_data) -assert pkt2.data.val == b"hello" +class OptMapPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_INTEGER("n", 0), + CBORF_optional(CBORF_ANY("any", None)), + CBORF_optional(CBORF_NULL("nil")), + CBORF_optional(CBORF_UNDEFINED("u")), + CBORF_optional(CBORF_SEMANTIC_TAG(1, CBORF_INTEGER("ts", 0))), + CBORF_optional(CBORF_TEXT_STRING("endpoint", "default")), + ) -= CBORF_BYTE_STRING empty bytes -from scapy.cbor.cborfields import CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet +pkt = OptMapPkt(b"\xa1\x61n\x00") +assert pkt.any is CBOR_ABSENT +assert pkt.nil is CBOR_ABSENT +assert pkt.u is CBOR_ABSENT +assert pkt.ts is CBOR_ABSENT +assert pkt.endpoint is CBOR_ABSENT +pkt.n = 1 +assert bytes(pkt) == b"\xa1\x61n\x01" -class PktBStrEmpty(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING("data", b"") ++ array item reservation -pkt = PktBStrEmpty() -raw_data = bytes(pkt) -assert raw_data == b'\x40' -pkt2 = PktBStrEmpty(raw_data) -assert pkt2.data.val == b"" += Nonterminal REMAINDER_OF is rejected at schema construction +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) -+ CBORF scalar fields - CBORF_TEXT_STRING +try: + CBORF_ARRAY( + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + assert False, "nonterminal REMAINDER_OF was accepted" +except ValueError: + pass + += Nested REMAINDER_OF inside ITEMS is rejected by outer ARRAY +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) -= CBORF_TEXT_STRING basic encode/decode -from scapy.cbor.cborfields import CBORF_TEXT_STRING +try: + CBORF_ARRAY( + CBORF_ITEMS( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_REMAINDER_OF( + "items", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ), + ), + CBORF_TEXT_STRING("tail", ""), + ) + assert False, "nested non-direct REMAINDER_OF was accepted" +except ValueError: + pass + += Nested REMAINDER_OF inside framed ARRAY is allowed with an outer sibling +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_REMAINDER_OF, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class PktTStr(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING("title", "hello") +class NestedDefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ARRAY( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_TEXT_STRING("tail", ""), + ) -pkt = PktTStr() -assert pkt.title.val == "hello" -raw_data = bytes(pkt) -pkt2 = PktTStr(raw_data) -assert pkt2.title.val == "hello" +class NestedIndefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ARRAY_INDEFINITE( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_TEXT_STRING("tail", ""), + ) -= CBORF_TEXT_STRING empty string -from scapy.cbor.cborfields import CBORF_TEXT_STRING +# Definite outer [[1, 2, 3], "tail"] +wire_def = bytes.fromhex("8283010203647461696c") +pkt = NestedDefinite(wire_def) +assert pkt.values == [1, 2, 3] +assert pkt.tail == "tail" +assert bytes(pkt) == wire_def + +# Indefinite inner array: [ [_ 1 2 3 break], "tail" ] +wire_indef = ( + b"\x82" + + b"\x9f\x01\x02\x03\xff" + + b"\x64tail" +) +pkt = NestedIndefinite(wire_indef) +assert pkt.values == [1, 2, 3] +assert pkt.tail == "tail" +# Rebuild uses indefinite encoding for the inner ARRAY_INDEFINITE schema. +assert bytes(pkt) == wire_indef + += Nested REMAINDER_OF inside ARRAY under ITEMS is allowed with an outer sibling +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class PktTStrEmpty(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING("title", "") - -pkt = PktTStrEmpty() -raw_data = bytes(pkt) -assert raw_data == b'\x60' -pkt2 = PktTStrEmpty(raw_data) -assert pkt2.title.val == "" - -+ CBORF scalar fields - CBORF_BOOLEAN +class NestedUnderItems(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_ARRAY( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= CBORF_BOOLEAN true value -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cbor.cbor import CBOR_TRUE +# Framed array [1,2,3] then sibling unsigned 4 +wire = bytes.fromhex("8301020304") +pkt = NestedUnderItems(wire) +assert pkt.values == [1, 2, 3] +assert pkt.tail == 4 +assert bytes(pkt) == wire + += Nested REMAINDER_OF inside indefinite ARRAY under ITEMS is allowed with an outer sibling +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class PktBool(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN("flag", True) +class NestedIndefUnderItems(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_ARRAY_INDEFINITE( + CBORF_REMAINDER_OF( + "values", [], + pkt_cls=CBORF_UNSIGNED_INTEGER, + ) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -pkt = PktBool() -assert isinstance(pkt.flag, CBOR_TRUE) -raw_data = bytes(pkt) -assert raw_data == b'\xf5' -pkt2 = PktBool(raw_data) -assert isinstance(pkt2.flag, CBOR_TRUE) +# Indefinite inner array [_ 1 2 3 break] then sibling unsigned 4 +wire = b"\x9f\x01\x02\x03\xff\x04" +pkt = NestedIndefUnderItems(wire) +assert pkt.values == [1, 2, 3] +assert pkt.tail == 4 +assert bytes(pkt) == wire -= CBORF_BOOLEAN false value -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cbor.cbor import CBOR_FALSE += Empty CBOR_Packet without CBOR_root is rejected by ARRAY_OF and PACKET +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_PACKET from scapy.cborpacket import CBOR_Packet -class PktBoolFalse(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN("flag", False) +class EmptyCBOR(CBOR_Packet): + pass -pkt = PktBoolFalse() -raw_data = bytes(pkt) -assert raw_data == b'\xf4' -pkt2 = PktBoolFalse(raw_data) -assert isinstance(pkt2.flag, CBOR_FALSE) +try: + CBORF_ARRAY_OF("items", [], pkt_cls=EmptyCBOR) + assert False, "EmptyCBOR accepted by ARRAY_OF" +except ValueError: + pass -+ CBORF scalar fields - CBORF_FLOAT +try: + CBORF_PACKET("child", None, EmptyCBOR) + assert False, "EmptyCBOR accepted by PACKET" +except ValueError: + pass + += Generic CBOR_Object fingerprint recurses into mutable .val +from scapy.cbor.cbor import CBOR_Object, CBOR_UNSIGNED_INTEGER +from scapy.cbor.cborfields import CBORF_ANY + +class MutableListObj(CBOR_Object): + tag = None # type: ignore + +obj = MutableListObj([CBOR_UNSIGNED_INTEGER(1)]) +snap = CBORF_ANY._cache_fingerprint(obj) +obj.val.append(CBOR_UNSIGNED_INTEGER(2)) +# Snapshot must not alias .val; otherwise in-place mutation keeps equality. +assert CBORF_ANY._cache_fingerprint(obj) != snap + += Optional cannot wrap REMAINDER_OF +from scapy.cbor.cborfields import ( + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) -= CBORF_FLOAT encode/decode -from scapy.cbor.cborfields import CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet +try: + CBORF_optional( + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER) + ) + assert False, "optional REMAINDER_OF was accepted" +except ValueError: + pass -class PktFloat(CBOR_Packet): - CBOR_root = CBORF_FLOAT("value", 1.5) += Conditional cannot wrap REMAINDER_OF +from scapy.cbor.cborfields import ( + CBORF_CONDITIONAL, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) -pkt = PktFloat() -raw_data = bytes(pkt) -pkt2 = PktFloat(raw_data) -assert abs(pkt2.value.val - 1.5) < 1e-9 +try: + CBORF_CONDITIONAL( + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + lambda pkt: True, + ) + assert False, "conditional REMAINDER_OF was accepted" +except ValueError: + pass -= CBORF_NULL encode/decode -from scapy.cbor.cborfields import CBORF_NULL -from scapy.cbor.cbor import CBOR_NULL -from scapy.cborpacket import CBOR_Packet += CBORF_SEMANTIC_TAG cannot wrap CBORF_REMAINDER_OF +from scapy.cbor.cborfields import ( + CBORF_SEMANTIC_TAG, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) -class PktNull(CBOR_Packet): - CBOR_root = CBORF_NULL("nothing") +try: + CBORF_SEMANTIC_TAG( + 1, + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) + assert False, "semantic-tagged REMAINDER_OF should be rejected" +except ValueError: + pass + += Optional semantic tag cannot hide CBORF_REMAINDER_OF +from scapy.cbor.cborfields import ( + CBORF_SEMANTIC_TAG, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) -pkt = PktNull() -raw_data = bytes(pkt) -assert raw_data == b'\xf6' -pkt2 = PktNull(raw_data) -assert isinstance(pkt2.nothing, CBOR_NULL) +try: + CBORF_optional( + CBORF_SEMANTIC_TAG( + 1, + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) + ) + assert False, "wrapped semantic-tagged REMAINDER_OF should be rejected" +except ValueError: + pass + += Tagged scalar and ARRAY_OF remain valid semantic-tag content +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_OF, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -+ CBORF scalar fields - CBORF_UNDEFINED +class TaggedInt(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + 1, CBORF_UNSIGNED_INTEGER("value", 0) + ) -= CBORF_UNDEFINED encode/decode -from scapy.cbor.cborfields import CBORF_UNDEFINED -from scapy.cbor.cbor import CBOR_UNDEFINED -from scapy.cborpacket import CBOR_Packet +pkt = TaggedInt(b"\xc1\x0a") +assert pkt.value == 10 +assert bytes(pkt) == b"\xc1\x0a" -class PktUndef(CBOR_Packet): - CBOR_root = CBORF_UNDEFINED("undef") +class TaggedArrayOf(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + 1, + CBORF_ARRAY_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) -pkt = PktUndef() -raw_data = bytes(pkt) -assert raw_data == b'\xf7' -pkt2 = PktUndef(raw_data) -assert isinstance(pkt2.undef, CBOR_UNDEFINED) +pkt = TaggedArrayOf(b"\xc1\x82\x01\x02") +assert pkt.vals == [1, 2] +assert bytes(pkt) == b"\xc1\x82\x01\x02" + +class TaggedStruct(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + 1, + CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_UNSIGNED_INTEGER("b", 0), + ), + ) -+ CBORF structured fields - CBORF_ARRAY +pkt = TaggedStruct(b"\xc1\x82\x01\x02") +assert pkt.a == 1 and pkt.b == 2 +assert bytes(pkt) == b"\xc1\x82\x01\x02" -= CBORF_ARRAY two-field encode/decode -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING += Direct terminal REMAINDER_OF respects definite array item budget +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet +from scapy.packet import Raw -class MyCBOR(CBOR_Packet): +class DirectTerminal(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("version", 1), - CBORF_TEXT_STRING("title", "test"), + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -pkt = MyCBOR() -assert pkt.version.val == 1 -assert pkt.title.val == "test" -raw_data = bytes(pkt) -pkt2 = MyCBOR(raw_data) -assert pkt2.version.val == 1 -assert pkt2.title.val == "test" - -= CBORF_ARRAY three-field encode/decode -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN +pkt = DirectTerminal() +result = DirectTerminal.CBOR_root._dissect_counted( + pkt, b"\x82\x01\x0a\x0b" +) +assert pkt.n == 1 +assert pkt.vals == [10] +assert result.remaining == b"\x0b" + +pkt = DirectTerminal(b"\x82\x01\x0a\x0b") +assert pkt.n == 1 +assert pkt.vals == [10] +assert isinstance(pkt.payload, Raw) and pkt.payload.load == b"\x0b" + += Direct terminal REMAINDER_OF in indefinite ARRAY stops at break +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class Multi(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("id", 99), - CBORF_TEXT_STRING("label", "x"), - CBORF_BOOLEAN("active", True), +class DirectTerminalIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -pkt = Multi() -raw_data = bytes(pkt) -pkt2 = Multi(raw_data) -assert pkt2.id.val == 99 -assert pkt2.label.val == "x" +pkt = DirectTerminalIndef(b"\x9f\x01\x0a\x0b\xff") +assert pkt.n == 1 +assert pkt.vals == [10, 11] -= CBORF_ARRAY single integer roundtrip -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER += Empty terminal REMAINDER_OF leaves an empty list +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class Single(CBOR_Packet): +class EmptyTerminal(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER("count", 5), + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), ) -pkt = Single() -raw_data = bytes(pkt) -pkt2 = Single(raw_data) -assert pkt2.count.val == 5 - -+ CBORF structured fields - CBORF_ARRAY_OF - -= CBORF_ARRAY_OF with CBORF_INTEGER elements -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER -from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER +pkt = EmptyTerminal(b"\x81\x05") +assert pkt.n == 5 +assert pkt.vals == [] + += Optional before trailing REMAINDER_OF does not invent a reserved count +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class ArrOfInt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF("items", [], CBORF_INTEGER) +class OptThenSeqOf(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) -pkt = ArrOfInt() -pkt.items = [CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(2), CBOR_UNSIGNED_INTEGER(3)] -raw_data = bytes(pkt) -pkt2 = ArrOfInt(raw_data) -assert len(pkt2.items) == 3 -assert pkt2.items[0].val == 1 -assert pkt2.items[2].val == 3 +# Terminal REMAINDER_OF reserves nothing; same-type optional may take the item. +pkt = OptThenSeqOf(b"\x81\x07") +assert pkt.opt == 7 +assert pkt.vals == [] -+ CBORF structured fields - CBORF_MAP +class BoolThenSeqOf(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("opt", None)), + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + ) -= CBORF_MAP basic encode/decode -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING +# Type mismatch leaves the item for the trailing REMAINDER_OF. +pkt = BoolThenSeqOf(b"\x81\x07") +assert pkt.opt is CBOR_ABSENT +assert pkt.vals == [7] + += Conditional trailing field follows a false discriminator with optional present +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_CONDITIONAL, + CBORF_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet -class MyMap(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER("version", 2), - CBORF_TEXT_STRING("title", "cbor"), +class ConditionalOpt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("flag", 1), + CBORF_optional(CBORF_INTEGER("optional", None)), + CBORF_CONDITIONAL( + CBORF_INTEGER("last", 8), lambda p: p.flag == 1 + ), ) -pkt = MyMap() -assert pkt.version.val == 2 -assert pkt.title.val == "cbor" -raw_data = bytes(pkt) -pkt2 = MyMap(raw_data) -assert pkt2.version.val == 2 -assert pkt2.title.val == "cbor" - -= CBORF_MAP byte string value -from scapy.cbor.cborfields import CBORF_MAP, CBORF_BYTE_STRING +wire = bytes(ConditionalOpt(flag=0, optional=3)) +pkt = ConditionalOpt(wire) +assert pkt.flag == 0 +assert pkt.optional == 3 + += Optional same-type scalar reserves the sole item for a required tail +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class BinMap(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_BYTE_STRING("data", b"\xde\xad\xbe\xef"), +class OptThenReq(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_UNSIGNED_INTEGER("req", 0), ) -pkt = BinMap() -raw_data = bytes(pkt) -pkt2 = BinMap(raw_data) -assert pkt2.data.val == b"\xde\xad\xbe\xef" - -+ CBORF complex fields - CBORF_optional - -= CBORF_optional present field -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +pkt = OptThenReq(b"\x81\x07") +assert pkt.opt is CBOR_ABSENT +assert pkt.req == 7 + += Active conditional reserves against a same-type optional +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_CONDITIONAL, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class OptPkt(CBOR_Packet): +class ConditionalTail(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("version", 1), - CBORF_optional(CBORF_TEXT_STRING("title", "")), + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("req", 0), + lambda pkt: pkt.flag == 1, + ), ) -pkt = OptPkt() -raw_data = bytes(pkt) -pkt2 = OptPkt(raw_data) -assert pkt2.version.val == 1 -assert pkt2.title.val == "" - -+ CBORF_PACKET nested packet - -= CBORF_PACKET basic nesting -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET +pkt = ConditionalTail(b"\x82\x01\x07") +assert pkt.flag == 1 +assert pkt.opt is CBOR_ABSENT +assert pkt.req == 7 + += Skipped definite-only optional does not poison a required bytestring +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BYTE_STRING, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class Inner(CBOR_Packet): +class OptionalDefinite(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("x", 10), + CBORF_optional( + CBORF_BYTE_STRING("opt", None, definite_only=True) + ), + CBORF_BYTE_STRING("value", b""), ) -class Outer(CBOR_Packet): +pkt = OptionalDefinite(b"\x81\x5f\x41a\xff") +assert pkt.opt is CBOR_ABSENT +assert pkt.value == b"a" + += Conditional disabled by default enables on wire with present value +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_CONDITIONAL, + CBORF_INTEGER, + CBORF_ITEMS, +) +from scapy.cborpacket import CBOR_Packet + +class CondOffArray(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING("label", "outer"), - CBORF_PACKET("inner", None, Inner), + CBORF_INTEGER("flag", 0), + CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), ) -inner = Inner() -outer = Outer() -outer.label = outer.label # keep default -outer.inner = inner -raw_data = bytes(outer) -outer2 = Outer(raw_data) -assert outer2.label.val == "outer" +class CondOffSeq(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_INTEGER("flag", 0), + CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), + ) -+ CBORF_SEMANTIC_TAG +class CondOffIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_INTEGER("flag", 0), + CBORF_CONDITIONAL(CBORF_INTEGER("last", 8), lambda p: p.flag == 1), + ) -= CBORF_SEMANTIC_TAG encode with inner integer -from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_INTEGER -from scapy.cbor.cbor import CBOR_SEMANTIC_TAG as CBOR_SEM +pkt = CondOffArray(b"\x82\x01\x08") +assert pkt.flag == 1 and pkt.last == 8 +pkt = CondOffSeq(b"\x01\x08") +assert pkt.flag == 1 and pkt.last == 8 +pkt = CondOffIndef(b"\x9f\x01\x08\xff") +assert pkt.flag == 1 and pkt.last == 8 + += Definite ARRAY rejects item count inconsistent with schema +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_INTEGER, + CBOR_Decoding_Error, +) from scapy.cborpacket import CBOR_Packet -class TaggedPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG("tag_info", None, 1, CBORF_INTEGER("ts", 0)) - -pkt = TaggedPkt() -# Build encodes tag 1 + inner field default -raw_data = bytes(pkt) -# Major type 6 (tag), tag number 1 => 0xc1 -assert raw_data[0:1] == b'\xc1' +class TwoIntsArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_INTEGER("a", 0), + CBORF_INTEGER("b", 0), + ) -+ CBOR_Packet / CBORF field integration +try: + TwoIntsArray(b"\x83\x01\x02\x03") + assert False, "inconsistent definite array count must raise" +except CBOR_Decoding_Error: + pass + += Indefinite array rejects nonterminal REMAINDER_OF +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) -= CBOR_Packet fields_desc built from CBORF_ARRAY -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBORF_TEXT_STRING +try: + CBORF_ARRAY_INDEFINITE( + CBORF_REMAINDER_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + assert False, "nonterminal REMAINDER_OF was accepted" +except ValueError: + pass + ++ Shared helpers + += Import follow-up test dependencies +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cbor import ( + CBOR_Decoding_Error, + CBOR_Encoding_Error, + CBORMapData, + CBOR_UNSIGNED_INTEGER, + CBOR_TEXT_STRING, + CBOR_FALSE, + CBOR_TRUE, + CBOR_FLOAT, +) +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + CBORcodec_ARRAY, +) +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_ARRAY_INDEFINITE, + CBORF_ARRAY_OF, + CBORF_BOOLEAN, + CBORF_CONDITIONAL, + CBORF_FLOAT, + CBORF_MAP, + CBORF_NULL, + CBORF_PACKET, + CBORF_SEMANTIC_TAG, + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_TEXT_STRING, + CBORF_UNDEFINED, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet -class Demo(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER("id", 1), - CBORF_TEXT_STRING("desc", "demo"), - ) +_RR_FLOAT_1_5 = b"\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00" -# fields_desc should contain both fields -field_names = [f.name for f in Demo.fields_desc] -assert "id" in field_names -assert "desc" in field_names -= CBOR_Packet roundtrip preserves raw bytes -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet ++ Finding 1: CBORF_ANY must preserve map identity -class Simple(CBOR_Packet): += A non-empty CBORF_ANY map remains a map after a sibling field changes +class RRAnyNonEmptyMap(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("a", 3), - CBORF_INTEGER("b", 7), + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), ) -pkt = Simple() -raw_data = bytes(pkt) -pkt2 = Simple(raw_data) -assert bytes(pkt2) == raw_data +wire = b"\x82\xa1\x01\x02\x00" +pkt = RRAnyNonEmptyMap(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" -########### Additional Unit Tests #################################### += An empty CBORF_ANY map never silently becomes an empty array +class RRAnyEmptyMap(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -+ CBOR Simple Values +pkt = RRAnyEmptyMap(b"\x82\xa0\x00") +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa0\x01" -= Decode CBOR simple value 0 -data = bytes.fromhex('e0') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -from scapy.cbor.cbor import CBOR_SIMPLE_VALUE -isinstance(obj, CBOR_SIMPLE_VALUE) and obj.val == 0 and remainder == b'' += A map nested in a CBORF_ANY array retains major type 5 on rebuild +class RRAnyNestedMap(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= Decode CBOR simple value 16 -data = bytes.fromhex('f0') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_SIMPLE_VALUE) and obj.val == 16 and remainder == b'' +wire = b"\x82\x81\xa1\x01\x02\x00" +pkt = RRAnyNestedMap(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\x81\xa1\x01\x02\x01" -= Decode CBOR simple value 255 (1-byte extended) -data = bytes.fromhex('f8ff') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_SIMPLE_VALUE) and obj.val == 255 and remainder == b'' += A map nested in a semantic tag retains map identity on rebuild +class RRAnyTaggedMap(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -+ CBOR Float Encodings - RFC 8949 Test Vectors +wire = b"\x82\xd8\x2a\xa1\x01\x02\x00" +pkt = RRAnyTaggedMap(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xd8\x2a\xa1\x01\x02\x01" -= Half-precision: positive zero (0xf90000) -import math -data = bytes.fromhex('f90000') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 0.0 and remainder == b'' - -= Half-precision: negative zero (0xf98000) -data = bytes.fromhex('f98000') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == -0.0 and math.copysign(1, obj.val) == -1.0 and remainder == b'' - -= Half-precision: 1.0 (0xf93c00) -data = bytes.fromhex('f93c00') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 1.0 and remainder == b'' - -= Half-precision: 1.5 (0xf93e00) -data = bytes.fromhex('f93e00') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 1.5 and remainder == b'' - -= Half-precision: max (65504.0) (0xf97bff) -data = bytes.fromhex('f97bff') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 65504.0 and remainder == b'' - -= Half-precision: smallest subnormal (0xf90001) -data = bytes.fromhex('f90001') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and abs(obj.val - 5.960464477539063e-8) < 1e-15 and remainder == b'' - -= Half-precision: smallest normal (0xf90400) -data = bytes.fromhex('f90400') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and abs(obj.val - 6.103515625e-5) < 1e-12 and remainder == b'' - -= Half-precision: positive infinity (0xf97c00) -data = bytes.fromhex('f97c00') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val > 0 and remainder == b'' - -= Half-precision: negative infinity (0xf9fc00) -data = bytes.fromhex('f9fc00') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val < 0 and remainder == b'' - -= Half-precision: NaN (0xf97e00) -data = bytes.fromhex('f97e00') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) and remainder == b'' - -= Single-precision: 100000.0 (0xfa47c35000) -data = bytes.fromhex('fa47c35000') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 100000.0 and remainder == b'' - -= Single-precision: max float32 (0xfa7f7fffff) -data = bytes.fromhex('fa7f7fffff') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and abs(obj.val - 3.4028234663852886e+38) < 1e30 and remainder == b'' - -= Single-precision: positive infinity (0xfa7f800000) -data = bytes.fromhex('fa7f800000') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val > 0 and remainder == b'' - -= Single-precision: NaN (0xfa7fc00000) -data = bytes.fromhex('fa7fc00000') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) and remainder == b'' - -= Double-precision: 1.1 (0xfb3ff199999999999a) -data = bytes.fromhex('fb3ff199999999999a') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and abs(obj.val - 1.1) < 1e-10 and remainder == b'' - -= Double-precision: 1.0e+300 (0xfb7e37e43c8800759c) -data = bytes.fromhex('fb7e37e43c8800759c') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and abs(obj.val - 1.0e+300) / 1.0e+300 < 1e-10 and remainder == b'' - -= Double-precision: NaN (0xfb7ff8000000000000) -data = bytes.fromhex('fb7ff8000000000000') -obj, remainder = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) and remainder == b'' - -+ CBOR Integer Encoding - RFC 8949 Test Vectors - -= RFC 8949: encode 0 -obj = CBOR_UNSIGNED_INTEGER(0) -bytes(obj) == bytes.fromhex('00') += A CBORF_ANY map with a compound array key round-trips faithfully +class RRAnyCompoundMapKey(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= RFC 8949: encode 1 -obj = CBOR_UNSIGNED_INTEGER(1) -bytes(obj) == bytes.fromhex('01') +wire = b"\x82\xa1\x81\x01\x02\x00" +pkt = RRAnyCompoundMapKey(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa1\x81\x01\x02\x01" -= RFC 8949: encode 10 -obj = CBOR_UNSIGNED_INTEGER(10) -bytes(obj) == bytes.fromhex('0a') += A CBORF_ANY map preserves integer 1 and Boolean true as distinct keys +class RRAnyTypedMapKeys(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= RFC 8949: encode 23 -obj = CBOR_UNSIGNED_INTEGER(23) -bytes(obj) == bytes.fromhex('17') +wire = b"\x82\xa2\x01\x61i\xf5\x61b\x00" +pkt = RRAnyTypedMapKeys(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa2\x01\x61i\xf5\x61b\x01" -= RFC 8949: encode 24 -obj = CBOR_UNSIGNED_INTEGER(24) -bytes(obj) == bytes.fromhex('1818') -= RFC 8949: encode 25 -obj = CBOR_UNSIGNED_INTEGER(25) -bytes(obj) == bytes.fromhex('1819') ++ Finding 2: optional major-type-7 lookahead must be exact -= RFC 8949: encode 100 -obj = CBOR_UNSIGNED_INTEGER(100) -bytes(obj) == bytes.fromhex('1864') += Optional Boolean does not consume a following floating-point value +class RROptionalBooleanThenFloat(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_FLOAT("value", None), + ) -= RFC 8949: encode 1000 -obj = CBOR_UNSIGNED_INTEGER(1000) -bytes(obj) == bytes.fromhex('1903e8') +pkt = RROptionalBooleanThenFloat(_RR_FLOAT_1_5) +assert pkt.value == 1.5 -= RFC 8949: encode 1000000 -obj = CBOR_UNSIGNED_INTEGER(1000000) -bytes(obj) == bytes.fromhex('1a000f4240') += Optional Boolean does not consume a following null +class RROptionalBooleanThenNull(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_NULL("value"), + ) -= RFC 8949: encode 1000000000000 -obj = CBOR_UNSIGNED_INTEGER(1000000000000) -bytes(obj) == bytes.fromhex('1b000000e8d4a51000') +pkt = RROptionalBooleanThenNull(b"\xf6") +assert bytes(pkt) == b"\xf6" -= RFC 8949: encode 18446744073709551615 (2^64-1) -obj = CBOR_UNSIGNED_INTEGER(18446744073709551615) -bytes(obj) == bytes.fromhex('1bffffffffffffffff') += Optional null does not consume a following Boolean +class RROptionalNullThenBoolean(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_NULL("maybe")), + CBORF_BOOLEAN("value", None), + ) -= RFC 8949: encode -1 -obj = CBOR_NEGATIVE_INTEGER(-1) -bytes(obj) == bytes.fromhex('20') +pkt = RROptionalNullThenBoolean(b"\xf5") +assert pkt.value is True -= RFC 8949: encode -10 -obj = CBOR_NEGATIVE_INTEGER(-10) -bytes(obj) == bytes.fromhex('29') += Optional undefined does not consume a following float +class RROptionalUndefinedThenFloat(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_UNDEFINED("maybe")), + CBORF_FLOAT("value", None), + ) -= RFC 8949: encode -100 -obj = CBOR_NEGATIVE_INTEGER(-100) -bytes(obj) == bytes.fromhex('3863') +pkt = RROptionalUndefinedThenFloat(_RR_FLOAT_1_5) +assert pkt.value == 1.5 -= RFC 8949: encode -1000 -obj = CBOR_NEGATIVE_INTEGER(-1000) -bytes(obj) == bytes.fromhex('3903e7') += Optional float does not consume a following Boolean +class RROptionalFloatThenBoolean(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_FLOAT("maybe", None)), + CBORF_BOOLEAN("value", None), + ) -= RFC 8949: decode 0 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('00')) -obj.val == 0 and remainder == b'' +pkt = RROptionalFloatThenBoolean(b"\xf4") +assert pkt.value is False -= RFC 8949: decode 23 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('17')) -obj.val == 23 and remainder == b'' += Exact major-type-7 matches are still consumed by optional fields +class RROptionalBooleanPresent(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("tail", None), + ) -= RFC 8949: decode 24 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('1818')) -obj.val == 24 and remainder == b'' +class RROptionalFloatPresent(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_FLOAT("maybe", None)), + CBORF_UNSIGNED_INTEGER("tail", None), + ) -= RFC 8949: decode 1000000000000 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('1b000000e8d4a51000')) -obj.val == 1000000000000 and remainder == b'' +boolean_pkt = RROptionalBooleanPresent(b"\xf5\x07") +assert boolean_pkt.maybe is True +assert boolean_pkt.tail == 7 -= RFC 8949: decode -1000 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('3903e7')) -obj.val == -1000 and remainder == b'' +float_pkt = RROptionalFloatPresent(_RR_FLOAT_1_5 + b"\x07") +assert float_pkt.maybe == 1.5 +assert float_pkt.tail == 7 -+ CBOR Byte String with All Byte Values += Optional Boolean lookahead recognizes half and single precision floats +for wire in (b"\xf9\x3e\x00", b"\xfa\x3f\xc0\x00\x00"): + pkt = RROptionalBooleanThenFloat(wire) + assert pkt.value == 1.5 -= CBOR_BYTE_STRING: encode/decode all 256 byte values -all_bytes = bytes(range(256)) -obj = CBOR_BYTE_STRING(all_bytes) -enc = bytes(obj) -dec, remainder = CBOR_Codecs.CBOR.dec(enc) -dec.val == all_bytes and remainder == b'' += Optional Boolean leaves direct and extended simple values for CBORF_ANY +class RROptionalBooleanThenAny(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("value", None), + ) -= CBOR_BYTE_STRING: cbor2 interop with all 256 byte values -import cbor2 -all_bytes = bytes(range(256)) -obj = CBOR_BYTE_STRING(all_bytes) -enc = bytes(obj) -dec = cbor2.loads(enc) -dec == all_bytes +from scapy.cbor.cbor import CBOR_SIMPLE_VALUE +for wire, expected in ((b"\xf0", 16), (b"\xf8\x20", 32)): + pkt = RROptionalBooleanThenAny(wire) + assert isinstance(pkt.value, CBOR_SIMPLE_VALUE) + assert pkt.value.val == expected + += Optional null and undefined do not consume each other's wire values +class RROptionalNullThenUndefined(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_NULL("maybe")), + CBORF_UNDEFINED("value"), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -+ CBOR Map with Integer Keys +class RROptionalUndefinedThenNull(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_UNDEFINED("maybe")), + CBORF_NULL("value"), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= Decode map with integer keys (cbor2 encode, Scapy decode) -import cbor2 -enc = cbor2.dumps({1: 'one', 2: 'two', -1: 'minus_one'}) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_MAP) and obj.val.get(1) is not None and obj.val[1].val == 'one' and remainder == b'' +undefined_pkt = RROptionalNullThenUndefined(b"\xf7\x00") +undefined_pkt.tail = 1 +assert bytes(undefined_pkt) == b"\xf7\x01" -= Encode map with integer keys (Scapy encode, cbor2 decode) -from scapy.cbor.cborcodec import CBORcodec_MAP -enc = CBORcodec_MAP.enc({1: 'one', 2: 'two'}) -dec = cbor2.loads(enc) -dec == {1: 'one', 2: 'two'} +null_pkt = RROptionalUndefinedThenNull(b"\xf6\x00") +null_pkt.tail = 1 +assert bytes(null_pkt) == b"\xf6\x01" -= Map with mixed key types roundtrip -enc = cbor2.dumps({'str_key': 42, 1: 'int_key'}) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_MAP) and len(obj.val) == 2 and remainder == b'' -+ CBOR Multiple Items in Stream ++ Finding 3: optional absence must be represented on every decode path -= Decode three integers from a single byte stream -data = bytes.fromhex('01') + bytes.fromhex('0a') + bytes.fromhex('17') -obj1, rest1 = CBOR_Codecs.CBOR.dec(data) -obj2, rest2 = CBOR_Codecs.CBOR.dec(rest1) -obj3, rest3 = CBOR_Codecs.CBOR.dec(rest2) -obj1.val == 1 and obj2.val == 10 and obj3.val == 23 and rest3 == b'' += Definite-array exhaustion marks a trailing optional CBORF_ANY absent +class RRAbsentAnyDefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_ANY("value", None)), + ) -= Decode integer followed by string -data = bytes.fromhex('1864') + bytes.fromhex('626869') -obj1, rest1 = CBOR_Codecs.CBOR.dec(data) -obj2, rest2 = CBOR_Codecs.CBOR.dec(rest1) -obj1.val == 100 and obj2.val == 'hi' and rest2 == b'' +pkt = RRAbsentAnyDefinite(b"\x81\x00") +pkt.head = 1 +assert bytes(pkt) == b"\x81\x01" -+ CBOR Nested Structures Unit Tests += Indefinite-array break marks a trailing optional CBORF_ANY absent +class RRAbsentAnyIndefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_ANY("value", None)), + ) -= Encode and decode doubly nested array -from scapy.cbor.cborcodec import CBORcodec_ARRAY -enc = CBORcodec_ARRAY.enc([[1, 2], [3, 4], [5, 6]]) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 and len(obj.val[0].val) == 2 and remainder == b'' +pkt = RRAbsentAnyIndefinite(b"\x9f\x00\xff") +pkt.head = 1 +assert bytes(pkt) == b"\x9f\x01\xff" -= Encode and decode map containing arrays -from scapy.cbor.cborcodec import CBORcodec_MAP, CBORcodec_ARRAY -enc = CBORcodec_MAP.enc({'nums': [1, 2, 3], 'strs': ['a', 'b']}) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_MAP) and 'nums' in obj.val and isinstance(obj.val['nums'], CBOR_ARRAY) and remainder == b'' += A missing optional fixed-map member remains omitted after rebuild +class RRAbsentAnyMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_optional(CBORF_ANY("b", None)), + ) -= Encode and decode array containing maps -from scapy.cbor.cborcodec import CBORcodec_ARRAY -enc = CBORcodec_ARRAY.enc([{'id': 1}, {'id': 2}]) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 2 and isinstance(obj.val[0], CBOR_MAP) and remainder == b'' +pkt = RRAbsentAnyMap(b"\xa1\x61a\x00") +pkt.a = 1 +assert bytes(pkt) == b"\xa1\x61a\x01" -########### Extended Interoperability Tests with cbor2 ################ += Optional null undefined and semantic-tag fields stay absent at array end +class RRAbsentNull(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_NULL("value")), + ) -+ CBOR Interoperability - RFC 8949 Appendix B (Scapy encode, cbor2 decode) +class RRAbsentUndefined(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_UNDEFINED("value")), + ) -= RFC 8949 Appendix B: 0 -import cbor2 -obj = CBOR_UNSIGNED_INTEGER(0) -cbor2.loads(bytes(obj)) == 0 +class RRAbsentTag(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional( + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", 7)) + ), + ) -= RFC 8949 Appendix B: 1 -obj = CBOR_UNSIGNED_INTEGER(1) -cbor2.loads(bytes(obj)) == 1 +for packet_cls in (RRAbsentNull, RRAbsentUndefined, RRAbsentTag): + pkt = packet_cls(b"\x81\x00") + pkt.head = 1 + assert bytes(pkt) == b"\x81\x01", packet_cls.__name__ -= RFC 8949 Appendix B: 10 -obj = CBOR_UNSIGNED_INTEGER(10) -cbor2.loads(bytes(obj)) == 10 += An absent optional scalar does not reappear from a non-None declared default +class RRAbsentDefaultScalar(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("value", 9)), + ) -= RFC 8949 Appendix B: 23 -obj = CBOR_UNSIGNED_INTEGER(23) -cbor2.loads(bytes(obj)) == 23 +pkt = RRAbsentDefaultScalar(b"\x81\x00") +pkt.head = 1 +assert bytes(pkt) == b"\x81\x01" -= RFC 8949 Appendix B: 24 -obj = CBOR_UNSIGNED_INTEGER(24) -cbor2.loads(bytes(obj)) == 24 += An absent optional packet does not reappear from its packet default +class RRAbsentPacketChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 9) -= RFC 8949 Appendix B: 1000 -obj = CBOR_UNSIGNED_INTEGER(1000) -cbor2.loads(bytes(obj)) == 1000 +class RRAbsentPacketParent(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional( + CBORF_PACKET( + "child", + RRAbsentPacketChild(value=9), + RRAbsentPacketChild, + ) + ), + ) -= RFC 8949 Appendix B: 1000000000000 -obj = CBOR_UNSIGNED_INTEGER(1000000000000) -cbor2.loads(bytes(obj)) == 1000000000000 +pkt = RRAbsentPacketParent(b"\x81\x00") +pkt.head = 1 +assert bytes(pkt) == b"\x81\x01" -= RFC 8949 Appendix B: 18446744073709551615 (max u64) -obj = CBOR_UNSIGNED_INTEGER(18446744073709551615) -cbor2.loads(bytes(obj)) == 18446744073709551615 += A missing optional fixed-map scalar does not reappear from its default +class RRAbsentDefaultMapScalar(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("b", 9)), + ) -= RFC 8949 Appendix B: -1 -obj = CBOR_NEGATIVE_INTEGER(-1) -cbor2.loads(bytes(obj)) == -1 +pkt = RRAbsentDefaultMapScalar(b"\xa1\x61a\x00") +pkt.a = 1 +assert bytes(pkt) == b"\xa1\x61a\x01" -= RFC 8949 Appendix B: -1000 -obj = CBOR_NEGATIVE_INTEGER(-1000) -cbor2.loads(bytes(obj)) == -1000 += A present optional CBOR null remains present after cache invalidation +class RRPresentOptionalNull(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", None)), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= RFC 8949 Appendix B: false -obj = CBOR_FALSE() -cbor2.loads(bytes(obj)) is False +pkt = RRPresentOptionalNull(b"\x82\xf6\x00") +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xf6\x01" -= RFC 8949 Appendix B: true -obj = CBOR_TRUE() -cbor2.loads(bytes(obj)) is True -= RFC 8949 Appendix B: null -obj = CBOR_NULL() -cbor2.loads(bytes(obj)) is None ++ Finding 4: positional arrays must reserve items for later required fields -= RFC 8949 Appendix B: undefined -obj = CBOR_UNDEFINED() -decoded = cbor2.loads(bytes(obj)) -from cbor2 import undefined -decoded is undefined += Definite arrays reject nonterminal REMAINDER_OF +try: + CBORF_ARRAY( + CBORF_REMAINDER_OF( + "values", + [], + CBORF_UNSIGNED_INTEGER("item", None), + ), + CBORF_UNSIGNED_INTEGER("tail", None), + ) + assert False, "nonterminal REMAINDER_OF accepted" +except ValueError: + pass -= RFC 8949 Appendix B: empty byte string -obj = CBOR_BYTE_STRING(b'') -cbor2.loads(bytes(obj)) == b'' += Indefinite arrays reject nonterminal REMAINDER_OF +try: + CBORF_ARRAY_INDEFINITE( + CBORF_REMAINDER_OF( + "values", + [], + CBORF_UNSIGNED_INTEGER("item", None), + ), + CBORF_UNSIGNED_INTEGER("tail", None), + ) + assert False, "nonterminal REMAINDER_OF accepted" +except ValueError: + pass -= RFC 8949 Appendix B: byte string b'\x01\x02\x03\x04' -obj = CBOR_BYTE_STRING(b'\x01\x02\x03\x04') -cbor2.loads(bytes(obj)) == b'\x01\x02\x03\x04' += An optional scalar yields a sole item to a required scalar of the same type +class RROptionalThenRequiredUnsigned(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNSIGNED_INTEGER("optional_value", None)), + CBORF_UNSIGNED_INTEGER("required_value", None), + ) -= RFC 8949 Appendix B: empty text string -obj = CBOR_TEXT_STRING('') -cbor2.loads(bytes(obj)) == '' +pkt = RROptionalThenRequiredUnsigned(b"\x81\x07") +assert pkt.required_value == 7 +pkt.required_value = 8 +assert bytes(pkt) == b"\x81\x08" -= RFC 8949 Appendix B: 'a' -obj = CBOR_TEXT_STRING('a') -cbor2.loads(bytes(obj)) == 'a' += An optional packet yields a sole item to a required packet +class RRBudgetChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", None) -= RFC 8949 Appendix B: 'IETF' -obj = CBOR_TEXT_STRING('IETF') -cbor2.loads(bytes(obj)) == 'IETF' +class RROptionalThenRequiredPacket(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_PACKET("optional_child", None, RRBudgetChild)), + CBORF_PACKET("required_child", None, RRBudgetChild), + ) -= RFC 8949 Appendix B: u00fc (ü) -obj = CBOR_TEXT_STRING('\u00fc') -cbor2.loads(bytes(obj)) == '\u00fc' +pkt = RROptionalThenRequiredPacket(b"\x81\x07") +assert pkt.required_child.value == 7 -= RFC 8949 Appendix B: u6c34 (water in Chinese) -obj = CBOR_TEXT_STRING('\u6c34') -cbor2.loads(bytes(obj)) == '\u6c34' += REMAINDER_OF before a required conditional field is rejected +try: + CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_REMAINDER_OF( + "values", + [], + CBORF_UNSIGNED_INTEGER("item", None), + ), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("tail", None), + lambda pkt: pkt.getfieldval("flag") == 1, + ), + ) + assert False, "nonterminal REMAINDER_OF was accepted" +except ValueError: + pass -= RFC 8949 Appendix B: empty array -from scapy.cbor.cborcodec import CBORcodec_ARRAY -enc = CBORcodec_ARRAY.enc([]) -cbor2.loads(enc) == [] -= RFC 8949 Appendix B: [1, 2, 3] -enc = CBORcodec_ARRAY.enc([1, 2, 3]) -cbor2.loads(enc) == [1, 2, 3] ++ Finding 5: recursive CBORF_ANY mutations must invalidate the raw cache -= RFC 8949 Appendix B: [1, [2, 3], [4, 5]] -enc = CBORcodec_ARRAY.enc([1, [2, 3], [4, 5]]) -cbor2.loads(enc) == [1, [2, 3], [4, 5]] += Appending to a decoded root CBORF_ANY array changes serialized bytes +from scapy.cbor.cbor import ( + CBOR_ARRAY, CBOR_MAP, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, + CBOR_UNSIGNED_INTEGER, CBORMapData, +) +class RRMutableAnyRoot(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= RFC 8949 Appendix B: empty map -from scapy.cbor.cborcodec import CBORcodec_MAP -enc = CBORcodec_MAP.enc({}) -cbor2.loads(enc) == {} +pkt = RRMutableAnyRoot(b"\x82\x01\x02") +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) +assert bytes(pkt) == b"\x83\x01\x02\x03" -= RFC 8949 Appendix B: {1: 2, 3: 4} -enc = CBORcodec_MAP.enc({1: 2, 3: 4}) -cbor2.loads(enc) == {1: 2, 3: 4} += Mutating a nested CBORF_ANY array changes serialized bytes +class RRMutableAnyNested(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= RFC 8949 Appendix B: {"a": 1, "b": [2, 3]} -enc = CBORcodec_MAP.enc({"a": 1, "b": [2, 3]}) -cbor2.loads(enc) == {"a": 1, "b": [2, 3]} +pkt = RRMutableAnyNested(b"\x82\x82\x01\x02\x00") +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) +assert bytes(pkt) == b"\x82\x83\x01\x02\x03\x00" + += Mutating the list inside a decoded semantic tag changes serialized bytes +pkt = RRMutableAnyRoot(b"\xd8\x2a\x82\x01\x02") +assert isinstance(pkt.value, CBOR_SEMANTIC_TAG) +pkt.value.val[1].val.append(CBOR_UNSIGNED_INTEGER(3)) +assert bytes(pkt) == b"\xd8\x2a\x83\x01\x02\x03" + += Mutating a decoded semantic tag number changes serialized bytes +pkt = RRMutableAnyRoot(b"\xd8\x2a\x01") +assert isinstance(pkt.value, CBOR_SEMANTIC_TAG) +pkt.value.val = (43, pkt.value.val[1]) +assert bytes(pkt) == b"\xd8\x2b\x01" + += Mutating a decoded extended simple value changes serialized bytes +pkt = RRMutableAnyRoot(b"\xf8\x20") +assert isinstance(pkt.value, CBOR_SIMPLE_VALUE) +pkt.value.val = 33 +assert bytes(pkt) == b"\xf8\x21" + += Mutating an array value inside a decoded map changes serialized bytes +pkt = RRMutableAnyRoot(b"\xa1\x61a\x81\x01") +assert isinstance(pkt.value, CBOR_MAP) +assert isinstance(pkt.value.val, CBORMapData) +pkt.value.val["a"].val.append(CBOR_UNSIGNED_INTEGER(2)) +assert bytes(pkt) == b"\xa1\x61a\x82\x01\x02" + + ++ Finding 7: generic map lookup must use typed CBOR key identity + += Typed lookup distinguishes unsigned integer 1 from Boolean true +obj, remaining = CBOR_Codecs.CBOR.dec(b"\xa2\x01\x61i\xf5\x61b") +assert remaining == b"" +map_data = obj.val +assert map_data[CBOR_UNSIGNED_INTEGER(1)].val == "i" +assert map_data[CBOR_TRUE()].val == "b" + += Typed lookup distinguishes unsigned integer 0 from Boolean false +obj, remaining = CBOR_Codecs.CBOR.dec(b"\xa2\x00\x61i\xf4\x61b") +assert remaining == b"" +map_data = obj.val +assert map_data[CBOR_UNSIGNED_INTEGER(0)].val == "i" +assert map_data[CBOR_FALSE()].val == "b" + += Typed lookup distinguishes unsigned integer 1 from floating-point 1.0 +obj, remaining = CBOR_Codecs.CBOR.dec( + b"\xa2\x01\x61i\xfb\x3f\xf0\x00\x00\x00\x00\x00\x00\x61f" +) +assert remaining == b"" +map_data = obj.val +assert map_data[CBOR_UNSIGNED_INTEGER(1)].val == "i" +assert map_data[CBOR_FLOAT(1.0)].val == "f" + + ++ Finding 10: nested packet builds must traverse each child schema once + += CBORF_PACKET builds a child root exactly once +class RRCountingArray(CBORF_ARRAY): + calls = 0 + def _build_counted(self, pkt): + type(self).calls += 1 + return super()._build_counted(pkt) + +class RRCountedChild(CBOR_Packet): + CBOR_root = RRCountingArray(CBORF_UNSIGNED_INTEGER("value", 1)) + +class RRCountedDirectParent(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_PACKET("child", None, RRCountedChild) + ) -+ CBOR Interoperability - RFC 8949 Appendix B (cbor2 encode, Scapy decode) +RRCountingArray.calls = 0 +bytes(RRCountedDirectParent(child=RRCountedChild(value=1))) +assert RRCountingArray.calls == 1 + += Packet-valued CBORF_ARRAY_OF builds each child root exactly once +class RRCountedArrayParent(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("children", [], RRCountedChild) + +RRCountingArray.calls = 0 +bytes(RRCountedArrayParent(children=[RRCountedChild(value=1)])) +assert RRCountingArray.calls == 1 + += Packet-valued CBORF_REMAINDER_OF builds each child root exactly once +class RRCountedSequenceParent(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF("children", [], RRCountedChild) + +RRCountingArray.calls = 0 +bytes(RRCountedSequenceParent(children=[RRCountedChild(value=1)])) +assert RRCountingArray.calls == 1 + + ++ Finding 11: decoder internals must not repeatedly copy unread suffixes + += Decoding a flat array has linear rather than quadratic suffix-copy volume +class RRSliceCountingBytes(bytes): + copied = 0 + slices = 0 + def __getitem__(self, key): + result = super().__getitem__(key) + if isinstance(key, slice) and isinstance(result, bytes): + type(self).copied += len(result) + type(self).slices += 1 + return type(self)(result) + return result + +wire = CBORcodec_ARRAY.enc([0] * 1024) + b"\x01" +RRSliceCountingBytes.copied = 0 +RRSliceCountingBytes.slices = 0 +obj, remaining = CBOR_Codecs.CBOR.dec(RRSliceCountingBytes(wire)) +assert len(obj.val) == 1024 +assert remaining == b"\x01" +assert RRSliceCountingBytes.copied <= len(wire) * 8, ( + "decoder copied %d bytes while consuming %d bytes" + % (RRSliceCountingBytes.copied, len(wire)) +) + + ++ Additional blind spots: fixed maps, mutable defaults, and simple values + += A fixed map skips an unknown nested indefinite value and decodes later keys +class RRKnownMapMember(CBOR_Packet): + CBOR_root = CBORF_MAP(CBORF_UNSIGNED_INTEGER("a", None)) + +wire = ( + b"\xa2" + b"\x61x" + b"\x9f\x01\xbf\x61k\x02\xff\xff" + b"\x61a\x07" +) +pkt = RRKnownMapMember(wire) +assert pkt.a == 7 + += A malformed unknown fixed-map value is not silently skipped +try: + RRKnownMapMember(b"\xa1\x61x\x9f\x01") + assert False, "Malformed unknown map content was silently accepted" +except (CBOR_Decoding_Error, CBOR_Codec_Decoding_Error): + pass -= RFC 8949 Appendix B decode: 0 -import cbor2 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(0)) -obj.val == 0 and isinstance(obj, CBOR_UNSIGNED_INTEGER) -= RFC 8949 Appendix B decode: 23 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(23)) -obj.val == 23 and isinstance(obj, CBOR_UNSIGNED_INTEGER) += Multi-map schemas require distinct unknown_field names +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet +try: + class BadTwoMaps(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_MAP(CBORF_UNSIGNED_INTEGER("a", 0)), + CBORF_MAP(CBORF_UNSIGNED_INTEGER("b", 0)), + ) + assert False, "duplicate default unknown_field accepted" +except ValueError: + pass + += Duplicate fixed-map schema names are rejected at class construction +try: + CBORF_MAP( + CBORF_UNSIGNED_INTEGER("duplicate", 0), + CBORF_TEXT_STRING("duplicate", ""), + ) + assert False, "Duplicate fixed-map field names were accepted" +except ValueError: + pass + += Nested mutable CBORF_ANY defaults are isolated between packet instances +from scapy.cbor.cbor import CBOR_ARRAY, CBOR_UNSIGNED_INTEGER, CBOR_SEMANTIC_TAG +class RRNestedMutableDefault(CBOR_Packet): + CBOR_root = CBORF_ANY("value", [[0]]) + +a = RRNestedMutableDefault() +b = RRNestedMutableDefault() +a.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(1)) +assert len(b.value.val[0].val) == 1 +assert b.value.val[0].val[0].val == 0 + += Mutable defaults are isolated without reading the sibling first +class RRMutableDefaultNoPeerRead(CBOR_Packet): + CBOR_root = CBORF_ANY("value", [[0]]) + +a = RRMutableDefaultNoPeerRead() +a.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(1)) +b = RRMutableDefaultNoPeerRead() +assert len(b.value.val[0].val) == 1 +assert b.value.val[0].val[0].val == 0 + += Mutable semantic-tag defaults are isolated between packet instances +class RRMutableTagDefault(CBOR_Packet): + CBOR_root = CBORF_ANY("value", CBOR_SEMANTIC_TAG((1, CBOR_ARRAY([])))) + +a = RRMutableTagDefault() +b = RRMutableTagDefault() +a.value.val[1].val.append(CBOR_UNSIGNED_INTEGER(1)) +assert len(b.value.val[1].val) == 0 + += Semantically duplicate map keys are rejected despite different encodings +try: + CBOR_Codecs.CBOR.dec(b"\xa2\x01\x00\x18\x01\x01") + assert False, "Equivalent unsigned-integer map keys were accepted twice" +except CBOR_Codec_Decoding_Error: + pass -= RFC 8949 Appendix B decode: 24 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(24)) -obj.val == 24 and isinstance(obj, CBOR_UNSIGNED_INTEGER) += Two adjacent unbounded positional sequences are rejected as ambiguous +try: + CBORF_ARRAY( + CBORF_REMAINDER_OF( + "left", + [], + CBORF_UNSIGNED_INTEGER("left_item", None), + ), + CBORF_REMAINDER_OF( + "right", + [], + CBORF_UNSIGNED_INTEGER("right_item", None), + ), + ) + assert False, "An inherently ambiguous array schema was accepted" +except ValueError: + pass + += A false conditional with a non-None default stays absent after rebuild +class RRConditionalDefaultAbsent(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("conditional_value", 9), + lambda pkt: pkt.getfieldval("flag") == 1, + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + +pkt = RRConditionalDefaultAbsent(b"\x82\x00\x07") +pkt.tail = 8 +assert bytes(pkt) == b"\x82\x00\x08" -= RFC 8949 Appendix B decode: -1 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(-1)) -obj.val == -1 and isinstance(obj, CBOR_NEGATIVE_INTEGER) += A false conditional fixed-map member stays absent after rebuild +class RRConditionalDefaultMapAbsent(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("conditional_value", 9), + lambda pkt: pkt.getfieldval("flag") == 1, + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -= RFC 8949 Appendix B decode: -1000 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(-1000)) -obj.val == -1000 and isinstance(obj, CBOR_NEGATIVE_INTEGER) +wire = b"\xa2\x64flag\x00\x64tail\x07" +pkt = RRConditionalDefaultMapAbsent(wire) +pkt.tail = 8 +assert bytes(pkt) == b"\xa2\x64flag\x00\x64tail\x08" + += Mutable CBORMapData defaults are isolated between packet instances +from scapy.cbor.cbor import CBOR_MAP, CBOR_UNSIGNED_INTEGER, CBORMapData, CBOR_TEXT_STRING +class RRMutableMapDefault(CBOR_Packet): + CBOR_root = CBORF_ANY( + "value", + CBORMapData([(CBOR_TEXT_STRING("a"), [])]), + ) -= RFC 8949 Appendix B decode: false -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(False)) -isinstance(obj, CBOR_FALSE) and obj.val is False +a = RRMutableMapDefault() +b = RRMutableMapDefault() +assert isinstance(a.value, CBOR_MAP) +a.value.val["a"].val.append(CBOR_UNSIGNED_INTEGER(1)) +assert len(b.value.val["a"].val) == 0 -= RFC 8949 Appendix B decode: true -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(True)) -isinstance(obj, CBOR_TRUE) and obj.val is True += Direct and extended simple values round-trip through CBORF_ANY +for wire in (b"\xf0", b"\xf8\x20", b"\xf8\xff"): + pkt = RRMutableAnyRoot(wire) + assert bytes(pkt) == wire -= RFC 8949 Appendix B decode: null -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(None)) -isinstance(obj, CBOR_NULL) and obj.val is None ++ Finding 1 - CBOR sentinel identity survives Scapy copying -= RFC 8949 Appendix B decode: empty string -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('')) -isinstance(obj, CBOR_TEXT_STRING) and obj.val == '' += CBOR_ABSENT and CBOR_UNDEFINED survive Packet.copy and deepcopy +import copy +from scapy.cbor import CBOR_UNDEFINED +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_optional, CBOR_ABSENT +from scapy.cborpacket import CBOR_Packet -= RFC 8949 Appendix B decode: 'IETF' -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('IETF')) -isinstance(obj, CBOR_TEXT_STRING) and obj.val == 'IETF' +class OptionalAnyCopy(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", CBOR_ABSENT)), + ) -= RFC 8949 Appendix B decode: u00fc -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('\u00fc')) -isinstance(obj, CBOR_TEXT_STRING) and obj.val == '\u00fc' +class UndefinedAnyCopy(CBOR_Packet): + CBOR_root = CBORF_ANY("value", CBOR_UNDEFINED()) -= RFC 8949 Appendix B decode: b'\x01\x02\x03\x04' -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(b'\x01\x02\x03\x04')) -isinstance(obj, CBOR_BYTE_STRING) and obj.val == b'\x01\x02\x03\x04' +absent = OptionalAnyCopy(b"\x80") +assert absent.getfieldval("value") is CBOR_ABSENT +assert absent.copy().getfieldval("value") is CBOR_ABSENT +assert copy.deepcopy(absent).getfieldval("value") is CBOR_ABSENT +assert bytes(absent.copy()) == b"\x80" -= RFC 8949 Appendix B decode: [1, 2, 3] -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps([1, 2, 3])) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 and obj.val[0].val == 1 +undefined = UndefinedAnyCopy(b"\xf7") +assert undefined.getfieldval("value") is CBOR_UNDEFINED() +assert undefined.copy().getfieldval("value") is CBOR_UNDEFINED() +assert copy.deepcopy(undefined).getfieldval("value") is CBOR_UNDEFINED() +assert bytes(undefined.copy()) == b"\xf7" -= RFC 8949 Appendix B decode: [1, [2, 3], [4, 5]] -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps([1, [2, 3], [4, 5]])) -isinstance(obj, CBOR_ARRAY) and len(obj.val) == 3 and isinstance(obj.val[1], CBOR_ARRAY) += CBOR structural sentinels preserve singleton identity under copy operations +import copy +from scapy.cbor.cbor import CBOR_NO_ITEM, CBOR_UNDEFINED +from scapy.cbor.cborfields import CBOR_ABSENT -= RFC 8949 Appendix B decode: {"a": 1, "b": [2, 3]} -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps({"a": 1, "b": [2, 3]})) -isinstance(obj, CBOR_MAP) and obj.val['a'].val == 1 and isinstance(obj.val['b'], CBOR_ARRAY) +for sentinel in (CBOR_ABSENT, CBOR_UNDEFINED(), CBOR_NO_ITEM): + assert copy.copy(sentinel) is sentinel + assert copy.deepcopy(sentinel) is sentinel -+ CBOR Interoperability - Byte-exact Comparison += Fresh optional ANY default is absent before any dissection occurs +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_optional, CBOR_ABSENT +from scapy.cborpacket import CBOR_Packet -= Scapy and cbor2 produce identical bytes for integer 0 -import cbor2 -bytes(CBOR_UNSIGNED_INTEGER(0)) == cbor2.dumps(0) +class OptionalAnyFreshDefault(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", CBOR_ABSENT)), + ) -= Scapy and cbor2 produce identical bytes for integer 255 -bytes(CBOR_UNSIGNED_INTEGER(255)) == cbor2.dumps(255) +fresh = OptionalAnyFreshDefault() +assert fresh.getfieldval("value") is CBOR_ABSENT +assert bytes(fresh) == b"\x80" +assert fresh.copy().getfieldval("value") is CBOR_ABSENT +assert bytes(fresh.copy()) == b"\x80" -= Scapy and cbor2 produce identical bytes for -1 -bytes(CBOR_NEGATIVE_INTEGER(-1)) == cbor2.dumps(-1) += Undefined values nested in a generic map survive packet copies +from scapy.cbor import CBOR_UNDEFINED +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet -= Scapy and cbor2 produce identical bytes for -1000 -bytes(CBOR_NEGATIVE_INTEGER(-1000)) == cbor2.dumps(-1000) +class AnyUndefinedMap(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= Scapy and cbor2 produce identical bytes for empty byte string -bytes(CBOR_BYTE_STRING(b'')) == cbor2.dumps(b'') +wire = b"\xa1\x61u\xf7" +pkt = AnyUndefinedMap(wire) +from scapy.cbor.cbor import CBOR_UNDEFINED +assert isinstance(pkt.value.val["u"], CBOR_UNDEFINED) +clone = pkt.copy() +assert isinstance(clone.value.val["u"], CBOR_UNDEFINED) +assert bytes(clone) == wire + ++ Finding 2 - Positional reservation must protect trailing required fields + += Zero-budget optional does not consume an item reserved for trailing CBORF_ANY +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -= Scapy and cbor2 produce identical bytes for 'hello' -bytes(CBOR_TEXT_STRING('hello')) == cbor2.dumps('hello') +class OptionalBoolThenAnyArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("required", CBOR_ABSENT), + ) -= Scapy and cbor2 produce identical bytes for true -bytes(CBOR_TRUE()) == cbor2.dumps(True) +# One array item is reserved for the required trailing field, so the +# optional Boolean stays absent even though the item is Boolean. +arr = OptionalBoolThenAnyArray(b"\x81\xf5") +assert arr.getfieldval("maybe") is CBOR_ABSENT +assert arr.required.val is True +assert bytes(arr) == b"\x81\xf5" + += Indefinite arrays reserve the final item for a required ANY field +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY_INDEFINITE, + CBORF_BOOLEAN, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -= Scapy and cbor2 produce identical bytes for false -bytes(CBOR_FALSE()) == cbor2.dumps(False) +class OptionalBoolThenAnyIndefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("required", CBOR_ABSENT), + ) -= Scapy and cbor2 produce identical bytes for null -bytes(CBOR_NULL()) == cbor2.dumps(None) +pkt = OptionalBoolThenAnyIndefinite(b"\x9f\xf5\xff") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required.val is True +assert bytes(pkt) == b"\x9f\xf5\xff" + += Optional ANY does not consume an item required by a trailing typed field +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -= Scapy and cbor2 produce identical bytes for undefined -from cbor2 import undefined -bytes(CBOR_UNDEFINED()) == cbor2.dumps(undefined) +class OptionalAnyThenBoolArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("maybe", CBOR_ABSENT)), + CBORF_BOOLEAN("required", None), + ) -= Scapy and cbor2 produce identical bytes for empty array -from scapy.cbor.cborcodec import CBORcodec_ARRAY -CBORcodec_ARRAY.enc([]) == cbor2.dumps([]) +arr = OptionalAnyThenBoolArray(b"\x81\xf5") +assert arr.getfieldval("maybe") is CBOR_ABSENT +assert arr.required is True + += Optional packet does not consume an item reserved for a trailing required ANY +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_PACKET, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -= Scapy and cbor2 produce identical bytes for empty map -from scapy.cbor.cborcodec import CBORcodec_MAP -CBORcodec_MAP.enc({}) == cbor2.dumps({}) - -= Scapy and cbor2 produce identical bytes for [1, 2, 3] -CBORcodec_ARRAY.enc([1, 2, 3]) == cbor2.dumps([1, 2, 3]) - -= Scapy and cbor2 produce identical bytes for {'a': 1} -CBORcodec_MAP.enc({'a': 1}) == cbor2.dumps({'a': 1}) - -+ CBOR Interoperability - Semantic Tags - -= Scapy encode semantic tag (tag 42), cbor2 decode -import cbor2 -obj = CBOR_SEMANTIC_TAG((42, CBOR_TEXT_STRING('test-content'))) -enc = bytes(obj) -dec = cbor2.loads(enc) -isinstance(dec, cbor2.CBORTag) and dec.tag == 42 and dec.value == 'test-content' - -= cbor2 encode semantic tag (tag 42), Scapy decode -enc = cbor2.dumps(cbor2.CBORTag(42, 'test-content')) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 42 and obj.val[1].val == 'test-content' and remainder == b'' - -= Scapy and cbor2 produce identical bytes for semantic tag 42 -import cbor2 -scapy_enc = bytes(CBOR_SEMANTIC_TAG((42, CBOR_TEXT_STRING('test-content')))) -cbor2_enc = cbor2.dumps(cbor2.CBORTag(42, 'test-content')) -scapy_enc == cbor2_enc - -= cbor2 encode epoch-based datetime tag (tag 1), Scapy decode -enc = cbor2.dumps(cbor2.CBORTag(1, 1363896240)) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 1 and obj.val[1].val == 1363896240 and remainder == b'' - -= cbor2 encode integer-tagged byte string, Scapy decode -enc = cbor2.dumps(cbor2.CBORTag(100, b'\xde\xad\xbe\xef')) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 100 and obj.val[1].val == b'\xde\xad\xbe\xef' and remainder == b'' - -+ CBOR Interoperability - Half-Precision Floats (RFC 8949 vectors) - -= Half-precision from RFC 8949: 0.0 -import cbor2 -data = bytes.fromhex('f90000') -obj, _ = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 0.0 - -= Half-precision from RFC 8949: 1.0 -data = bytes.fromhex('f93c00') -obj, _ = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 1.0 - -= Half-precision from RFC 8949: 1.5 -data = bytes.fromhex('f93e00') -obj, _ = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and obj.val == 1.5 - -= Half-precision from RFC 8949: positive infinity -import math -data = bytes.fromhex('f97c00') -obj, _ = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isinf(obj.val) and obj.val > 0 - -= Half-precision from RFC 8949: NaN -data = bytes.fromhex('f97e00') -obj, _ = CBOR_Codecs.CBOR.dec(data) -isinstance(obj, CBOR_FLOAT) and math.isnan(obj.val) - -= Scapy decode half-precision 1.5 agrees with cbor2 decode of double 1.5 -import cbor2 -half_data = bytes.fromhex('f93e00') -scapy_obj, _ = CBOR_Codecs.CBOR.dec(half_data) -double_data = bytes.fromhex('fb3ff8000000000000') -cbor2_val = cbor2.loads(double_data) -scapy_obj.val == cbor2_val - -+ CBOR Interoperability - Large Integers - -= Large uint 18446744073709551615 bytes match cbor2 -import cbor2 -max_u64 = 18446744073709551615 -bytes(CBOR_UNSIGNED_INTEGER(max_u64)) == cbor2.dumps(max_u64) - -= Large uint roundtrip Scapy to cbor2 to Scapy -max_u64 = 18446744073709551615 -scapy_enc = bytes(CBOR_UNSIGNED_INTEGER(max_u64)) -cbor2_val = cbor2.loads(scapy_enc) -cbor2_enc = cbor2.dumps(cbor2_val) -scapy_dec, _ = CBOR_Codecs.CBOR.dec(cbor2_enc) -scapy_dec.val == max_u64 - -= Large negative int -18446744073709551616 roundtrip via cbor2 -neg_max = -18446744073709551616 -cbor2_enc = cbor2.dumps(neg_max) -scapy_dec, _ = CBOR_Codecs.CBOR.dec(cbor2_enc) -scapy_dec.val == neg_max - -+ CBOR Interoperability - Complex Nested Structures - -= cbor2 deeply nested map: 3 levels, Scapy decode -import cbor2 -deep = {"level1": {"level2": {"level3": [1, 2, 3]}}} -enc = cbor2.dumps(deep) -obj, _ = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_MAP) and 'level1' in obj.val - -= Scapy deeply nested array, cbor2 decode -from scapy.cbor.cborcodec import CBORcodec_ARRAY -enc = CBORcodec_ARRAY.enc([[1, [2, [3, [4]]]], 5]) -dec = cbor2.loads(enc) -dec == [[1, [2, [3, [4]]]], 5] - -= cbor2 complex mixed structure: Scapy decodes it -import cbor2 -data = { - "name": "Alice", - "scores": [100, 95, 87], - "active": True, - "meta": {"created": 12345, "tag": "user"}, -} -enc = cbor2.dumps(data) -obj, _ = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_MAP) and 'name' in obj.val and 'scores' in obj.val +class BooleanChild(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN("value", None) -= Scapy encode complex structure, cbor2 decode, values match -from scapy.cbor.cborcodec import CBORcodec_MAP, CBORcodec_ARRAY -enc = CBORcodec_MAP.enc({ - "items": [1, 2, 3], - "count": 3, - "valid": True, -}) -dec = cbor2.loads(enc) -dec["items"] == [1, 2, 3] and dec["count"] == 3 and dec["valid"] is True +class OptionalPacketThenAny(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_PACKET("child", None, BooleanChild)), + CBORF_ANY("required", CBOR_ABSENT), + ) -########### CBORF Fields Interoperability Tests with cbor2 ############ +pkt = OptionalPacketThenAny(b"\x81\xf5") +assert pkt.getfieldval("child") is CBOR_ABSENT +assert pkt.required.val is True -+ CBORF Fields - Interop: CBORF_ARRAY packet to cbor2 += Nonterminal REMAINDER_OF is rejected in unframed sequences +from scapy.cbor.cborfields import ( + CBORF_ITEMS, + CBORF_REMAINDER_OF, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) -= CBORF_ARRAY packet to cbor2 list (version info) -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +try: + CBORF_ITEMS( + CBORF_REMAINDER_OF("items", [], CBORF_UNSIGNED_INTEGER), + CBORF_TEXT_STRING("tail", ""), + ) + assert False, "nonterminal REMAINDER_OF accepted" +except ValueError: + pass + += Ambiguous unbounded array schema is rejected even with an optional field between sequences +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) -class VersionInfo(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER('major', 1), - CBORF_UNSIGNED_INTEGER('minor', 2), - CBORF_UNSIGNED_INTEGER('patch', 3), +try: + CBORF_ARRAY( + CBORF_REMAINDER_OF("left", [], CBORF_UNSIGNED_INTEGER), + CBORF_optional(CBORF_BOOLEAN("middle", None)), + CBORF_REMAINDER_OF("right", [], CBORF_UNSIGNED_INTEGER), ) +except ValueError: + pass +else: + raise AssertionError("ambiguous separated unbounded sequences were accepted") -pkt = VersionInfo() -raw = bytes(pkt) -dec = cbor2.loads(raw) -isinstance(dec, list) and dec == [1, 2, 3] ++ Finding 8 - Nested rebuild must preserve a valid child raw cache -= cbor2 list to CBORF_ARRAY packet -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER += Parent rebuild preserves untouched child wire representation +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class VersionInfo2(CBOR_Packet): +class OverlongUintChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + +class ParentWithRawCachedChild(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER('major', 0), - CBORF_UNSIGNED_INTEGER('minor', 0), - CBORF_UNSIGNED_INTEGER('patch', 0), + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_PACKET("child", None, OverlongUintChild), ) -cbor2_data = cbor2.dumps([4, 5, 6]) -pkt = VersionInfo2(cbor2_data) -pkt.major.val == 4 and pkt.minor.val == 5 and pkt.patch.val == 6 +# 0x18 0x01 is a valid but non-preferred encoding of integer 1. +wire = b"\x82\x00\x18\x01" +pkt = ParentWithRawCachedChild(wire) +assert bytes(pkt.child) == b"\x18\x01" -= CBORF_ARRAY packet roundtrip through cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING +# Rebuilding the parent after changing only a sibling must not normalize the +# untouched nested child from 0x18 0x01 to 0x01. +pkt.sibling = 1 +assert bytes(pkt) == b"\x82\x01\x18\x01" +assert bytes(pkt.child) == b"\x18\x01" + += Parent rebuild preserves an untouched child encoded as an indefinite array +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class MsgPkt(CBOR_Packet): +class IndefiniteArrayChild(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 200), - CBORF_TEXT_STRING('status', 'ok'), + CBORF_UNSIGNED_INTEGER("value", 0), ) -pkt = MsgPkt() -raw = bytes(pkt) -cbor2_dec = cbor2.loads(raw) -cbor2_re_enc = cbor2.dumps(cbor2_dec) -pkt2 = MsgPkt(cbor2_re_enc) -pkt2.code.val == 200 and pkt2.status.val == 'ok' +class ParentWithIndefiniteChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_PACKET("child", None, IndefiniteArrayChild), + ) -= CBORF_ARRAY with boolean and null fields to cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_BOOLEAN, CBORF_NULL, CBORF_INTEGER +wire = b"\x82\x00\x9f\x01\xff" +pkt = ParentWithIndefiniteChild(wire) +assert bytes(pkt.child) == b"\x9f\x01\xff" +pkt.sibling = 1 +assert bytes(pkt) == b"\x82\x01\x9f\x01\xff" + += REMAINDER_OF preserves raw representations of untouched packet children +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class FlagPkt(CBOR_Packet): +class SequenceArrayChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("value", 0), + ) + +class ParentWithChildSequence(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('id', 7), - CBORF_BOOLEAN('active', True), - CBORF_NULL('reserved'), + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_REMAINDER_OF("children", [], SequenceArrayChild), ) -pkt = FlagPkt() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec[0] == 7 and dec[1] is True and dec[2] is None +wire = b"\x83\x00\x9f\x01\xff\x81\x02" +pkt = ParentWithChildSequence(wire) +assert bytes(pkt.children[0]) == b"\x9f\x01\xff" +assert bytes(pkt.children[1]) == b"\x81\x02" +pkt.sibling = 1 +assert bytes(pkt) == b"\x83\x01\x9f\x01\xff\x81\x02" -= cbor2 list with mixed types to CBORF_ARRAY packet -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_BOOLEAN, CBORF_NULL += Mutating a nested child invalidates the parent cache and rebuilds the child +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class Mixed(CBOR_Packet): +class MutableUintChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + +class ParentWithMutableChild(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('num', 0), - CBORF_BOOLEAN('flag', False), - CBORF_NULL('nval'), + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_PACKET("child", None, MutableUintChild), ) -cbor2_data = cbor2.dumps([42, False, None]) -pkt = Mixed(cbor2_data) -pkt.num.val == 42 +pkt = ParentWithMutableChild(b"\x82\x00\x18\x01") +pkt.child.value = 2 +assert bytes(pkt) == b"\x82\x00\x02" -+ CBORF Fields - Interop: CBORF_MAP packet to cbor2 ++ Additional CBOR API blind spots -= CBORF_MAP packet to cbor2 dict -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING += Optional semantic tag with default=None builds an empty array +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet -class ClaimSet(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', 'scapy'), - CBORF_INTEGER('exp', 9999999), +class OptionalTagged(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_SEMANTIC_TAG( + 1, + CBORF_UNSIGNED_INTEGER("value", None), + ) + ) ) -pkt = ClaimSet() -raw = bytes(pkt) -dec = cbor2.loads(raw) -isinstance(dec, dict) and dec.get('iss') == 'scapy' and dec.get('exp') == 9999999 +pkt = OptionalTagged() +assert pkt.getfieldval("value") is None +assert bytes(pkt) == b"\x80" -= cbor2 dict to CBORF_MAP packet -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER += Fixed-schema maps reject duplicate known keys instead of silently taking the last value +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_Decoding_Error from scapy.cborpacket import CBOR_Packet -class Claims(CBOR_Packet): +class OneKeyMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', ''), - CBORF_INTEGER('exp', 0), + CBORF_UNSIGNED_INTEGER("x", 0), ) -cbor2_data = cbor2.dumps({'iss': 'myapp', 'exp': 12345}) -pkt = Claims(cbor2_data) -pkt.iss.val == 'myapp' and pkt.exp.val == 12345 +try: + OneKeyMap(b"\xa2\x61x\x01\x61x\x02") +except CBOR_Decoding_Error: + pass +else: + raise AssertionError("duplicate fixed-map key was silently accepted") + -= CBORF_MAP packet roundtrip through cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING += Fixed-schema maps reject unknown keys that collide with known members on encode +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class BinHeader(CBOR_Packet): +class VersionMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('alg', 'ES256'), - CBORF_BYTE_STRING('kid', b'\x01\x02\x03\x04'), + CBORF_UNSIGNED_INTEGER("version", 1), ) -pkt = BinHeader() -raw = bytes(pkt) -cbor2_dec = cbor2.loads(raw) -cbor2_re_enc = cbor2.dumps(cbor2_dec) -pkt2 = BinHeader(cbor2_re_enc) -pkt2.alg.val == 'ES256' and pkt2.kid.val == b'\x01\x02\x03\x04' +pkt = VersionMap(version=1) +pkt._cbor_unknown = [("version", CBOR_UNSIGNED_INTEGER(42))] +try: + bytes(pkt) + assert False, "encode accepted unknown key colliding with known member" +except CBOR_Encoding_Error: + pass -= CBORF_MAP with boolean values to cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_BOOLEAN, CBORF_INTEGER + += Fixed-schema maps reject unknown keys colliding with absent optional members +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER, CBORF_optional +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class Flags(CBOR_Packet): +class OptMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_BOOLEAN('enabled', True), - CBORF_INTEGER('count', 5), + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), ) -pkt = Flags() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec.get('enabled') is True and dec.get('count') == 5 - -= cbor2 dict with unknown keys: CBORF_MAP skips them -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING +pkt = OptMap(a=1) +pkt._cbor_unknown = [("opt", CBOR_UNSIGNED_INTEGER(42))] +try: + bytes(pkt) + assert False, "encode accepted unknown key colliding with absent optional" +except CBOR_Encoding_Error: + pass + + += Fixed-schema maps reject unknown keys colliding with false conditional members +from scapy.cbor.cborfields import ( + CBORF_CONDITIONAL, + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class SimpleMap(CBOR_Packet): +class CondMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('known', 'default'), + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("cond", 9), + lambda pkt: pkt.getfieldval("flag") == 1, + ), ) -cbor2_data = cbor2.dumps({'known': 'value', 'unknown': 'extra'}) -pkt = SimpleMap(cbor2_data) -pkt.known.val == 'value' - -+ CBORF Fields - Interop: CBORF_ARRAY_OF packet to cbor2 - -= CBORF_ARRAY_OF with integer elements to cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER -from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER -from scapy.cborpacket import CBOR_Packet - -class IntList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) +pkt = CondMap(flag=0) +pkt._cbor_unknown = [("cond", CBOR_UNSIGNED_INTEGER(42))] +try: + bytes(pkt) + assert False, "encode accepted unknown key colliding with false conditional" +except CBOR_Encoding_Error: + pass -pkt = IntList() -pkt.items = [CBOR_UNSIGNED_INTEGER(10), CBOR_UNSIGNED_INTEGER(20), CBOR_UNSIGNED_INTEGER(30)] -raw = bytes(pkt) -dec = cbor2.loads(raw) -isinstance(dec, list) and dec == [10, 20, 30] -= cbor2 list to CBORF_ARRAY_OF -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER += Fixed-schema maps reject duplicate unknown text keys on encode +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class IntList2(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) +class ExtMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + ) -cbor2_data = cbor2.dumps([100, 200, 300]) -pkt = IntList2(cbor2_data) -len(pkt.items) == 3 and pkt.items[0].val == 100 and pkt.items[2].val == 300 +pkt = ExtMap(a=1) +pkt._cbor_unknown = [ + ("x", CBOR_UNSIGNED_INTEGER(1)), + ("x", CBOR_UNSIGNED_INTEGER(2)), +] +try: + bytes(pkt) + assert False, "encode accepted duplicate unknown map keys" +except CBOR_Encoding_Error: + pass -+ CBORF Fields - Interop: CBORF_SEMANTIC_TAG to cbor2 -= CBORF_SEMANTIC_TAG packet to cbor2 CBORTag -import cbor2 -from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_UNSIGNED_INTEGER += Fixed-schema maps still encode distinct unknown extension keys +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class TimestampPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag_info', None, 1, CBORF_UNSIGNED_INTEGER('ts', 1363896240)) - -pkt = TimestampPkt() -raw = bytes(pkt) -import datetime -dec = cbor2.loads(raw) -isinstance(dec, (cbor2.CBORTag, datetime.datetime, datetime.date)) - -= cbor2 CBORTag (tag 42) decoded by Scapy CBOR_SEMANTIC_TAG -import cbor2 -enc = cbor2.dumps(cbor2.CBORTag(42, 'tagged-value')) -obj, remainder = CBOR_Codecs.CBOR.dec(enc) -isinstance(obj, CBOR_SEMANTIC_TAG) and obj.val[0] == 42 and obj.val[1].val == 'tagged-value' and remainder == b'' - -= CBORF_SEMANTIC_TAG bytes identical to cbor2 CBORTag bytes -import cbor2 -scapy_enc = bytes(CBOR_SEMANTIC_TAG((42, CBOR_TEXT_STRING('tagged-value')))) -cbor2_enc = cbor2.dumps(cbor2.CBORTag(42, 'tagged-value')) -scapy_enc == cbor2_enc - -+ CBORF Fields - Interop: CBORF_UNSIGNED_INTEGER with cbor2 +class ExtOkMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + ) -= CBORF_UNSIGNED_INTEGER boundary values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER -from scapy.cborpacket import CBOR_Packet +pkt = ExtOkMap(a=1) +pkt._cbor_unknown = [("x", CBOR_UNSIGNED_INTEGER(2))] +assert bytes(pkt) == b"\xa2\x61a\x01\x61x\x02" -class UIntPkt(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) -results = [] -for val in [0, 23, 24, 255, 256, 65535, 65536, 4294967295, 4294967296, 18446744073709551615]: - pkt = UIntPkt() - pkt.n.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) += Generic CBOR maps reject duplicate keys on encode +from scapy.cbor.cbor import CBOR_MAP, CBORMapData, CBOR_UNSIGNED_INTEGER +from scapy.cbor.cborcodec import CBOR_Codec_Encoding_Error, CBORcodec_MAP -all(results) +dup = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), 0), + (CBOR_UNSIGNED_INTEGER(1), 1), +]) +try: + CBORcodec_MAP.enc(dup) + assert False, "generic map encode accepted duplicate integer keys" +except CBOR_Codec_Encoding_Error: + pass -= CBORF_UNSIGNED_INTEGER boundary values - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER -from scapy.cborpacket import CBOR_Packet +try: + CBOR_MAP(dup).enc() + assert False, "CBOR_MAP.enc accepted duplicate integer keys" +except CBOR_Codec_Encoding_Error: + pass -class UIntPkt2(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) +ok = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), 0), + (CBOR_UNSIGNED_INTEGER(2), 1), +]) +assert CBORcodec_MAP.enc(ok) == b"\xa2\x01\x00\x02\x01" -results = [] -for val in [0, 23, 24, 255, 256, 65535, 65536, 4294967295, 4294967296]: - pkt = UIntPkt2(cbor2.dumps(val)) - results.append(pkt.n.val == val) -all(results) ++ Finding 10 - Indefinite arrays should scale linearly without repeated suffix pre-decodes -= CBORF_UNSIGNED_INTEGER byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER += Indefinite array span work remains linear in the input size +import scapy.cbor.cborfields as cborfields +from scapy.cbor.cborfields import CBORF_ARRAY_INDEFINITE, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class UIntExact(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) +many_fields = [CBORF_UNSIGNED_INTEGER("v%d" % i, 0) for i in range(64)] -results = [] -for val in [0, 1, 10, 23, 24, 255, 256, 65535, 65536, 4294967295]: - pkt = UIntExact() - pkt.n.val = val - results.append(bytes(pkt) == cbor2.dumps(val)) +class IndefiniteManyInts(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE(*many_fields) -all(results) +orig_span = cborfields.cbor_item_span +span_input_sizes = [] -+ CBORF Fields - Interop: CBORF_NEGATIVE_INTEGER with cbor2 +def counted_span(data): + span_input_sizes.append(len(data)) + return orig_span(data) -= CBORF_NEGATIVE_INTEGER boundary values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER +wire = b"\x9f" + (b"\x00" * 64) + b"\xff" +cborfields.cbor_item_span = counted_span +try: + pkt = IndefiniteManyInts(wire) + assert pkt.v0 == 0 + assert pkt.v63 == 0 +finally: + cborfields.cbor_item_span = orig_span + +# Repeatedly handing cbor_item_span() the complete shrinking suffix is +# quadratic. Exact-item spans or a shared cursor keep aggregate scanned input +# proportional to the original wire size. The 4x allowance avoids constraining +# the exact implementation while still rejecting an O(n^2) pre-scan. +assert sum(span_input_sizes) <= len(wire) * 4, span_input_sizes + ++ Additional regressions for recently fixed generic-CBOR behavior + += Optional major-type-7 fields discriminate Boolean, null, undefined, and float exactly +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_FLOAT, + CBORF_NULL, + CBORF_UNDEFINED, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class NIntPkt(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) - -results = [] -for val in [-1, -24, -25, -256, -257, -65536, -65537, -4294967296, -4294967297]: - pkt = NIntPkt() - pkt.n.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) +class OptionalBoolThenFloat(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_FLOAT("required", 0.0), + ) -all(results) +class OptionalNullThenBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_NULL("maybe")), + CBORF_BOOLEAN("required", None), + ) -= CBORF_NEGATIVE_INTEGER boundary values - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER -from scapy.cborpacket import CBOR_Packet +class OptionalUndefinedThenBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNDEFINED("maybe")), + CBORF_BOOLEAN("required", None), + ) -class NIntPkt2(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) +# Half-precision 1.5 is a float, not a Boolean even though both are major type 7. +pkt = OptionalBoolThenFloat(b"\x81\xf9\x3e\x00") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required == 1.5 -results = [] -for val in [-1, -24, -25, -256, -257, -65536, -4294967296]: - pkt = NIntPkt2(cbor2.dumps(val)) - results.append(pkt.n.val == val) +pkt = OptionalNullThenBool(b"\x81\xf5") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required is True -all(results) +pkt = OptionalUndefinedThenBool(b"\x81\xf4") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required is False -= CBORF_NEGATIVE_INTEGER byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER += Generic ANY preserves CBOR map identity when an unrelated sibling is changed +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class NIntExact(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) - -results = [] -for val in [-1, -10, -24, -25, -256, -257, -65536, -65537]: - pkt = NIntExact() - pkt.n.val = val - results.append(bytes(pkt) == cbor2.dumps(val)) - -all(results) +class AnyMapWithSibling(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("sibling", 0), + ) -+ CBORF Fields - Interop: CBORF_INTEGER with cbor2 +wire = b"\x82\xa1\x01\x02\x00" +pkt = AnyMapWithSibling(wire) +assert pkt.value.val[1].val == 2 +pkt.sibling = 1 +assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" -= CBORF_INTEGER positive values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER += In-place mutation of a generic ANY array invalidates the packet raw cache +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class IntPkt(CBOR_Packet): - CBOR_root = CBORF_INTEGER('n', 0) +class AnyArrayWithSibling(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("sibling", 0), + ) -results = [] -for val in [0, 1, 42, 100, 1000, 1000000]: - pkt = IntPkt() - pkt.n.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +pkt = AnyArrayWithSibling(b"\x82\x82\x01\x02\x00") +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) +assert bytes(pkt) == b"\x82\x83\x01\x02\x03\x00" -all(results) += Generic map lookup keeps integer 1 and Boolean true as distinct CBOR keys +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet -= CBORF_INTEGER negative values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER +class TypedKeyMap(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +pkt = TypedKeyMap(b"\xa2\x01\x61i\xf5\x61b") +assert pkt.value.val[1].val == "i" +assert pkt.value.val[True].val == "b" +assert len(pkt.value.val.cbor_pairs()) == 2 +assert bytes(pkt) == b"\xa2\x01\x61i\xf5\x61b" + ++ Deterministic CBOR and float edge cases + ++ Deterministic CBOR: large binary64 and indefinite map key order + += Large binary64 values do not crash the deterministic scanner +import struct +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic + +# RFC 8949 Appendix A example: 1.0e+300 as binary64 +wire = bytes.fromhex("fb7e37e43c8800759c") +assert cbor_find_non_deterministic(wire) == [] + +wire = struct.pack(">B", 0xfb) + struct.pack(">d", -1e300) +assert cbor_find_non_deterministic(wire) == [] + += Indefinite maps require bytewise lexicographic key order +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic + +assert not cbor_find_non_deterministic( + bytes.fromhex("bf616101616202ff"), + allow_indefinite=True, +) +assert cbor_find_non_deterministic( + bytes.fromhex("bf616201616102ff"), + allow_indefinite=True, +) + += NaN preferred width uses the original payload bit pattern +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic + +# binary64 NaN with a low payload bit cannot shorten to binary16/32 +assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000000000001")) == [] + +# binary64 quiet NaN with only top significand bits set prefers binary16 +assert cbor_find_non_deterministic(bytes.fromhex("fb7ffc000000000000")) + += Non-det walker enforces nesting depth and indefinite string chunks +from scapy.cbor.cborcodec import MAX_CBOR_NESTING +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic + +# Malformed indefinite strings: checker must not raise RecursionError. +bad_bstr = b"\x5f\x61a\xff" # byte string with a text chunk +bad_tstr = b"\x7f\x41a\xff" # text string with a byte chunk +nested_indef = b"\x5f\x5f\xff\xff" +for wire in (bad_bstr, bad_tstr, nested_indef): + issues = cbor_find_non_deterministic(wire, allow_indefinite=True) + assert isinstance(issues, list) + +# Over-nested arrays: swallowed as malformed, not RecursionError. +too_deep = b"\x81" * (MAX_CBOR_NESTING + 1) + b"\x00" +issues = cbor_find_non_deterministic(too_deep) +assert isinstance(issues, list) + +# Well-formed indefinite byte string still scans without error issues. +assert cbor_find_non_deterministic( + b"\x5f\x41a\xff", allow_indefinite=True +) == [] + + ++ Cached unframed packets preserve exact bytes + += Unframed SEQUENCE cache returns exact bytes without rebuild +from scapy.cbor.cborfields import ( + CBORF_PACKET, + CBORF_ITEMS, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet -class IntNegPkt(CBOR_Packet): - CBOR_root = CBORF_INTEGER('n', -1) +class SeqChild(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_UNSIGNED_INTEGER("b", 0), + ) -results = [] -for val in [-1, -10, -100, -1000, -1000000]: - pkt = IntNegPkt() - pkt.n.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) +# Overlong encoding of 1, then 2: two top-level items +overlong = b"\x18\x01\x02" +child = SeqChild(overlong) +assert child.raw_packet_cache == overlong +assert bytes(child) == overlong + += Absent optional scalar preserves non-preferred following encoding +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -all(results) +class OptionalThenInt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) -= CBORF_INTEGER - cbor2 encode positive and negative, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER +raw = b"\x81\x18\x00" +pkt = OptionalThenInt(raw) +assert pkt.maybe is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw +assert pkt.raw_packet_cache == raw + +pkt = OptionalThenInt(raw) +pkt.value = 1 +assert bytes(pkt) == b"\x81\x01" + +pkt = OptionalThenInt(raw) +pkt.maybe = True +rebuilt = bytes(pkt) +assert rebuilt != raw +assert OptionalThenInt(rebuilt).maybe is True + += Absent optional integer preserves overlong follower +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class IntPkt2(CBOR_Packet): - CBOR_root = CBORF_INTEGER('n', 0) - -results = [] -for val in [0, 42, -1, -42, 255, -256, 65536, -65537]: - pkt = IntPkt2(cbor2.dumps(val)) - results.append(pkt.n.val == val) +class OptIntThenInt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) -all(results) +raw = b"\x81\x18\x00" +pkt = OptIntThenInt(raw) +assert pkt.opt is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw + += Multiple absent optionals preserve untouched raw cache +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -+ CBORF Fields - Interop: CBORF_BYTE_STRING with cbor2 +class MultiAbsent(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("a", None)), + CBORF_optional(CBORF_BOOLEAN("b", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) -= CBORF_BYTE_STRING empty bytes - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING +raw = b"\x81\x18\x00" +pkt = MultiAbsent(raw) +assert pkt.a is CBOR_ABSENT and pkt.b is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw + += Nested packet with absent optional preserves exact child bytes +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class BytePkt(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING('data', b'') - -pkt = BytePkt() -dec = cbor2.loads(bytes(pkt)) -dec == b'' +class NestedOptChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) -= CBORF_BYTE_STRING all 256 byte values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING +class NestedOptParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, NestedOptChild) + +raw = b"\x81\x18\x00" +pkt = NestedOptParent(raw) +assert pkt.child.maybe is CBOR_ABSENT +assert pkt.child.value == 0 +assert bytes(pkt) == raw +assert pkt.child.raw_packet_cache == raw + += Absent optional map member preserves non-preferred known value +from scapy.cbor.cborfields import ( + CBORF_BOOLEAN, + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class ByteAllPkt(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING('data', b'') - -pkt = ByteAllPkt() -pkt.data.val = bytes(range(256)) -dec = cbor2.loads(bytes(pkt)) -dec == bytes(range(256)) +class OptMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("value", 0), + ) -= CBORF_BYTE_STRING - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING +# map with only "value": overlong 0 +raw = b"\xa1\x65value\x18\x00" +pkt = OptMap(raw) +assert pkt.maybe is CBOR_ABSENT +assert pkt.value == 0 +assert bytes(pkt) == raw + += Unframed SEQUENCE optional parsing is greedy left-to-right +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_BOOLEAN, + CBORF_ITEMS, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class BytePkt3(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING('data', b'') +class GreedySequence(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_UNSIGNED_INTEGER("optional", None)), + CBORF_UNSIGNED_INTEGER("required", 0), + ) -for raw_val in [b'', b'\xde\xad\xbe\xef', bytes(range(256))]: - pkt = BytePkt3(cbor2.dumps(raw_val)) - assert pkt.data.val == raw_val +pkt = GreedySequence(b"\x01\x02") +assert pkt.optional == 1 +assert pkt.required == 2 -True +try: + GreedySequence(b"\x01") + assert False, "ambiguous single unsigned item must fail under greedy SEQUENCE" +except CBOR_Decoding_Error: + pass + +class TypedSequence(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_BOOLEAN("optional", None)), + CBORF_UNSIGNED_INTEGER("required", 0), + ) -= CBORF_BYTE_STRING byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING +pkt = TypedSequence(b"\x07") +assert pkt.optional is CBOR_ABSENT +assert pkt.required == 7 + +pkt = TypedSequence(b"\xf5\x07") +assert pkt.optional is True +assert pkt.required == 7 + +pkt = TypedSequence() +result = TypedSequence.CBOR_root._dissect_counted(pkt, b"\x07\x18") +assert pkt.optional is CBOR_ABSENT +assert pkt.required == 7 +assert result.remaining == b"\x18" + += Optional-only SEQUENCE marks absent on empty input +from scapy.cbor.cborfields import ( + CBORF_BOOLEAN, + CBORF_ITEMS, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class ByteExact(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING('data', b'') - -results = [] -for raw_val in [b'', b'\x00', b'\xff', b'\xde\xad\xbe\xef', b'hello']: - pkt = ByteExact() - pkt.data.val = raw_val - results.append(bytes(pkt) == cbor2.dumps(raw_val)) - -all(results) +class OptionalOnly(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + ) -+ CBORF Fields - Interop: CBORF_TEXT_STRING with cbor2 +# Packet(b"") skips dissect (empty bytes are falsy); drive the schema directly. +pkt = OptionalOnly() +result = OptionalOnly.CBOR_root._dissect_counted(pkt, b"") +assert pkt.maybe is CBOR_ABSENT +assert result.remaining == b"" -= CBORF_TEXT_STRING empty string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING +# CBORF_PACKET represents exactly one CBOR item: embedding a multi-item +# SEQUENCE child must fail (do not put this child in a CBORF_PACKET parent +# and expect serialization to succeed). +fld = CBORF_PACKET("x", None, pkt_cls=SeqChild) +try: + fld._build_value(None, child) + assert False, "multi-item child must be rejected by CBORF_PACKET" +except CBOR_Encoding_Error: + pass + += CBORF_PACKET _build_value enforces one-item cardinality like _build_counted +from scapy.cbor.cborfields import ( + CBORF_PACKET, + CBORF_ITEMS, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet -class TextPkt(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') - -pkt = TextPkt() -dec = cbor2.loads(bytes(pkt)) -dec == '' +class TwoItemChild(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_UNSIGNED_INTEGER("a", 1), + CBORF_UNSIGNED_INTEGER("b", 2), + ) -= CBORF_TEXT_STRING ASCII string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +class OneItemChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("a", 1) -class TextPkt2(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') +fld = CBORF_PACKET("x", None, pkt_cls=OneItemChild) +ok = fld._build_value(None, OneItemChild(a=7)) +assert ok.items == 1 -pkt = TextPkt2() -pkt.txt.val = 'Hello, World!' -dec = cbor2.loads(bytes(pkt)) -dec == 'Hello, World!' +fld2 = CBORF_PACKET("x", None, pkt_cls=TwoItemChild) +try: + fld2._build_value(None, TwoItemChild()) + assert False, "multi-item child must be rejected by _build_value" +except CBOR_Encoding_Error: + pass -= CBORF_TEXT_STRING unicode string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING += Nested CBORF_PACKET honors child post_build +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class TextUniPkt(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') +class PostBuildChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + def post_build(self, pkt, pay): + # Force a distinct single CBOR unsigned integer on the wire. + return b"\x18\x2a" + pay + +class PostBuildParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, PostBuildChild) -pkt = TextUniPkt() -pkt.txt.val = u'Hello, \u4e16\u754c' -dec = cbor2.loads(bytes(pkt)) -dec == u'Hello, \u4e16\u754c' +child = PostBuildChild(value=1) +assert bytes(child) == b"\x18\x2a" +bytes(PostBuildParent(child=child)) == bytes(child) -= CBORF_TEXT_STRING - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING += CBORF_ANY subclass build() is used by ARRAY counted path +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY from scapy.cborpacket import CBOR_Packet -class TextPkt3(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') +class ForcedAny(CBORF_ANY): + def build(self, pkt): + return b"\x18\x63" -for s in ['', 'hello', 'Hello, World!', u'caf\u00e9', u'\u4e16\u754c']: - pkt = TextPkt3(cbor2.dumps(s)) - assert pkt.txt.val == s +class ForcedAnyPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY(ForcedAny("value", None)) -True +bytes(ForcedAnyPkt(value=0)) == b"\x81\x18\x63" -= CBORF_TEXT_STRING byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING += CBORF_PACKET rejects non-CBOR Packet/bytes that are not exactly one item +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet +from scapy.packet import Raw -class TextExact(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') +class OneItemPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -results = [] -for s in ['', 'a', 'hello', 'IETF', u'\u6c34']: - pkt = TextExact() - pkt.txt.val = s - results.append(bytes(pkt) == cbor2.dumps(s)) +fld = CBORF_PACKET("x", None, pkt_cls=OneItemPkt) -all(results) +# Two valid CBOR integers must not be reported as one item +try: + fld._build_value(None, Raw(b"\x01\x02")) + assert False, "two CBOR items must be rejected" +except CBOR_Encoding_Error: + pass -+ CBORF Fields - Interop: CBORF_BOOLEAN with cbor2 +# Illegal standalone break +try: + fld._build_value(None, Raw(b"\xff")) + assert False, "bare break must be rejected" +except CBOR_Encoding_Error: + pass -= CBORF_BOOLEAN true - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet +# Truncated CBOR +try: + fld._build_value(None, Raw(b"\x18")) + assert False, "truncated CBOR must be rejected" +except CBOR_Encoding_Error: + pass -class BoolPkt(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', True) +# Exactly one valid item is accepted via the Raw fallback +ok = fld._build_value(None, Raw(b"\x01")) +assert ok.items == 1 +assert ok.data == b"\x01" -pkt = BoolPkt() -dec = cbor2.loads(bytes(pkt)) -dec is True -= CBORF_BOOLEAN false - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet ++ Scapy-native packet ownership -class BoolFalsePkt(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', False) += CBORF_PACKET construction uses parent ownership, not protocol underlayer +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet -pkt = BoolFalsePkt() -dec = cbor2.loads(bytes(pkt)) -dec is False +class OwnedChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= CBORF_BOOLEAN - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN +class DirectParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, OwnedChild) + +child = OwnedChild(value=1) +parent = DirectParent(child=child) +assert parent.child is child +assert child.parent is parent +assert child.underlayer is None + += CBORF_PACKET assignment preserves an existing protocol underlayer +from scapy.packet import Raw + +child = OwnedChild(value=1) +real_underlayer = Raw(load=b"lower") +child.add_underlayer(real_underlayer) +parent = DirectParent(child=child) +assert child.parent is parent +assert child.underlayer is real_underlayer + += CBORF_PACKET dissection uses parent ownership, not protocol underlayer +parent = DirectParent(b"\x01") +assert isinstance(parent.child, OwnedChild) +assert parent.child.parent is parent +assert parent.child.underlayer is None +assert bytes(parent) == b"\x01" + += CBORF_BYTE_STRING_PACKET uses parent ownership on construction and dissection +from scapy.cbor.cborfields import CBORF_BYTE_STRING_PACKET +from scapy.packet import Raw + +class ByteStringParent(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING_PACKET("child", None, pkt_cls=Raw) + +child = Raw(load=b"x") +parent = ByteStringParent(child=child) +assert parent.child is child +assert child.parent is parent +assert child.underlayer is None +assert bytes(parent) == b"\x41x" + +parsed = ByteStringParent(b"\x41x") +assert isinstance(parsed.child, Raw) +assert parsed.child.load == b"x" +assert parsed.child.parent is parsed +assert parsed.child.underlayer is None + += CBORF_BYTE_STRING_PACKET honors conf.debug_dissector for child TypeError +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_BYTE_STRING_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class BoolPkt2(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', False) +class BspTypeErrorChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + def __init__(self, *args, **kwargs): + raise TypeError("intentional byte-string packet failure") -pkt_true = BoolPkt2(cbor2.dumps(True)) -pkt_false = BoolPkt2(cbor2.dumps(False)) -pkt_true.flag.val is True and pkt_false.flag.val is False +class BspTypeErrorParent(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING_PACKET( + "child", None, pkt_cls=BspTypeErrorChild + ) -= CBORF_BOOLEAN byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet +# CBOR byte string wrapping one unsigned integer: 0x41 0x01 +wire = b"\x41\x01" +_old_dbg = conf.debug_dissector +try: + conf.debug_dissector = False + try: + BspTypeErrorParent(wire) + except CBOR_Decoding_Error as exc: + assert "intentional byte-string packet failure" in str(exc) + else: + raise AssertionError("TypeError was not wrapped as CBOR_Decoding_Error") -class BoolExactTrue(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', True) + conf.debug_dissector = True + try: + BspTypeErrorParent(wire) + except TypeError as exc: + assert "intentional byte-string packet failure" in str(exc) + else: + raise AssertionError("TypeError was hidden despite debug_dissector") +finally: + conf.debug_dissector = _old_dbg + ++ packet-valued collection ownership + += CBORF_ARRAY_OF construction attaches every packet child to parent +from scapy.cbor.cborfields import CBORF_ARRAY_OF + +class ArrayParent(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("children", [], OwnedChild) + +children = [OwnedChild(value=1), OwnedChild(value=2)] +parent = ArrayParent(children=children) +assert parent.children == children +for child in parent.children: + assert child.parent is parent + assert child.underlayer is None + +assert bytes(parent) == b"\x82\x01\x02" + += CBORF_ARRAY_OF dissection attaches every packet child to parent +parent = ArrayParent(b"\x82\x01\x02") +assert [child.value for child in parent.children] == [1, 2] +for child in parent.children: + assert child.parent is parent + assert child.underlayer is None + +assert bytes(parent) == b"\x82\x01\x02" + += CBORF_REMAINDER_OF construction attaches every packet child to parent +from scapy.cbor.cborfields import CBORF_REMAINDER_OF + +class SequenceParent(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF("children", [], OwnedChild) + +children = [OwnedChild(value=1), OwnedChild(value=2)] +parent = SequenceParent(children=children) +assert parent.children == children +for child in parent.children: + assert child.parent is parent + assert child.underlayer is None + +assert bytes(parent) == b"\x01\x02" + += CBORF_REMAINDER_OF dissection attaches every packet child to parent +parent = SequenceParent(b"\x01\x02") +assert [child.value for child in parent.children] == [1, 2] +for child in parent.children: + assert child.parent is parent + assert child.underlayer is None + +assert bytes(parent) == b"\x01\x02" + ++ deterministic fixed-schema maps + += CBORF_MAP emits deterministic encoded-key order independent of declaration order +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic +from scapy.cbor.cborfields import CBORF_MAP + +class ReverseDeclaredMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("b", 1), + CBORF_UNSIGNED_INTEGER("a", 2), + ) -class BoolExactFalse(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', False) +wire = bytes(ReverseDeclaredMap()) +# RFC 8949 deterministic ordering sorts by the encoded key bytes, so "a" +# precedes "b" even though the fields were declared in the opposite order. +assert wire == b"\xa2\x61a\x02\x61b\x01" +assert cbor_find_non_deterministic(wire) == [] -pkt_t = BoolExactTrue() -pkt_f = BoolExactFalse() -bytes(pkt_t) == cbor2.dumps(True) and bytes(pkt_f) == cbor2.dumps(False) +########### Scapy-native conversion pipeline ################# -+ CBORF Fields - Interop: CBORF_NULL with cbor2 ++ Field defaults and i2m / RawVal -= CBORF_NULL - Scapy encode, cbor2 decode gives None -import cbor2 -from scapy.cbor.cborfields import CBORF_NULL += Field defaults are normalized through any2i like native Scapy +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class NullPkt(CBOR_Packet): - CBOR_root = CBORF_NULL('n') +class DefaultNormPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", "12") -pkt = NullPkt() -dec = cbor2.loads(bytes(pkt)) -dec is None +assert DefaultNormPkt().value == 12 +assert DefaultNormPkt(value="34").value == 34 +assert bytes(DefaultNormPkt()) == b"\x0c" -= CBORF_NULL byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_NULL += RawVal injects exact CBOR wire bytes through i2m +from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet +from scapy.fields import RawVal +from scapy.cbor.cbor import CBOR_Encoding_Error -class NullExact(CBOR_Packet): - CBOR_root = CBORF_NULL('n') +class RawValPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -pkt = NullExact() -bytes(pkt) == cbor2.dumps(None) +assert bytes(RawValPkt(value=RawVal(b"\x18\x64"))) == b"\x18\x64" -= CBORF_NULL - cbor2 None encode, Scapy decode gives CBOR_NULL -import cbor2 -from scapy.cbor.cbor import CBOR_NULL -from scapy.cbor.cborfields import CBORF_NULL +try: + bytes(RawValPkt(value=RawVal(b"\x01\x02"))) + assert False, "multi-item RawVal must be rejected" +except CBOR_Encoding_Error: + pass + += RawVal and raw packet encode reject semantically invalid CBOR +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, + _encode_exactly_one_cbor_item, +) +from scapy.cbor.cborcodec import cbor_item_span from scapy.cborpacket import CBOR_Packet +from scapy.fields import RawVal +from scapy.cbor.cbor import CBOR_Encoding_Error -class NullPkt2(CBOR_Packet): - CBOR_root = CBORF_NULL('n') +bad_utf8 = b"\x61\xff" +duplicate_map = bytes.fromhex("a201000101") -pkt = NullPkt2(cbor2.dumps(None)) -isinstance(pkt.n, CBOR_NULL) +# Structural span only finds boundaries; it does not UTF-8 / key-validate. +item, rest = cbor_item_span(bad_utf8) +assert item == bad_utf8 and rest == b"" +item, rest = cbor_item_span(duplicate_map) +assert item == duplicate_map and rest == b"" -+ CBORF Fields - Interop: CBORF_FLOAT with cbor2 +class RawAnyPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= CBORF_FLOAT basic values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet +try: + bytes(RawAnyPkt(value=RawVal(bad_utf8))) + assert False, "RawVal accepted invalid UTF-8 text string" +except CBOR_Encoding_Error: + pass -class FloatPkt(CBOR_Packet): - CBOR_root = CBORF_FLOAT('f', 0.0) +try: + bytes(RawAnyPkt(value=RawVal(duplicate_map))) + assert False, "RawVal accepted duplicate map keys" +except CBOR_Encoding_Error: + pass -results = [] -for val in [0.0, 1.0, -1.0, 3.14159, 1e10, -2.5]: - pkt = FloatPkt() - pkt.f.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) +try: + _encode_exactly_one_cbor_item(bad_utf8, context="raw") + assert False, "_encode_exactly_one_cbor_item accepted invalid UTF-8" +except CBOR_Encoding_Error: + pass -all(results) +try: + _encode_exactly_one_cbor_item(duplicate_map, context="raw") + assert False, "_encode_exactly_one_cbor_item accepted duplicate keys" +except CBOR_Encoding_Error: + pass -= CBORF_FLOAT special values (NaN, Inf, -Inf) - Scapy encode, cbor2 decode -import cbor2, math -from scapy.cbor.cborfields import CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet +class RawChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -class FloatSpecialPkt(CBOR_Packet): - CBOR_root = CBORF_FLOAT('f', 0.0) +class RawParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, RawChild) -pkt_nan = FloatSpecialPkt() -pkt_nan.f.val = float('nan') -raw_nan = bytes(pkt_nan) -pkt_inf = FloatSpecialPkt() -pkt_inf.f.val = float('inf') -raw_inf = bytes(pkt_inf) -pkt_ninf = FloatSpecialPkt() -pkt_ninf.f.val = float('-inf') -raw_ninf = bytes(pkt_ninf) -math.isnan(cbor2.loads(raw_nan)) and math.isinf(cbor2.loads(raw_inf)) and cbor2.loads(raw_ninf) == float('-inf') +try: + bytes(RawParent(child=bad_utf8)) + assert False, "packet-valued raw bytes accepted invalid UTF-8" +except CBOR_Encoding_Error: + pass -= CBORF_FLOAT special values - cbor2 encode, Scapy decode -import cbor2, math -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_FLOAT += Byte-string internals remain encoded (bytes is not a wire bypass) +from scapy.cbor.cborfields import CBORF_BYTE_STRING from scapy.cborpacket import CBOR_Packet -class FloatArrPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_FLOAT('nan_val', 0.0), - CBORF_FLOAT('inf_val', 0.0), - CBORF_FLOAT('ninf_val', 0.0), - ) +class BstrPkt(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("blob", b"ABC") -pkt = FloatArrPkt(cbor2.dumps([float('nan'), float('inf'), float('-inf')])) -math.isnan(pkt.nan_val.val) and math.isinf(pkt.inf_val.val) and pkt.ninf_val.val == float('-inf') +assert BstrPkt().blob == b"ABC" +assert bytes(BstrPkt()) == b"\x43" + b"ABC" -= CBORF_FLOAT - cbor2 encode, Scapy decode roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_FLOAT ++ CBORF_BYTE_STRING_PACKET default normalization + += CBORF_BYTE_STRING_PACKET normalizes byte defaults after packet-class state is initialized +from scapy.cbor.cborfields import CBORF_BYTE_STRING_PACKET from scapy.cborpacket import CBOR_Packet +from scapy.packet import Raw -class FloatPkt2(CBOR_Packet): - CBOR_root = CBORF_FLOAT('f', 0.0) +class ByteStringDefaultParent(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING_PACKET( + "child", + b"abc", + pkt_cls=Raw, + ) -results = [] -for val in [0.0, 1.0, -1.0, 2.5, 100.0]: - pkt = FloatPkt2(cbor2.dumps(val)) - results.append(pkt.f.val == val) +pkt = ByteStringDefaultParent() +assert isinstance(pkt.child, Raw) +assert pkt.child.load == b"abc" +assert pkt.child.parent is pkt +assert bytes(pkt) == b"\x43abc" -all(results) -+ CBORF Fields - Interop: CBORF_ARRAY with cbor2 ++ PacketListField-style next_cls_cb semantics -= CBORF_ARRAY with integer fields - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER += CBORF_REMAINDER_OF next_cls_cb selects packet classes dynamically +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class PointPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('x', 10), - CBORF_INTEGER('y', 20), - CBORF_INTEGER('z', 30), - ) - -pkt = PointPkt() -dec = cbor2.loads(bytes(pkt)) -dec == [10, 20, 30] +class DynamicSequenceChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= CBORF_ARRAY with mixed types - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet +NextClsCalls = [] +def choose_dynamic_child(pkt, lst, cur, remain): + NextClsCalls.append(len(lst)) + return DynamicSequenceChild -class MixedPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('id', 99), - CBORF_TEXT_STRING('label', 'test'), - CBORF_BOOLEAN('active', True), +class DynamicSequenceParent(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF( + "children", + [], + next_cls_cb=choose_dynamic_child, ) -pkt = MixedPkt() -dec = cbor2.loads(bytes(pkt)) -dec[0] == 99 and dec[1] == 'test' and dec[2] is True +pkt = DynamicSequenceParent(b"\x01") +assert NextClsCalls == [0] +assert len(pkt.children) == 1 +assert isinstance(pkt.children[0], DynamicSequenceChild) +assert pkt.children[0].parent is pkt -= CBORF_ARRAY - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING += CBORF_REMAINDER_OF rejects combining next_cls_cb with pkt_cls +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class RecordPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('msg', ''), - ) - -pkt = RecordPkt(cbor2.dumps([200, 'OK'])) -pkt.code.val == 200 and pkt.msg.val == 'OK' - -= CBORF_ARRAY roundtrip through cbor2 - multiple encode/decode cycles -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +class FixedSequenceChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -class RTPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('seq', 1), - CBORF_TEXT_STRING('data', 'payload'), +try: + CBORF_REMAINDER_OF( + "children", + [], + pkt_cls=FixedSequenceChild, + next_cls_cb=lambda *a: FixedSequenceChild, ) +except ValueError: + pass +else: + raise AssertionError("conflicting REMAINDER_OF selectors accepted") -pkt = RTPkt() -raw = bytes(pkt) -cbor2_dec = cbor2.loads(raw) -re_enc = cbor2.dumps(cbor2_dec) -pkt2 = RTPkt(re_enc) -pkt2.seq.val == 1 and pkt2.data.val == 'payload' - -= CBORF_ARRAY with null elements - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_NULL += CBORF_REMAINDER_OF rejects a packet instance as pkt_cls +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class NullArrPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('id', 5), - CBORF_NULL('opt'), - ) +class SeqOfChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -pkt = NullArrPkt() -dec = cbor2.loads(bytes(pkt)) -dec[0] == 5 and dec[1] is None +CBORF_REMAINDER_OF("ok", [], pkt_cls=SeqOfChild) +try: + CBORF_REMAINDER_OF("bad", [], pkt_cls=SeqOfChild()) +except ValueError: + pass +else: + raise AssertionError("REMAINDER_OF accepted a packet instance as pkt_cls") + += CBORF_ARRAY_OF rejects a packet instance as pkt_cls +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet -+ CBORF Fields - Interop: CBORF_ARRAY_OF with cbor2 +class ArrayOfChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= CBORF_ARRAY_OF with text strings - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cbor import CBOR_TEXT_STRING -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +CBORF_ARRAY_OF("ok", [], pkt_cls=ArrayOfChild) +try: + CBORF_ARRAY_OF("bad", [], pkt_cls=ArrayOfChild()) +except ValueError: + pass +else: + raise AssertionError("ARRAY_OF accepted a packet instance as pkt_cls") + += CBORF_REMAINDER_OF and ARRAY_OF reject fake CBOR_root classes +from scapy.cbor.cborfields import ( + CBORF_ARRAY_OF, + CBORF_REMAINDER_OF, + CBORF_UNSIGNED_INTEGER, +) + +class FakeRoot(object): + CBOR_root = CBORF_UNSIGNED_INTEGER("x", 0) -class TextListPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) +try: + CBORF_REMAINDER_OF("items", [], pkt_cls=FakeRoot) +except ValueError: + pass +else: + raise AssertionError("REMAINDER_OF accepted a non-Packet CBOR_root class") -pkt = TextListPkt(cbor2.dumps(['hello', 'world', 'foo'])) -len(pkt.items) == 3 and pkt.items[0].val == 'hello' and pkt.items[2].val == 'foo' +try: + CBORF_ARRAY_OF("items", [], pkt_cls=FakeRoot) +except ValueError: + pass +else: + raise AssertionError("ARRAY_OF accepted a non-Packet CBOR_root class") -= CBORF_ARRAY_OF with text strings - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cbor import CBOR_TEXT_STRING -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet += CBORF_REMAINDER_OF and ARRAY_OF reject plain Packet without CBOR_root +from scapy.packet import Packet +from scapy.fields import ByteField +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_REMAINDER_OF -class TextListPkt2(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) +class PlainPacket(Packet): + fields_desc = [ByteField("x", 0)] -pkt = TextListPkt2() -pkt.items = [CBOR_TEXT_STRING('abc'), CBOR_TEXT_STRING('def'), CBOR_TEXT_STRING('ghi')] -dec = cbor2.loads(bytes(pkt)) -dec == ['abc', 'def', 'ghi'] +try: + CBORF_REMAINDER_OF("items", [], pkt_cls=PlainPacket) +except ValueError: + pass +else: + raise AssertionError("REMAINDER_OF accepted Packet without CBOR_root") -= CBORF_ARRAY_OF with text strings roundtrip through cbor2 -import cbor2 -from scapy.cbor.cbor import CBOR_TEXT_STRING -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +try: + CBORF_ARRAY_OF("items", [], pkt_cls=PlainPacket) +except ValueError: + pass +else: + raise AssertionError("ARRAY_OF accepted Packet without CBOR_root") -class TextListRT(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) -pkt = TextListRT() -pkt.items = [CBOR_TEXT_STRING('x'), CBOR_TEXT_STRING('y'), CBOR_TEXT_STRING('z')] -raw = bytes(pkt) -re_enc = cbor2.dumps(cbor2.loads(raw)) -pkt2 = TextListRT(re_enc) -len(pkt2.items) == 3 and pkt2.items[1].val == 'y' ++ TypeError must not be used for callback signature probing -= CBORF_ARRAY_OF with byte strings - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cbor import CBOR_BYTE_STRING -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_BYTE_STRING += CBORF_REMAINDER_OF does not retry next_cls_cb when the callback itself raises TypeError +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_REMAINDER_OF from scapy.cborpacket import CBOR_Packet -class ByteListPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_BYTE_STRING) +BrokenNextClsCalls = [] +def broken_next_cls(*args): + BrokenNextClsCalls.append(len(args)) + raise TypeError("intentional next_cls_cb failure") -pkt = ByteListPkt(cbor2.dumps([b'\x01\x02', b'\x03\x04', b'\x05\x06'])) -len(pkt.items) == 3 and pkt.items[0].val == b'\x01\x02' and pkt.items[2].val == b'\x05\x06' +class BrokenCallbackParent(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF( + "children", + [], + next_cls_cb=broken_next_cls, + ) -= CBORF_ARRAY_OF with byte strings - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cbor import CBOR_BYTE_STRING -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet +try: + BrokenCallbackParent(b"\x01") +except TypeError as exc: + assert str(exc) == "intentional next_cls_cb failure" +except CBOR_Decoding_Error as exc: + raise AssertionError( + "next_cls_cb TypeError was unexpectedly translated: %r" % (exc,) + ) +else: + raise AssertionError("next_cls_cb TypeError was swallowed") -class ByteListPkt2(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_BYTE_STRING) +assert BrokenNextClsCalls == [4], BrokenNextClsCalls -pkt = ByteListPkt2() -pkt.items = [CBOR_BYTE_STRING(b'\xaa\xbb'), CBOR_BYTE_STRING(b'\xcc\xdd')] -dec = cbor2.loads(bytes(pkt)) -dec == [b'\xaa\xbb', b'\xcc\xdd'] -= CBORF_ARRAY_OF integers - large list cbor2 roundtrip -import cbor2 -from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER += Nested packet construction is not retried when the child constructor raises TypeError +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class BigIntList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) - -cbor2_data = cbor2.dumps(list(range(50))) -pkt = BigIntList(cbor2_data) -len(pkt.items) == 50 and pkt.items[0].val == 0 and pkt.items[49].val == 49 +class TypeErrorChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + init_calls = [] + def __init__(self, *args, **kwargs): + type(self).init_calls.append("_parent" in kwargs) + raise TypeError("intentional nested packet failure") -= CBORF_ARRAY_OF integers - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER -from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet +class TypeErrorParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, TypeErrorChild) -class IntListPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) +_old_dbg = conf.debug_dissector +try: + conf.debug_dissector = False + TypeErrorChild.init_calls[:] = [] + try: + TypeErrorParent(b"\x01") + except CBOR_Decoding_Error as exc: + assert "intentional nested packet failure" in str(exc) + else: + raise AssertionError("nested packet TypeError was unexpectedly swallowed") + assert TypeErrorChild.init_calls == [True], TypeErrorChild.init_calls + + conf.debug_dissector = True + TypeErrorChild.init_calls[:] = [] + try: + TypeErrorParent(b"\x01") + except TypeError as exc: + assert "intentional nested packet failure" in str(exc) + else: + raise AssertionError("TypeError was hidden despite debug_dissector") + assert TypeErrorChild.init_calls == [True], TypeErrorChild.init_calls +finally: + conf.debug_dissector = _old_dbg -pkt = IntListPkt() -pkt.items = [CBOR_UNSIGNED_INTEGER(i) for i in [10, 20, 30, 40, 50]] -dec = cbor2.loads(bytes(pkt)) -dec == [10, 20, 30, 40, 50] -+ CBORF Fields - Interop: CBORF_MAP with cbor2 ++ fixed-map unknown member preservation -= CBORF_MAP with text string values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER += CBORF_MAP preserves an unknown member when a known field is mutated +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class HeaderPkt(CBOR_Packet): +class ExtensibleMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('alg', 'ES256'), - CBORF_TEXT_STRING('typ', 'JWT'), - CBORF_INTEGER('ver', 1), + CBORF_UNSIGNED_INTEGER("a", 0), ) -pkt = HeaderPkt() -dec = cbor2.loads(bytes(pkt)) -isinstance(dec, dict) and dec.get('alg') == 'ES256' and dec.get('typ') == 'JWT' and dec.get('ver') == 1 +# {"x": 1, "a": 7} +wire = b"\xa2\x61x\x01\x61a\x07" +pkt = ExtensibleMap(wire) +assert pkt.a == 7 +# Exact received bytes are retained while the packet is untouched. +assert bytes(pkt) == wire -= CBORF_MAP - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet +# Mutation invalidates Scapy's raw packet cache. Rebuilding must not silently +# discard the unknown extension member. Fixed maps build deterministically, +# therefore "a" sorts before "x". +pkt.a = 8 +assert bytes(pkt) == b"\xa2\x61a\x08\x61x\x01" -class CredPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('sub', ''), - CBORF_INTEGER('iat', 0), - CBORF_BOOLEAN('admin', False), - ) -pkt = CredPkt(cbor2.dumps({'sub': 'user42', 'iat': 1700000000, 'admin': True})) -pkt.sub.val == 'user42' and pkt.iat.val == 1700000000 and pkt.admin.val is True - -= CBORF_MAP roundtrip through cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_INTEGER += CBORF_MAP preserves an unknown nested value after a known-field mutation +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class CoseHeaderPkt(CBOR_Packet): +class ExtensibleNestedMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('alg', 'ES256'), - CBORF_BYTE_STRING('kid', b'\x01\x02\x03\x04'), - CBORF_INTEGER('crit', 1), + CBORF_UNSIGNED_INTEGER("a", 0), ) -pkt = CoseHeaderPkt() -raw = bytes(pkt) -re_enc = cbor2.dumps(cbor2.loads(raw)) -pkt2 = CoseHeaderPkt(re_enc) -pkt2.alg.val == 'ES256' and pkt2.kid.val == b'\x01\x02\x03\x04' and pkt2.crit.val == 1 - -= CBORF_MAP with null value - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_NULL +# Indefinite input map: {"x": [1, {"k": 2}], "a": 7} +# The unknown value is itself indefinite/nested, exercising preservation of +# the complete encoded value rather than only simple Python-native values. +wire = ( + b"\xbf" + b"\x61x" + b"\x9f\x01\xbf\x61k\x02\xff\xff" + b"\x61a\x07" + b"\xff" +) +pkt = ExtensibleNestedMap(wire) +assert pkt.a == 7 +pkt.a = 8 + +# CBORF_MAP's normal rebuild is definite and deterministic; unknown members +# are re-encoded canonically after mutation (not as the original wire spans). +assert bytes(pkt) == ( + b"\xa2" + b"\x61a\x08" + b"\x61x\x82\x01\xa1\x61k\x02" +) + ++ CBOR_Packet.copy re-parents embedded children + += Copied CBOR packet children point at the clone, not the original +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class OptionalPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('id', 7), - CBORF_NULL('optional_data'), - ) +class CopyChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -pkt = OptionalPkt() -dec = cbor2.loads(bytes(pkt)) -dec.get('id') == 7 and dec.get('optional_data') is None +class CopyParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, pkt_cls=CopyChild) -= CBORF_MAP with boolean values roundtrip with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_BOOLEAN, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +a = CopyParent(child=CopyChild(n=1)) +assert a.child.parent is a +b = a.copy() +assert b.child is not a.child +assert b.child.parent is b, b.child.parent +assert a.child.parent is a +assert b.child.n == 1 -class FlagsPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_BOOLEAN('active', True), - CBORF_BOOLEAN('verified', False), - CBORF_INTEGER('level', 3), - CBORF_TEXT_STRING('role', 'admin'), - ) -pkt = FlagsPkt() -raw = bytes(pkt) -dec = cbor2.loads(raw) -re_enc = cbor2.dumps(dec) -pkt2 = FlagsPkt(re_enc) -pkt2.active.val is True and pkt2.verified.val is False and pkt2.level.val == 3 and pkt2.role.val == 'admin' ++ CBORF_MAP deterministic unknown rebuild -= CBORF_MAP skip unknown keys from cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER += CBORF_MAP re-encodes non-preferred unknown members after a known-field mutation +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class KnownKeysPkt(CBOR_Packet): +class DeterministicUnknownMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('known', 'default'), - CBORF_INTEGER('count', 0), + CBORF_UNSIGNED_INTEGER("z", 0), ) -pkt = KnownKeysPkt(cbor2.dumps({'known': 'found', 'count': 42, 'extra': 'ignored'})) -pkt.known.val == 'found' and pkt.count.val == 42 - -+ CBORF Fields - Interop: CBOR_Packet complex structures with cbor2 - -= CBOR_Packet CBORF_ARRAY with multiple field types - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet - -class SensorReading(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('sensor_id', 42), - CBORF_TEXT_STRING('unit', 'fahrenheit'), - CBORF_INTEGER('value', 98), - CBORF_BOOLEAN('alarm', True), - ) +# known "z":1, unknown "a":1 with non-preferred key (78 01 61) and value (18 01) +wire = b"\xa2\x61z\x01\x78\x01\x61\x18\x01" +pkt = DeterministicUnknownMap(wire) +assert bytes(pkt) == wire +pkt.z = 2 +assert bytes(pkt) == b"\xa2\x61a\x01\x61z\x02" -pkt = SensorReading() -dec = cbor2.loads(bytes(pkt)) -dec[0] == 42 and dec[1] == 'fahrenheit' and dec[2] == 98 and dec[3] is True -= CBOR_Packet with CBORF_MAP multiple field types - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN, CBORF_BYTE_STRING += CBORF_MAP recursively determinizes nested unknown map keys after mutation +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class DeviceInfo(CBOR_Packet): +class NestedUnknownMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('id', 0), - CBORF_TEXT_STRING('label', ''), - CBORF_BOOLEAN('online', False), - CBORF_BYTE_STRING('hwaddr', b''), + CBORF_UNSIGNED_INTEGER("z", 0), ) -pkt = DeviceInfo(cbor2.dumps({'id': 1001, 'label': 'device-01', 'online': True, 'hwaddr': b'\x00\x11\x22\x33\x44\x55'})) -dec = cbor2.loads(bytes(pkt)) -dec.get('id') == 1001 and dec.get('label') == 'device-01' and dec.get('online') is True and dec.get('hwaddr') == b'\x00\x11\x22\x33\x44\x55' +# known z:1, unknown x:{b:1,a:2} with nested keys out of deterministic order +wire = b"\xa2\x61z\x01\x61x\xa2\x61b\x01\x61a\x02" +pkt = NestedUnknownMap(wire) +assert bytes(pkt) == wire +pkt.z = 2 +assert bytes(pkt) == b"\xa2\x61x\xa2\x61a\x02\x61b\x01\x61z\x02" -= CBOR_Packet CBORF_MAP full cbor2 roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN + += CBORF_MAP copy isolates nested unknown extension values from the original packet +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class ClaimsPkt(CBOR_Packet): +class MapCopyIsolation(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', ''), - CBORF_TEXT_STRING('sub', ''), - CBORF_INTEGER('exp', 0), - CBORF_BOOLEAN('admin', False), + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", ) -pkt = ClaimsPkt(cbor2.dumps({'iss': 'auth.example.com', 'sub': 'user99', 'exp': 9999999, 'admin': False})) -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec.get('iss') == 'auth.example.com' and dec.get('sub') == 'user99' and dec.get('exp') == 9999999 and dec.get('admin') is False - -= CBOR_Packet CBORF_MAP with negative integer - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING +wire = ( + b"\xbf" + b"\x61x" + b"\x9f\x01\xbf\x61k\x02\xff\xff" + b"\x61a\x07" + b"\xff" +) +orig = MapCopyIsolation(wire) +clone = orig.copy() +assert clone.unknown_pairs is not orig.unknown_pairs +assert clone.unknown_pairs[0][1] is not orig.unknown_pairs[0][1] +clone.unknown_pairs[0][1].val.append(CBOR_UNSIGNED_INTEGER(3)) +assert len(orig.unknown_pairs[0][1].val) == 2 +assert len(clone.unknown_pairs[0][1].val) == 3 + + ++ CBORF_ARRAY_OF indefinite decoding + += CBORF_ARRAY_OF decodes indefinite-length scalar arrays +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class OffsetPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('name', ''), - CBORF_INTEGER('offset', 0), - CBORF_INTEGER('count', 0), - ) +class IndefUIntArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], pkt_cls=CBORF_UNSIGNED_INTEGER) -pkt = OffsetPkt(cbor2.dumps({'name': 'delta', 'offset': -1024, 'count': 512})) -pkt.name.val == 'delta' and pkt.offset.val == -1024 and pkt.count.val == 512 +wire = b"\x9f\x01\x02\x03\xff" +pkt = IndefUIntArray(wire) +assert pkt.values == [1, 2, 3] +assert bytes(pkt) == wire -+ CBOR_Packet - nested CBORF_PACKET structures -= CBORF_PACKET three levels deep: Outer(ARRAY) -> Middle(ARRAY) -> Inner(ARRAY) -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET += CBORF_ARRAY_OF decodes indefinite-length packet arrays +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class NestInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('x', 0), - CBORF_INTEGER('y', 0), - ) +class IndefChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -class NestMiddle(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('zone', ''), - CBORF_PACKET('point', None, NestInner), - ) +class IndefPacketArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("children", [], pkt_cls=IndefChild) -class NestOuter(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_PACKET('region', None, NestMiddle), - ) +wire = b"\x9f\x01\x02\xff" +pkt = IndefPacketArray(wire) +assert len(pkt.children) == 2 +assert pkt.children[0].n == 1 +assert pkt.children[0].parent is pkt +assert pkt.children[1].parent is pkt -inner = NestInner(cbor2.dumps([30, 40])) -mid = NestMiddle() -mid.zone.val = 'north' -mid.point = inner -outer = NestOuter() -outer.version.val = 2 -outer.region = mid -raw = bytes(outer) -outer2 = NestOuter(raw) -outer2.version.val == 2 and outer2.region.zone.val == 'north' and outer2.region.point.x.val == 30 and outer2.region.point.y.val == 40 -= CBORF_PACKET three-level nesting cbor2 interop -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET -from scapy.cborpacket import CBOR_Packet ++ Homogeneous collection max_count limits -class NestInner2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('x', 0), - CBORF_INTEGER('y', 0), - ) += CBORF_ARRAY_OF respects explicit max_count on definite arrays +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet -class NestMiddle2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('zone', ''), - CBORF_PACKET('point', None, NestInner2), +class LimitedArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "values", [], pkt_cls=CBORF_UNSIGNED_INTEGER, max_count=2 ) -class NestOuter2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_PACKET('region', None, NestMiddle2), - ) +assert LimitedArray(b"\x82\x01\x02").values == [1, 2] +try: + LimitedArray(b"\x83\x01\x02\x03") + assert False, "definite ARRAY_OF ignored max_count" +except CBOR_Decoding_Error: + pass -inner = NestInner2(cbor2.dumps([10, 20])) -mid = NestMiddle2() -mid.zone.val = 'south' -mid.point = inner -outer = NestOuter2() -outer.version.val = 1 -outer.region = mid -dec = cbor2.loads(bytes(outer)) -dec == [1, ['south', [10, 20]]] -= CBORF_PACKET inside CBORF_MAP: cbor2 decode matches field values -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_MAP, CBORF_PACKET += CBORF_ARRAY_OF respects explicit max_count on indefinite arrays +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class MapInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('px', 0), - CBORF_INTEGER('py', 0), +class LimitedIndefArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "values", [], pkt_cls=CBORF_UNSIGNED_INTEGER, max_count=2 ) -class MapWithNestedPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('label', ''), - CBORF_PACKET('coords', None, MapInner), - ) +assert LimitedIndefArray(b"\x9f\x01\x02\xff").values == [1, 2] +try: + LimitedIndefArray(b"\x9f\x01\x02\x03\xff") + assert False, "indefinite ARRAY_OF ignored max_count" +except CBOR_Decoding_Error: + pass -inner = MapInner(cbor2.dumps([5, 7])) -pkt = MapWithNestedPkt() -pkt.label.val = 'origin' -pkt.coords = inner -dec = cbor2.loads(bytes(pkt)) -dec.get('label') == 'origin' and dec.get('coords') == [5, 7] -= CBORF_PACKET inside CBORF_MAP: Scapy decode roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_MAP, CBORF_PACKET += CBORF_ARRAY_OF falls back to conf.max_list_count +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class CoordsInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('px', 0), - CBORF_INTEGER('py', 0), - ) +class ConfLimitedArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], pkt_cls=CBORF_UNSIGNED_INTEGER) -class CoordsOuter(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('label', ''), - CBORF_PACKET('coords', None, CoordsInner), - ) +old = conf.max_list_count +conf.max_list_count = 2 +try: + assert ConfLimitedArray(b"\x82\x01\x02").values == [1, 2] + try: + ConfLimitedArray(b"\x83\x01\x02\x03") + assert False, "ARRAY_OF ignored conf.max_list_count" + except CBOR_Decoding_Error: + pass +finally: + conf.max_list_count = old -inner = CoordsInner(cbor2.dumps([5, 7])) -pkt = CoordsOuter() -pkt.label.val = 'origin' -pkt.coords = inner -pkt2 = CoordsOuter(bytes(pkt)) -pkt2.label.val == 'origin' and pkt2.coords.px.val == 5 and pkt2.coords.py.val == 7 -= CBORF_PACKET: nested MAP-in-MAP via CBORF_PACKET (Document/Metadata) -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_INTEGER, CBORF_PACKET += CBORF_REMAINDER_OF respects max_count +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_REMAINDER_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class DocMeta(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('creator', ''), - CBORF_INTEGER('version', 0), - ) - -class DocPacket(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('title', ''), - CBORF_BYTE_STRING('body', b''), - CBORF_PACKET('metadata', None, DocMeta), +class LimitedSeq(CBOR_Packet): + CBOR_root = CBORF_REMAINDER_OF( + "values", [], pkt_cls=CBORF_UNSIGNED_INTEGER, max_count=2 ) -meta = DocMeta() -meta.creator.val = 'alice' -meta.version.val = 3 -doc = DocPacket() -doc.title.val = 'My Document' -doc.body.val = b'hello world' -doc.metadata = meta -raw = bytes(doc) -dec = cbor2.loads(raw) -dec.get('title') == 'My Document' and dec.get('body') == b'hello world' and dec.get('metadata') == {'creator': 'alice', 'version': 3} - -= CBORF_PACKET: nested MAP-in-MAP Scapy roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_INTEGER, CBORF_PACKET +assert LimitedSeq(b"\x01\x02").values == [1, 2] +try: + LimitedSeq(b"\x01\x02\x03") + assert False, "REMAINDER_OF ignored max_count" +except CBOR_Decoding_Error: + pass + += CBORF_MAP respects conf.max_list_count on definite maps +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet -class DocMeta2(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('creator', ''), - CBORF_INTEGER('version', 0), - ) - -class DocPacket2(CBOR_Packet): +class LimitedMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('title', ''), - CBORF_BYTE_STRING('body', b''), - CBORF_PACKET('metadata', None, DocMeta2), + CBORF_optional(CBORF_UNSIGNED_INTEGER("a", 0)), + unknown_field="unknown_pairs", ) -meta = DocMeta2() -meta.creator.val = 'bob' -meta.version.val = 7 -doc = DocPacket2() -doc.title.val = 'Report' -doc.body.val = b'\x01\x02\x03' -doc.metadata = meta -raw = bytes(doc) -doc2 = DocPacket2(raw) -doc2.title.val == 'Report' and doc2.body.val == b'\x01\x02\x03' and doc2.metadata.creator.val == 'bob' and doc2.metadata.version.val == 7 - -+ CBOR_Packet - CBORF_ARRAY_OF with CBOR_Packet elements - -= CBORF_ARRAY_OF with CBOR_Packet class: cbor2 list of lists → Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +old = conf.max_list_count +conf.max_list_count = 2 +try: + # Exactly at limit: two unknown extension pairs + at_limit = LimitedMap(b"\xa2\x61x\x01\x61y\x02") + assert [k for k, _ in at_limit.unknown_pairs] == ["x", "y"] + assert [v.val for _, v in at_limit.unknown_pairs] == [1, 2] + try: + # Three unknown pairs exceeds conf.max_list_count + LimitedMap(b"\xa3\x61x\x01\x61y\x02\x61z\x03") + assert False, "definite MAP ignored conf.max_list_count" + except CBOR_Decoding_Error: + pass +finally: + conf.max_list_count = old + += CBORF_MAP respects conf.max_list_count on indefinite maps +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import ( + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) from scapy.cborpacket import CBOR_Packet -class StatusItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('msg', ''), +class LimitedMapIndef(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_optional(CBORF_UNSIGNED_INTEGER("a", 0)), + unknown_field="unknown_pairs", ) -class StatusList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('statuses', [], StatusItem) - -raw = cbor2.dumps([[200, 'OK'], [201, 'Created'], [204, 'No Content']]) -pkt = StatusList(raw) -len(pkt.statuses) == 3 and pkt.statuses[0].code.val == 200 and pkt.statuses[1].msg.val == 'Created' and pkt.statuses[2].code.val == 204 - -= CBORF_ARRAY_OF with CBOR_Packet class: Scapy encode → cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF -from scapy.cborpacket import CBOR_Packet - -class ErrItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('msg', ''), - ) +old = conf.max_list_count +conf.max_list_count = 2 +try: + at_limit = LimitedMapIndef(b"\xbf\x61x\x01\x61y\x02\xff") + assert [k for k, _ in at_limit.unknown_pairs] == ["x", "y"] + assert [v.val for _, v in at_limit.unknown_pairs] == [1, 2] + try: + LimitedMapIndef(b"\xbf\x61x\x01\x61y\x02\x61z\x03\xff") + assert False, "indefinite MAP ignored conf.max_list_count" + except CBOR_Decoding_Error: + pass +finally: + conf.max_list_count = old -class ErrList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('errors', [], ErrItem) -pkt = ErrList() -pkt.errors = [ErrItem(cbor2.dumps([404, 'Not Found'])), ErrItem(cbor2.dumps([500, 'Server Error']))] -dec = cbor2.loads(bytes(pkt)) -dec == [[404, 'Not Found'], [500, 'Server Error']] ++ Medium-severity review follow-ups -= CBORF_ARRAY_OF with CBOR_Packet class: roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF += CBORF_FLOAT rebuilds preferred half-float after cache clear +from scapy.cbor.cborfields import CBORF_FLOAT from scapy.cborpacket import CBOR_Packet -class MsgItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('id', 0), - CBORF_TEXT_STRING('txt', ''), - ) - -class MsgList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('messages', [], MsgItem) - -raw = cbor2.dumps([[1, 'hello'], [2, 'world'], [3, 'foo']]) -pkt = MsgList(raw) -raw2 = bytes(pkt) -pkt2 = MsgList(raw2) -len(pkt2.messages) == 3 and pkt2.messages[2].id.val == 3 and pkt2.messages[2].txt.val == 'foo' - -= CBORF_ARRAY_OF with CBOR_Packet class: empty list -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF +class FloatPkt(CBOR_Packet): + CBOR_root = CBORF_FLOAT("value", 0.0) + +wire = b"\xf9\x3e\x00" # 1.5 as half +pkt = FloatPkt(wire) +assert abs(pkt.value - 1.5) < 1e-6 +assert bytes(pkt) == wire # untouched raw cache +pkt.raw_packet_cache = None +pkt.raw_packet_cache_fields = None +assert bytes(pkt) == wire # preferred encoding for 1.5 is half +pkt.value = 1.5 +assert bytes(pkt) == wire + += Unframed CBORF_ITEMS leaves trailing CBOR items +from scapy.cbor.cborfields import CBORF_ITEMS, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class EmptyItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('val', 0), +class TwoInts(CBOR_Packet): + CBOR_root = CBORF_ITEMS( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_UNSIGNED_INTEGER("b", 0), ) -class EmptyItemList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], EmptyItem) +pkt = TwoInts(b"\x01\x02\x03") +assert pkt.a == 1 and pkt.b == 2 +assert isinstance(pkt.payload, Raw) or pkt.original.endswith(b"\x03") +# Remaining third item is not consumed by the schema +remain = TwoInts.CBOR_root._dissect_counted(TwoInts(), b"\x01\x02\x03").remaining +assert remain == b"\x03" -pkt = EmptyItemList() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec == [] and len(EmptyItemList(raw).items) == 0 += CBORF_SEMANTIC_TAG rejects the wrong tag number +from scapy.cbor.cborfields import ( + CBORF_SEMANTIC_TAG, CBORF_INTEGER, CBOR_Type_Mismatch, +) -= CBORF_ARRAY_OF with CBOR_Packet class inside CBORF_MAP -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF, CBORF_MAP, CBORF_PACKET +fld = CBORF_SEMANTIC_TAG(1, CBORF_INTEGER("ts", 0)) +try: + fld._parse_tag_head(b"\xc2\x00") # tag 2 +except CBOR_Type_Mismatch: + pass +else: + raise AssertionError("wrong tag accepted") + += Deterministic encoder accepts CBOR_Object wrappers +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER, CBOR_TEXT_STRING, CBOR_MAP, CBORMapData +from scapy.cbor.cborcodec import CBORcodec_Object + +obj = CBOR_MAP(CBORMapData([ + (CBOR_TEXT_STRING("b"), CBOR_UNSIGNED_INTEGER(1)), + (CBOR_TEXT_STRING("a"), CBOR_UNSIGNED_INTEGER(2)), +])) +wire = CBORcodec_Object.encode_cbor_item_deterministic(obj) +assert wire == b"\xa2\x61a\x02\x61b\x01" + += Non-determinism scanner reports bare break and short simples +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic + +issues = cbor_find_non_deterministic(b"\xff") +assert issues and "break" in issues[0][1].lower() +issues = cbor_find_non_deterministic(b"\xf8\x14") # simple 20 via AI=24 +assert issues and "simple" in issues[0][1].lower() + += CBORMapData lookup treats +0.0 and -0.0 as equivalent map keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {+0.0: 1, -0.0: 2} as half floats — duplicate under RFC 8949 key equivalence +wire = b"\xa2\xf9\x00\x00\x01\xf9\x80\x00\x02" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "+0.0 / -0.0 duplicate keys were accepted" +except CBOR_Codec_Decoding_Error: + pass + += Budget-skipped optional yields a matching item to trailing CBORF_ANY +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_ARRAY, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) from scapy.cborpacket import CBOR_Packet -class EventItem(CBOR_Packet): +class OptionalTaggedBeforeAny(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('evt', ''), - CBORF_INTEGER('ts', 0), - ) - -class EventLog(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('events', [], EventItem) - -class Report(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('source', ''), - CBORF_INTEGER('count', 0), - CBORF_PACKET('log', None, EventLog), + CBORF_optional( + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) + ), + CBORF_ANY("fallback", None), ) -log = EventLog() -log.events = [EventItem(cbor2.dumps(['boot', 1000])), EventItem(cbor2.dumps(['login', 2000]))] -rpt = Report() -rpt.source.val = 'sensor-1' -rpt.count.val = 2 -rpt.log = log -raw = bytes(rpt) -dec = cbor2.loads(raw) -dec.get('source') == 'sensor-1' and dec.get('count') == 2 and dec.get('log') == [['boot', 1000], ['login', 2000]] +# Outer type matches the optional, but the sole item is reserved for ANY. +pkt = OptionalTaggedBeforeAny(b"\x81\xc1\x61x") +assert pkt.getfieldval("tagged_value") is CBOR_ABSENT +assert pkt.fallback is not None -+ CBOR_Packet - CBORF_optional extended tests +ok = OptionalTaggedBeforeAny(b"\x81\xc1\x01") +assert ok.getfieldval("tagged_value") is CBOR_ABSENT +assert ok.fallback is not None -= CBORF_optional: type mismatch in CBORF_ARRAY sets field to None -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional += CBORF_MAP rejects non-text map keys +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER, CBOR_Decoding_Error from scapy.cborpacket import CBOR_Packet -class TwoFieldPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_optional(CBORF_TEXT_STRING('description', 'none')), - ) - -raw = cbor2.dumps([7, 99]) -pkt = TwoFieldPkt(raw) -pkt.version.val == 7 and pkt.description is None - -= CBORF_optional: correct type present is decoded normally -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional -from scapy.cborpacket import CBOR_Packet +class NamedMap(CBOR_Packet): + CBOR_root = CBORF_MAP(CBORF_UNSIGNED_INTEGER("a", 0)) -class OptPresentPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_optional(CBORF_TEXT_STRING('description', '')), - ) +# {1: 2} — integer key is not allowed for schema maps +try: + NamedMap(b"\xa1\x01\x02") +except CBOR_Decoding_Error: + pass +else: + raise AssertionError("CBORF_MAP accepted an integer key") + += Indefinite text rejects a UTF-8 code point split across chunks +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# U+00E4 is UTF-8 C3 A4; RFC 8949 forbids splitting a code point across chunks. +wire = b"\x7f\x61\xc3\x61\xa4\xff" +try: + CBOR_Codecs.CBOR.dec(wire) +except CBOR_Codec_Decoding_Error: + pass +else: + raise AssertionError("split UTF-8 code point across chunks was accepted") -raw = cbor2.dumps([3, 'hello world']) -pkt = OptPresentPkt(raw) -pkt.version.val == 3 and pkt.description.val == 'hello world' +# Valid split on a code-point boundary still works. +obj, rem = CBOR_Codecs.CBOR.dec(b"\x7f\x62\xc3\xa4\x61\x61\xff") +assert rem == b"" and obj.val == "äa" -= CBORF_optional: encode and decode roundtrip with present field -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional += CBORF_FLOAT preserves non-preferred NaN payload only while raw cache is valid +from scapy.cbor.cborfields import CBORF_FLOAT from scapy.cborpacket import CBOR_Packet -class OptRTPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 1), - CBORF_optional(CBORF_TEXT_STRING('title', '')), - ) - -pkt = OptRTPkt() -pkt.title.val = 'test title' -raw = bytes(pkt) -pkt2 = OptRTPkt(raw) -pkt2.version.val == 1 and pkt2.title.val == 'test title' - -= CBORF_optional: cbor2 interop - optional present -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +class FloatPkt(CBOR_Packet): + CBOR_root = CBORF_FLOAT("value", 0.0) + +# binary64 NaN with a low payload bit +wire = bytes.fromhex("fb7ff8000000000001") +pkt = FloatPkt(wire) +assert pkt.value != pkt.value # NaN +assert bytes(pkt) == wire +pkt.raw_packet_cache = None +pkt.raw_packet_cache_fields = None +# After cache clear, rebuild uses preferred quiet NaN (binary16). +assert bytes(pkt) == bytes.fromhex("f97e00") + += CBORF_TEXT_STRING rejects bytes values instead of str(bytes) corruption +from scapy.cbor.cborfields import CBORF_TEXT_STRING from scapy.cborpacket import CBOR_Packet -class OptInteropPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('seq', 0), - CBORF_optional(CBORF_TEXT_STRING('note', '')), - ) - -pkt = OptInteropPkt() -pkt.note.val = 'cbor2 interop' -dec = cbor2.loads(bytes(pkt)) -dec == [0, 'cbor2 interop'] +class TextPkt(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING("label", "") -= CBORF_optional inside CBORF_MAP: key present in cbor2 dict is decoded -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +pkt = TextPkt() +try: + pkt.label = b"hi" +except TypeError: + pass +else: + raise AssertionError("bytes were coerced via str(bytes)") + += CBORF_ANY preserves non-preferred float wire only while raw cache is valid +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY from scapy.cborpacket import CBOR_Packet -class ConfigWithOpt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('timeout', 30), - CBORF_optional(CBORF_TEXT_STRING('endpoint', '')), - CBORF_INTEGER('retries', 3), - ) - -pkt = ConfigWithOpt(cbor2.dumps({'timeout': 60, 'endpoint': 'https://example.com', 'retries': 5})) -pkt.timeout.val == 60 and pkt.endpoint.val == 'https://example.com' and pkt.retries.val == 5 - -= CBORF_optional inside CBORF_MAP: missing key stays at default -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional +class AnyFloatPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +# binary64 NaN with a low payload bit +wire = bytes.fromhex("fb7ff8000000000001") +pkt = AnyFloatPkt(wire) +assert isinstance(pkt.value, CBOR_FLOAT) +assert pkt.value.val != pkt.value.val # NaN +assert bytes(pkt) == wire +pkt.raw_packet_cache = None +pkt.raw_packet_cache_fields = None +# CBOR_FLOAT.enc() still preserves _encoded when present on the object. +assert bytes(pkt) == wire + += Assigning CBOR_FLOAT.val invalidates the retained wire encoding +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborcodec import CBORcodec_Object + +obj = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +assert obj.enc() == bytes.fromhex("fb3ff8000000000000") +obj.val = 2.0 +decoded, rem = CBORcodec_Object.dec(obj.enc()) +assert rem == b"" +assert decoded.val == 2.0 +# Explicit assignment clears the cache even when the value is unchanged. +obj2 = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +obj2.val = 1.5 +assert obj2._encoded is None +assert obj2.enc() == bytes.fromhex("f93e00") + += CBORF_ANY float mutation rebuilds from the new semantic value +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY from scapy.cborpacket import CBOR_Packet -class ConfigNoOpt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('timeout', 30), - CBORF_optional(CBORF_TEXT_STRING('endpoint', '')), - CBORF_INTEGER('retries', 3), - ) - -pkt = ConfigNoOpt(cbor2.dumps({'timeout': 15, 'retries': 2})) -pkt.timeout.val == 15 and pkt.retries.val == 2 - -+ CBOR_Packet - CBORF_SEMANTIC_TAG extended tests - -= CBORF_SEMANTIC_TAG with TEXT_STRING inner: Scapy encode, cbor2 decode as datetime -import cbor2, datetime -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_TEXT_STRING, CBORF_SEMANTIC_TAG +class AnyFloatMutPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +wire = bytes.fromhex("fb3ff8000000000000") # 1.5 as binary64 +pkt = AnyFloatMutPkt(wire) +assert bytes(pkt) == wire +assert isinstance(pkt.value, CBOR_FLOAT) +pkt.value.val = 2.0 +rebuilt = bytes(pkt) +parsed = AnyFloatMutPkt(rebuilt) +assert parsed.value.val == 2.0 + += CBORF_ANY same-value float mutation invalidates the packet raw cache +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY from scapy.cborpacket import CBOR_Packet -class DatetimePkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 0, CBORF_TEXT_STRING('dt', '')) - -pkt = DatetimePkt() -pkt.dt.val = '2023-01-15T12:00:00Z' -dec = cbor2.loads(bytes(pkt)) -isinstance(dec, datetime.datetime) - -= CBORF_SEMANTIC_TAG with INTEGER inner: Scapy encode, cbor2 decode as datetime -import cbor2, datetime -from scapy.cbor.cborfields import CBORF_INTEGER, CBORF_SEMANTIC_TAG +class AnyFloatSameMutPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +wire = bytes.fromhex("fb3ff8000000000000") # 1.5 as binary64 +pkt = AnyFloatSameMutPkt(wire) +assert bytes(pkt) == wire +assert isinstance(pkt.value, CBOR_FLOAT) +pkt.value.val = 1.5 +rebuilt = bytes(pkt) +assert rebuilt != wire +assert rebuilt == bytes.fromhex("f93e00") +assert AnyFloatSameMutPkt(rebuilt).value.val == 1.5 + += CBORF_ANY NaN mutation does not reuse the original encoded payload +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY from scapy.cborpacket import CBOR_Packet +import math -class UnixTimePkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) - -pkt = UnixTimePkt() -pkt.ts.val = 1700000000 -dec = cbor2.loads(bytes(pkt)) -isinstance(dec, datetime.datetime) - -= CBORF_SEMANTIC_TAG roundtrip: Scapy encode → Scapy decode preserves inner value -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER, CBORF_SEMANTIC_TAG +class AnyNanMutPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +wire = bytes.fromhex("fb7ff8000000000001") +pkt = AnyNanMutPkt(wire) +assert isinstance(pkt.value, CBOR_FLOAT) +assert bytes(pkt) == wire +pkt.value.val = float("nan") +rebuilt = bytes(pkt) +assert rebuilt != wire +assert math.isnan(AnyNanMutPkt(rebuilt).value.val) + += RandCBORObject generates encodable objects including nested containers +import random +from scapy.cbor.cbor import ( + RandCBORObject, + CBOR_UNSIGNED_INTEGER, + CBOR_ARRAY, + CBOR_MAP, + CBOR_TEXT_STRING, + CBOR_NULL, +) + +random.seed(42) +obj = RandCBORObject()._fix() +assert bytes(obj) # encodable +# Custom list forces deep recursion fallbacks and array/map nesting. +nested = RandCBORObject(objlist=[CBOR_ARRAY, CBOR_MAP])._fix(n=0) +assert isinstance(nested, (CBOR_ARRAY, CBOR_MAP)) +assert bytes(nested) +# Depth cap strips recursive types. +leaf = RandCBORObject(objlist=[CBOR_ARRAY, CBOR_MAP])._fix(n=10) +assert not isinstance(leaf, (CBOR_ARRAY, CBOR_MAP)) +assert bytes(leaf) +# Only recursive types at high depth still yields a leaf via fallback. +only_recursive = RandCBORObject(objlist=[CBOR_ARRAY])._fix(n=10) +assert isinstance(only_recursive, CBOR_UNSIGNED_INTEGER) +simple = RandCBORObject(objlist=[CBOR_TEXT_STRING, CBOR_NULL])._fix() +assert bytes(simple) + += Lightweight count path rejects nesting beyond MAX_CBOR_NESTING +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + MAX_CBOR_NESTING, + cbor_count_items, +) +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_ITEMS, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class TagRTPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) +# Nested arrays: 0x81 (array of 1) repeated, then unsigned 0. +too_deep = b"\x81" * (MAX_CBOR_NESTING + 1) + b"\x00" +try: + cbor_count_items(too_deep) + assert False, "expected nesting-depth error" +except CBOR_Codec_Decoding_Error as err: + assert "nesting" in str(err).lower() +except RecursionError: + assert False, "pre-scan leaked RecursionError" -pkt = TagRTPkt() -pkt.ts.val = 1700000000 -raw = bytes(pkt) -pkt2 = TagRTPkt(raw) -pkt2.ts.val == 1700000000 +class DeepSeq(CBOR_Packet): + CBOR_root = CBORF_ITEMS(CBORF_UNSIGNED_INTEGER("n", 0)) -= CBORF_SEMANTIC_TAG: tag byte matches CBOR major type 6 encoding -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING, CBORF_SEMANTIC_TAG -from scapy.cborpacket import CBOR_Packet +try: + DeepSeq(too_deep) + assert False, "SEQUENCE accepted over-nested input" +except Exception as err: + assert not isinstance(err, RecursionError) + # Streaming SEQUENCE does not pre-count the buffer; the first item is + # rejected as the wrong major type rather than a nesting pre-scan error. + msg = str(err).lower() + assert "nesting" in msg or "major type" in msg + +# Indefinite array pre-scan uses the same depth-limited skip path. +indef_too_deep = b"\x9f" + too_deep + b"\xff" + +class DeepIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_UNSIGNED_INTEGER("n", 0), + ) -class TagBigNum(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 2, CBORF_BYTE_STRING('n', b'')) +try: + DeepIndef(indef_too_deep) + assert False, "indefinite ARRAY accepted over-nested input" +except Exception as err: + assert not isinstance(err, RecursionError) + assert "nesting" in str(err).lower() + += Lightweight skip rejects malformed indefinite string chunks +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + cbor_count_items, + cbor_item_span, +) + +# Indefinite byte string (0x5f) whose "chunk" is an array (0x80), then break. +bad_bstr = b"\x5f\x80\xff" +try: + cbor_count_items(bad_bstr) + assert False, "count accepted wrong-type indefinite byte chunk" +except CBOR_Codec_Decoding_Error as err: + assert "chunk" in str(err).lower() or "major type" in str(err).lower() -pkt = TagBigNum() -pkt.n.val = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00' -raw = bytes(pkt) -raw[0:1] == b'\xc2' +try: + cbor_item_span(bad_bstr) + assert False, "span accepted wrong-type indefinite byte chunk" +except CBOR_Codec_Decoding_Error as err: + assert "chunk" in str(err).lower() or "major type" in str(err).lower() -= CBORF_SEMANTIC_TAG: byte-exact comparison with cbor2 CBORTag -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER, CBORF_SEMANTIC_TAG -from scapy.cborpacket import CBOR_Packet +# Indefinite text string (0x7f) with an unsigned integer chunk (0x01). +bad_tstr = b"\x7f\x01\xff" +try: + cbor_item_span(bad_tstr) + assert False, "span accepted wrong-type indefinite text chunk" +except CBOR_Codec_Decoding_Error: + pass -class TagCmpPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) +# Nested indefinite byte string chunk is rejected. +nested_indef = b"\x5f\x5f\xff\xff" +try: + cbor_count_items(nested_indef) + assert False, "count accepted nested indefinite byte string" +except CBOR_Codec_Decoding_Error: + pass + +# Well-formed indefinite byte string still spans correctly. +good = b"\x5f\x41a\xff" + b"\x00" +item, rest = cbor_item_span(good) +assert item == b"\x5f\x41a\xff" +assert rest == b"\x00" + += CBOR object display helpers and decoding-error repr +import copy +from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_BYTE_STRING, + CBOR_DECODING_ERROR, + CBOR_Error, + CBOR_FALSE, + CBOR_FLOAT, + CBOR_MAP, + CBOR_NULL, + CBORMapData, + CBOR_Object, + CBOR_TRUE, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_TEXT_STRING, + CBOR_UNDEFINED, + CBOR_UNSIGNED_INTEGER, +) + +assert "h'6162'" in repr(CBOR_BYTE_STRING(b"ab")) +assert "CBOR_ARRAY" in CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1), 2]).strshow() +assert "CBOR_MAP" in CBOR_MAP(CBORMapData([(1, CBOR_TRUE())])).strshow() +assert "CBOR_MAP" in CBOR_MAP({1: CBOR_FALSE()}).strshow() +assert "CBORMapData" in repr(CBORMapData([(b"k", 1)])) +assert "CBOR_SEMANTIC_TAG" in repr(CBOR_SEMANTIC_TAG((1, CBOR_TEXT_STRING("x")))) +assert "CBOR_SIMPLE_VALUE" in repr(CBOR_SIMPLE_VALUE(41)) +assert isinstance(CBOR_UNDEFINED(), CBOR_UNDEFINED) +assert not CBOR_UNDEFINED() +assert copy.copy(CBOR_UNDEFINED()) is CBOR_UNDEFINED() +assert copy.deepcopy(CBOR_UNDEFINED()) is CBOR_UNDEFINED() +bad = bytes.fromhex("ff") +err = CBOR_DECODING_ERROR(bad, exc=ValueError("boom")) +assert "boom" in repr(err) +assert err.enc() == bad +assert CBOR_DECODING_ERROR(CBOR_NULL()).enc() == bytes(CBOR_NULL()) +untagged_ok = False +try: + CBOR_Object(None).enc() +except CBOR_Error: + untagged_ok = True + +assert untagged_ok +assert CBOR_TRUE() == CBOR_TRUE() +assert CBOR_TRUE() != CBOR_FALSE() +encoded = bytes.fromhex("fa3fc00000") +assert CBOR_FLOAT(1.5, encoded=encoded).enc() == encoded +True -pkt = TagCmpPkt() -pkt.ts.val = 9999999 -bytes(pkt) == cbor2.dumps(cbor2.CBORTag(1, 9999999)) -= CBORF_SEMANTIC_TAG inside CBORF_MAP: Scapy encode, cbor2 decode -import cbor2, datetime -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG -from scapy.cborpacket import CBOR_Packet ++ PR5125 refactor regressions (RFC + lifecycle) -class EventPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('event_type', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) += RFC map-key equivalence helper covers scalars, containers, tags, and NaNs +from scapy.cbor.cbor import _cbor_key_equivalent +from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_FLOAT, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_TEXT_STRING, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) +import math -pkt = EventPkt() -pkt.event_type.val = 'login' -pkt.ts.val = 9999999 -dec = cbor2.loads(bytes(pkt)) -isinstance(dec, dict) and dec.get('event_type') == 'login' and isinstance(dec.get('tag'), datetime.datetime) +assert _cbor_key_equivalent(0.0, -0.0) +assert not _cbor_key_equivalent(1, 1.0) +assert _cbor_key_equivalent([1, 2], [1, 2]) +assert not _cbor_key_equivalent([1, 2], [2, 1]) +assert _cbor_key_equivalent( + CBORMapData([(CBOR_TEXT_STRING("a"), 1), (CBOR_TEXT_STRING("b"), 2)]), + CBORMapData([(CBOR_TEXT_STRING("b"), 2), (CBOR_TEXT_STRING("a"), 1)]), +) +assert _cbor_key_equivalent( + CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), + CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), +) +assert not _cbor_key_equivalent( + CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), + CBOR_SEMANTIC_TAG((2, CBOR_UNSIGNED_INTEGER(5))), +) +# Case A: same quiet-NaN significand across half and binary64 widths +nan_half = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")) +nan_double_same = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000000000000") +) +assert math.isnan(nan_half.val) and math.isnan(nan_double_same.val) +assert _cbor_key_equivalent(nan_half, nan_double_same) +# Case B: different significand payloads are distinct keys +nan_payload = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000000000001") +) +assert not _cbor_key_equivalent(nan_half, nan_payload) +# Case C: opposite sign with the same payload +nan_neg = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f9fe00")) +assert not _cbor_key_equivalent(nan_half, nan_neg) +# Plain Python NaNs (best-effort binary64 identity) remain self-equivalent +assert _cbor_key_equivalent(float("nan"), float("nan")) +# Finite float width differences with the same numeric value are equivalent +assert _cbor_key_equivalent( + CBOR_FLOAT(1.5, encoded=bytes.fromhex("f93e00")), + CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")), +) + += Generic maps reject +0.0 and -0.0 as duplicate keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {+0.0: 1, -0.0: 2} half-floats — equivalent keys under RFC 8949 +wire = b"\xa2\xf9\x00\x00\x01\xf9\x80\x00\x02" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "+0.0 and -0.0 were accepted as distinct map keys" +except CBOR_Codec_Decoding_Error: + pass + += Generic maps keep integer 1 and float 1.0 as distinct keys +from scapy.cbor import CBOR_Codecs + +# {1: "i", 1.0: "f"} — half float 1.0 is f93c00 +wire = b"\xa2\x01\x61i\xf9\x3c\x00\x61f" +obj, rem = CBOR_Codecs.CBOR.dec(wire) +assert rem == b"" +md = obj.val +assert md[1].val == "i" +assert md[1.0].val == "f" + += Generic maps reject reordered equivalent maps used as map keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {{"a":1,"b":2}: 0, {"b":2,"a":1}: 1} — equivalent map keys +k1 = b"\xa2\x61a\x01\x61b\x02" +k2 = b"\xa2\x61b\x02\x61a\x01" +wire = b"\xa2" + k1 + b"\x00" + k2 + b"\x01" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "reordered map keys were treated as distinct" +except CBOR_Codec_Decoding_Error: + pass -= CBORF_SEMANTIC_TAG inside CBORF_MAP: Scapy roundtrip preserves inner value -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG -from scapy.cborpacket import CBOR_Packet += Generic maps reject duplicate semantic-tag keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error -class EventRTPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('event_type', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) +# {1(5): 0, 1(5): 1} +wire = b"\xa2\xc1\x05\x00\xc1\x05\x01" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "duplicate tagged keys were accepted" +except CBOR_Codec_Decoding_Error: + pass -pkt = EventRTPkt() -pkt.event_type.val = 'logout' -pkt.ts.val = 1234567890 -raw = bytes(pkt) -pkt2 = EventRTPkt(raw) -pkt2.event_type.val == 'logout' and pkt2.ts.val == 1234567890 += Generic maps keep distinct NaN payloads as separate keys +from scapy.cbor import CBOR_Codecs +import math -= CBORF_SEMANTIC_TAG inside CBORF_ARRAY: Scapy encode, cbor2 decode -import cbor2, datetime -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG -from scapy.cborpacket import CBOR_Packet +# {NaN16: 1, NaN64-low-payload: 2} — different significands +wire = bytes.fromhex("a2f97e0001fb7ff800000000000102") +obj, rem = CBOR_Codecs.CBOR.dec(wire) +assert rem == b"" +pairs = obj.val.cbor_pairs() +assert len(pairs) == 2 +assert all(math.isnan(key.val) for key, _ in pairs) +assert obj.enc() == wire + += Generic maps reject identical NaN keys as duplicates +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {NaN16: 1, same NaN as binary64 extension: 2} +wire = bytes.fromhex("a2f97e0001fb7ff800000000000002") +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "equivalent NaN keys were accepted" +except CBOR_Codec_Decoding_Error: + pass + += Generic maps keep opposite-sign NaNs with the same payload as distinct keys +from scapy.cbor import CBOR_Codecs + +# {+NaN16: 1, -NaN16: 2} — opposite signs are distinct keys +wire = bytes.fromhex("a2f97e0001f9fe0002") +obj, rem = CBOR_Codecs.CBOR.dec(wire) +assert rem == b"" +assert len(obj.val.cbor_pairs()) == 2 +assert obj.enc() == wire + += CBORMapData treats +0.0 and -0.0 as the same lookup key +from scapy.cbor import CBOR_Codecs + +# single-entry map keyed by +0.0 +wire = b"\xa1\xf9\x00\x00\x01" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +md = obj.val +assert md[0.0].val == 1 +assert md[-0.0].val == 1 +assert md[0.0] is md[-0.0] + += CBORMapData.as_dict converts distinct keys and rejects collisions +from scapy.cbor.cbor import ( + CBOR_BYTE_STRING, + CBOR_FALSE, + CBOR_FLOAT, + CBOR_TEXT_STRING, + CBOR_TRUE, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) + +md = CBORMapData([ + (CBOR_TEXT_STRING("a"), CBOR_UNSIGNED_INTEGER(10)), + (CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(20)), + (CBOR_BYTE_STRING(b"b"), CBOR_UNSIGNED_INTEGER(30)), + (CBOR_FALSE(), CBOR_UNSIGNED_INTEGER(40)), +]) +as_dict = md.as_dict() +assert as_dict["a"].val == 10 +assert as_dict[1].val == 20 +assert as_dict[b"b"].val == 30 +assert as_dict[False].val == 40 + +# RFC-equivalent keys (+0.0 / -0.0) cannot become a Python dict. +try: + CBORMapData([ + (CBOR_FLOAT(0.0), 1), + (CBOR_FLOAT(-0.0), 2), + ]).as_dict() + assert False, "equivalent float keys were collapsed into a dict" +except ValueError: + pass + +# Python dict key collision (True vs 1) is rejected even when CBOR norms differ. +try: + CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "i"), + (CBOR_TRUE(), "b"), + ]).as_dict() + assert False, "True/1 Python key collision was accepted by as_dict" +except ValueError: + pass + += CBORMapData equality is independent of pair order +from scapy.cbor.cbor import ( + CBOR_TRUE, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) + +a = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "a"), + (CBOR_UNSIGNED_INTEGER(2), "b"), +]) +b = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(2), "b"), + (CBOR_UNSIGNED_INTEGER(1), "a"), +]) +assert a == b +assert b == a +assert a != CBORMapData([(CBOR_UNSIGNED_INTEGER(1), "a")]) +assert a != CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "a"), + (CBOR_UNSIGNED_INTEGER(2), "c"), +]) +# Integer 1 and Boolean true remain distinct CBOR keys. +typed = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "i"), + (CBOR_TRUE(), "b"), +]) +assert typed == CBORMapData([ + (CBOR_TRUE(), "b"), + (CBOR_UNSIGNED_INTEGER(1), "i"), +]) +assert typed != CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "i"), + (CBOR_UNSIGNED_INTEGER(1), "b"), +]) + + += CBOR_MAP normalizes constructors to CBORMapData and equates list-of-pairs keys +from scapy.cbor.cbor import ( + CBOR_MAP, + CBOR_TEXT_STRING, + CBOR_UNSIGNED_INTEGER, + CBORMapData, + _cbor_key_equivalent, +) +from scapy.cbor.cborcodec import CBOR_Codec_Encoding_Error, CBORcodec_MAP + +empty = CBOR_MAP({}) +assert isinstance(empty.val, CBORMapData) +assert empty.val == {} + +a = CBOR_TEXT_STRING("a") +b = CBOR_TEXT_STRING("b") +one = CBOR_UNSIGNED_INTEGER(1) +two = CBOR_UNSIGNED_INTEGER(2) +k1 = CBOR_MAP([(a, one), (b, two)]) +k2 = CBOR_MAP([(b, two), (a, one)]) +k3 = CBOR_MAP({"a": 1, "b": 2}) +k4 = CBOR_MAP(CBORMapData([(a, one), (b, two)])) +assert isinstance(k1.val, CBORMapData) +assert _cbor_key_equivalent(k1, k2) +assert _cbor_key_equivalent(k1, k4) +# Native dict keys normalize to the same text/int norms as CBOR objects. +assert _cbor_key_equivalent(k1, k3) + +# Encode must reject reorder-equivalent map-valued keys. +dup = CBORMapData([(k1, 0), (k2, 1)]) +try: + CBORcodec_MAP.enc(dup) + assert False, "encode accepted reorder-equivalent map-valued keys" +except CBOR_Codec_Encoding_Error: + pass -class TimedEventArr(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('evt', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) -pkt = TimedEventArr() -pkt.evt.val = 'start' -pkt.ts.val = 1700000000 -dec = cbor2.loads(bytes(pkt)) -dec[0] == 'start' and isinstance(dec[1], datetime.datetime) += CBORMapData.as_dict rejects unhashable array and map keys +from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_MAP, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) -+ CBOR_Packet - realistic models +try: + CBORMapData([ + (CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1)]), CBOR_UNSIGNED_INTEGER(0)), + ]).as_dict() + assert False, "array map key did not raise ValueError" +except ValueError as exc: + assert "cannot be represented as a Python dict key" in str(exc) -= Realistic model: EAT-like attestation token -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING +try: + CBORMapData([ + ( + CBOR_MAP(CBORMapData([(CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(2))])), + CBOR_UNSIGNED_INTEGER(0), + ), + ]).as_dict() + assert False, "map map key did not raise ValueError" +except ValueError as exc: + assert "cannot be represented as a Python dict key" in str(exc) + += Deterministic encoding rebuilds floats from the semantic value +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborcodec import CBORcodec_Object + +# 1.5 received as binary64 must still encode as preferred binary16 +value = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +assert CBORcodec_Object.encode_cbor_item_deterministic(value) == bytes.fromhex("f93e00") + += Deterministic NaN encoding preserves sign and preferred width +from scapy.cbor.cbor import CBOR_FLOAT, _cbor_key_equivalent +from scapy.cbor.cborcodec import CBORcodec_Object +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic + +enc = CBORcodec_Object.encode_cbor_item_deterministic + +# Already-shortest quiet half NaN stays half +half = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")) +assert enc(half) == bytes.fromhex("f97e00") +assert cbor_find_non_deterministic(enc(half)) == [] + +# binary64 quiet NaN with only top significand bits shortens to half +wide_half = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("fb7ff8000000000000")) +assert enc(wide_half) == bytes.fromhex("f97e00") +assert _cbor_key_equivalent(half, wide_half) +assert cbor_find_non_deterministic(enc(wide_half)) == [] + +# binary64 NaN using top 23 significand bits shortens to float32, not half +# significand bits 51..29 set as 0x40000001 << 29? Use mant with bit 29 set: +# 0x8000000000000 | 0x20000000 = top quiet bit + lowest single-preserved bit +to_single = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000020000000") +) +assert enc(to_single) == bytes.fromhex("fa7fc00001") +assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000020000000")) +assert cbor_find_non_deterministic(enc(to_single)) == [] + +# binary64 NaN with a low significand bit cannot shorten +irreducible = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000000000001") +) +assert enc(irreducible) == bytes.fromhex("fb7ff8000000000001") +assert cbor_find_non_deterministic(enc(irreducible)) == [] + +# Negative quiet half NaN keeps its sign under deterministic encoding +neg = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("fbfff8000000000000")) +assert enc(neg) == bytes.fromhex("f9fe00") +assert cbor_find_non_deterministic(enc(neg)) == [] +assert not _cbor_key_equivalent(half, neg) + += Unknown map NaN values keep payload through deterministic rebuild +from scapy.cbor.cbor import CBOR_FLOAT, _cbor_key_equivalent +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class EATToken(CBOR_Packet): +class MapWithUnknown(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('nonce', 0), - CBORF_TEXT_STRING('ueid', ''), - CBORF_BYTE_STRING('boot_seed', b''), - CBORF_INTEGER('hwver', 0), + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", ) -raw = cbor2.dumps({'nonce': 12345, 'ueid': 'device-abc', 'boot_seed': b'\x00' * 16, 'hwver': 3}) -pkt = EATToken(raw) -pkt.nonce.val == 12345 and pkt.ueid.val == 'device-abc' and pkt.boot_seed.val == b'\x00' * 16 and pkt.hwver.val == 3 +# {"a": 1, "n": } +wire = bytes.fromhex("a2616101616efb7ff8000000000000") +pkt = MapWithUnknown(wire) +assert pkt.a == 1 +assert len(pkt.unknown_pairs) == 1 +assert isinstance(pkt.unknown_pairs[0][1], CBOR_FLOAT) +pkt.a = 2 # invalidate raw cache; unknown members rebuild deterministically +rebuilt = bytes(pkt) +assert b"\x61a\x02" in rebuilt +# preferred half NaN with the same identity +assert bytes.fromhex("f97e00") in rebuilt +assert _cbor_key_equivalent( + pkt.unknown_pairs[0][1], + CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")), +) + += Compound CBOR_Object values are unhashable +from scapy.cbor.cbor import CBOR_ARRAY, CBOR_UNSIGNED_INTEGER + +arr = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1)]) +try: + hash(arr) + assert False, "mutable/compound CBOR_Object remained hashable" +except TypeError: + pass -= Realistic model: EAT-like token Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING += Schema-fixed CBORF_SEMANTIC_TAG does not expose an editable tag field +from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_INTEGER from scapy.cborpacket import CBOR_Packet -class EATToken2(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('nonce', 0), - CBORF_TEXT_STRING('ueid', ''), - CBORF_BYTE_STRING('boot_seed', b''), - CBORF_INTEGER('hwver', 0), - ) - -pkt = EATToken2() -pkt.nonce.val = 99999 -pkt.ueid.val = 'iot-sensor-01' -pkt.boot_seed.val = b'\xde\xad\xbe\xef' * 4 -pkt.hwver.val = 5 -dec = cbor2.loads(bytes(pkt)) -dec.get('nonce') == 99999 and dec.get('ueid') == 'iot-sensor-01' and dec.get('boot_seed') == b'\xde\xad\xbe\xef' * 4 and dec.get('hwver') == 5 - -= Realistic model: SensorReport with CBORF_PACKET inner reading -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_FLOAT, CBORF_PACKET +class TaggedTs(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG(1, CBORF_INTEGER("ts", 0)) + +assert "tag_number" not in [f.name for f in TaggedTs.fields_desc] +assert "ts" in [f.name for f in TaggedTs.fields_desc] +pkt = TaggedTs(b"\xc1\x0a") +assert pkt.ts == 10 +assert bytes(pkt) == b"\xc1\x0a" +# Only the inner value is packet state; mutating ts must change the wire. +pkt.ts = 11 +assert bytes(pkt) == b"\xc1\x0b" + += Two independent CBORF_MAP fields keep separate unknown extensions after mutation +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class SensorData(CBOR_Packet): +class TwoMaps(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('sensor_id', 0), - CBORF_FLOAT('temperature', 0.0), + CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_a", + ), + CBORF_MAP( + CBORF_UNSIGNED_INTEGER("b", 0), + unknown_field="unknown_b", + ), ) -class SensorReport(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('station', 0), - CBORF_TEXT_STRING('unit', ''), - CBORF_PACKET('reading', None, SensorData), - ) - -reading = SensorData() -reading.sensor_id.val = 3 -reading.temperature.val = 98.6 -rpt = SensorReport() -rpt.station.val = 5 -rpt.unit.val = 'fahrenheit' -rpt.reading = reading -dec = cbor2.loads(bytes(rpt)) -dec.get('station') == 5 and dec.get('unit') == 'fahrenheit' and dec.get('reading') == [3, 98.6] - -= Realistic model: SensorReport Scapy roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_FLOAT, CBORF_PACKET +# [{"a":1,"x":10},{"b":2,"y":20}] +wire = ( + b"\x82" + b"\xa2\x61a\x01\x61x\x0a" + b"\xa2\x61b\x02\x61y\x14" +) +pkt = TwoMaps(wire) +assert pkt.a == 1 and pkt.b == 2 +assert bytes(pkt) == wire +pkt.a = 3 +# Each map must retain its own unknown: map0 keeps x, map1 keeps y. +# Deterministic rebuild sorts keys inside each map. +assert bytes(pkt) == ( + b"\x82" + b"\xa2\x61a\x03\x61x\x0a" + b"\xa2\x61b\x02\x61y\x14" +) + += Nested CBORF_MAP unknown extensions survive outer mutation +from scapy.cbor.cborfields import ( + CBORF_MAP, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, +) from scapy.cborpacket import CBOR_Packet -class SensorData2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('sensor_id', 0), - CBORF_FLOAT('temperature', 0.0), - ) - -class SensorReport2(CBOR_Packet): +class InnerMapPkt(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('station', 0), - CBORF_TEXT_STRING('unit', ''), - CBORF_PACKET('reading', None, SensorData2), + CBORF_UNSIGNED_INTEGER("inner", 0), ) -raw = cbor2.dumps({'station': 9, 'unit': 'celsius', 'reading': [7, 36.5]}) -pkt = SensorReport2(raw) -pkt2 = SensorReport2(bytes(pkt)) -pkt2.station.val == 9 and pkt2.unit.val == 'celsius' and pkt2.reading.sensor_id.val == 7 - -= Realistic model: StatusList (CBORF_ARRAY_OF of CBOR_Packets) encode and decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF -from scapy.cborpacket import CBOR_Packet - -class HttpStatus(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('phrase', ''), - ) - -class HttpStatusList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('statuses', [], HttpStatus) - -raw = cbor2.dumps([[200, 'OK'], [201, 'Created'], [404, 'Not Found']]) -pkt = HttpStatusList(raw) -raw2 = bytes(pkt) -dec = cbor2.loads(raw2) -dec == [[200, 'OK'], [201, 'Created'], [404, 'Not Found']] - -= Realistic model: HTTP response header map -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet - -class HttpResponse(CBOR_Packet): +class OuterMapPkt(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('status', 0), - CBORF_TEXT_STRING('content_type', ''), - CBORF_INTEGER('content_length', 0), - CBORF_BYTE_STRING('body', b''), + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_PACKET("child", None, InnerMapPkt), ) -pkt = HttpResponse() -pkt.status.val = 200 -pkt.content_type.val = 'application/cbor' -pkt.content_length.val = 4 -pkt.body.val = b'\x01\x02\x03\x04' -dec = cbor2.loads(bytes(pkt)) -dec.get('status') == 200 and dec.get('content_type') == 'application/cbor' and dec.get('body') == b'\x01\x02\x03\x04' - -= Realistic model: HTTP response header cbor2 → Scapy roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING +# {"child":{"inner":2,"u":9},"n":1,"x":5} +wire = ( + b"\xa3" + b"\x65child\xa2\x65inner\x02\x61u\x09" + b"\x61n\x01" + b"\x61x\x05" +) +pkt = OuterMapPkt(wire) +assert pkt.n == 1 +assert pkt.child.inner == 2 +pkt.n = 4 +built = bytes(pkt) +# Outer unknowns (x) and nested unknowns (u) must both survive. +assert b"\x61x\x05" in built +assert b"\x61u\x09" in built +assert pkt.child.inner == 2 + + ++ CBOR_Packet standard Scapy lifecycle + += Unknown map extensions survive Packet.__iter__ and standard do_build +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class HttpResponse2(CBOR_Packet): +class LifeMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('status', 0), - CBORF_TEXT_STRING('content_type', ''), - CBORF_INTEGER('content_length', 0), - CBORF_BYTE_STRING('body', b''), + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", ) -raw = cbor2.dumps({'status': 404, 'content_type': 'text/plain', 'content_length': 9, 'body': b'Not Found'}) -pkt = HttpResponse2(raw) -pkt2 = HttpResponse2(bytes(pkt)) -pkt2.status.val == 404 and pkt2.content_type.val == 'text/plain' and pkt2.body.val == b'Not Found' - -= Realistic model: COSE-like header map with integer algorithm -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_BYTE_STRING, CBORF_TEXT_STRING +wire = b"\xa2\x61a\x01\x61x\x0a" +pkt = LifeMap(wire) +pkt.a = 2 # clears explicit / raw cache +assert bytes(pkt) == b"\xa2\x61a\x02\x61x\x0a" +iterated = next(iter(pkt)) +assert bytes(iterated) == b"\xa2\x61a\x02\x61x\x0a" +assert iterated.unknown_pairs[0][0] == "x" + += fuzz() and show() accept CBOR packets with unknown map members +from scapy.packet import fuzz +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class CoseHeader(CBOR_Packet): +class LifeMap2(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('alg', 0), - CBORF_TEXT_STRING('kid', ''), - CBORF_BYTE_STRING('x5t', b''), + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", ) -pkt = CoseHeader(cbor2.dumps({'alg': -7, 'kid': 'key-42', 'x5t': b'\xaa\xbb\xcc\xdd'})) -dec = cbor2.loads(bytes(pkt)) -dec.get('alg') == -7 and dec.get('kid') == 'key-42' and dec.get('x5t') == b'\xaa\xbb\xcc\xdd' - -= Realistic model: CBOR_Packet fields_desc populated for complex structures -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_FLOAT +wire = b"\xa2\x61a\x01\x61x\x0a" +fuzzed = fuzz(LifeMap2()) +assert isinstance(fuzzed, LifeMap2) +bytes(fuzzed) +fuzzed.show(dump=True) +fuzzed.show2(dump=True) + +parsed = LifeMap2(wire) +fuzzed = fuzz(parsed) +assert isinstance(fuzzed, LifeMap2) +bytes(fuzzed) +fuzzed.show(dump=True) +fuzzed.show2(dump=True) + += copy and deepcopy re-parent nested CBOR packets +import copy +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class FullRecord(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('seq', 0), - CBORF_TEXT_STRING('source', ''), - CBORF_FLOAT('score', 0.0), - CBORF_BYTE_STRING('checksum', b''), - ) - -field_names = [f.name for f in FullRecord.fields_desc] -'seq' in field_names and 'source' in field_names and 'score' in field_names and 'checksum' in field_names - -= Realistic model: multi-field packet encoding is byte-for-byte reproducible -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING, CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet - -class MeasurementPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('seq', 0), - CBORF_TEXT_STRING('sensor', ''), - CBORF_FLOAT('value', 0.0), - CBORF_BYTE_STRING('raw', b''), - ) - -pkt = MeasurementPkt() -pkt.seq.val = 42 -pkt.sensor.val = 'temp-01' -pkt.value.val = 23.5 -pkt.raw.val = b'\x01\x02' -raw1 = bytes(pkt) -raw2 = bytes(MeasurementPkt(raw1)) -raw1 == raw2 - -########### CBOR Fuzzing / Random Object Tests #################### - -+ CBOR Random Object Generation - -= Create RandCBORObject -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -isinstance(rand, RandCBORObject) - -= Generate random CBOR unsigned integer -from scapy.cbor import RandCBORObject, CBOR_UNSIGNED_INTEGER -rand = RandCBORObject(objlist=[CBOR_UNSIGNED_INTEGER]) -obj = rand._fix() -isinstance(obj, CBOR_UNSIGNED_INTEGER) and isinstance(obj.val, int) and obj.val >= 0 - -= Generate random CBOR negative integer -from scapy.cbor import RandCBORObject, CBOR_NEGATIVE_INTEGER -rand = RandCBORObject(objlist=[CBOR_NEGATIVE_INTEGER]) -obj = rand._fix() -isinstance(obj, CBOR_NEGATIVE_INTEGER) and isinstance(obj.val, int) and obj.val < 0 - -= Generate random CBOR byte string -from scapy.cbor import RandCBORObject, CBOR_BYTE_STRING -rand = RandCBORObject(objlist=[CBOR_BYTE_STRING]) -obj = rand._fix() -isinstance(obj, CBOR_BYTE_STRING) and isinstance(obj.val, bytes) - -= Generate random CBOR text string -from scapy.cbor import RandCBORObject, CBOR_TEXT_STRING -rand = RandCBORObject(objlist=[CBOR_TEXT_STRING]) -obj = rand._fix() -isinstance(obj, CBOR_TEXT_STRING) and isinstance(obj.val, str) and len(obj.val) > 0 - -= Generate random CBOR array -from scapy.cbor import RandCBORObject, CBOR_ARRAY -rand = RandCBORObject(objlist=[CBOR_ARRAY]) -obj = rand._fix() -isinstance(obj, CBOR_ARRAY) and isinstance(obj.val, list) - -= Generate random CBOR map -from scapy.cbor import RandCBORObject, CBOR_MAP -rand = RandCBORObject(objlist=[CBOR_MAP]) -obj = rand._fix() -isinstance(obj, CBOR_MAP) and isinstance(obj.val, dict) - -= Generate random CBOR boolean (false) -from scapy.cbor import RandCBORObject, CBOR_FALSE -rand = RandCBORObject(objlist=[CBOR_FALSE]) -obj = rand._fix() -isinstance(obj, CBOR_FALSE) and obj.val == False - -= Generate random CBOR boolean (true) -from scapy.cbor import RandCBORObject, CBOR_TRUE -rand = RandCBORObject(objlist=[CBOR_TRUE]) -obj = rand._fix() -isinstance(obj, CBOR_TRUE) and obj.val == True - -= Generate random CBOR null -from scapy.cbor import RandCBORObject, CBOR_NULL -rand = RandCBORObject(objlist=[CBOR_NULL]) -obj = rand._fix() -isinstance(obj, CBOR_NULL) and obj.val is None - -= Generate random CBOR undefined -from scapy.cbor import RandCBORObject, CBOR_UNDEFINED -rand = RandCBORObject(objlist=[CBOR_UNDEFINED]) -obj = rand._fix() -isinstance(obj, CBOR_UNDEFINED) and obj.val is None - -= Generate random CBOR float -from scapy.cbor import RandCBORObject, CBOR_FLOAT -rand = RandCBORObject(objlist=[CBOR_FLOAT]) -obj = rand._fix() -isinstance(obj, CBOR_FLOAT) and isinstance(obj.val, float) - -+ CBOR Random Object Encoding/Decoding - -= Encode and decode random unsigned integer -from scapy.cbor import RandCBORObject, CBOR_UNSIGNED_INTEGER, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_UNSIGNED_INTEGER]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_UNSIGNED_INTEGER) and remainder == b'' and decoded.val == obj.val - -= Encode and decode random text string -from scapy.cbor import RandCBORObject, CBOR_TEXT_STRING, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_TEXT_STRING]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_TEXT_STRING) and remainder == b'' and decoded.val == obj.val - -= Encode and decode random byte string -from scapy.cbor import RandCBORObject, CBOR_BYTE_STRING, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_BYTE_STRING]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_BYTE_STRING) and remainder == b'' and decoded.val == obj.val - -= Encode and decode random array -from scapy.cbor import RandCBORObject, CBOR_ARRAY, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_ARRAY]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_ARRAY) and remainder == b'' and len(decoded.val) == len(obj.val) - -= Encode and decode random map -from scapy.cbor import RandCBORObject, CBOR_MAP, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_MAP]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_MAP) and remainder == b'' and len(decoded.val) == len(obj.val) - -= Encode and decode random float -from scapy.cbor import RandCBORObject, CBOR_FLOAT, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_FLOAT]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_FLOAT) and remainder == b'' - -+ CBOR Random Mixed Types - -= Generate multiple random objects of different types -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -objects = [rand._fix() for _ in range(10)] -len(objects) == 10 and all(hasattr(obj, 'val') for obj in objects) - -= Encode and decode multiple random objects -from scapy.cbor import RandCBORObject, CBOR_Codecs -rand = RandCBORObject() -success_count = 0 -for _ in range(20): - obj = rand._fix() - try: - encoded = bytes(obj) - decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) - if remainder == b'': - success_count += 1 - except: - pass - -success_count >= 18 - -= Random nested arrays encode/decode correctly -from scapy.cbor import RandCBORObject, CBOR_ARRAY, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_ARRAY]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_ARRAY) and remainder == b'' - -= Random nested maps encode/decode correctly -from scapy.cbor import RandCBORObject, CBOR_MAP, CBOR_Codecs -rand = RandCBORObject(objlist=[CBOR_MAP]) -obj = rand._fix() -encoded = bytes(obj) -decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) -isinstance(decoded, CBOR_MAP) and remainder == b'' - -+ CBOR Fuzzing Stress Tests - -= Generate 100 random objects without errors -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -objects = [] -for _ in range(100): - obj = None - try: - obj = rand._fix() - except: - pass - if obj is not None: - objects.append(obj) - -len(objects) >= 95 +class LifeChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -= Encode 50 random objects without errors -from scapy.cbor import RandCBORObject -rand = RandCBORObject() -encoded_count = 0 -for _ in range(50): - obj = rand._fix() - try: - encoded = bytes(obj) - if len(encoded) > 0: - encoded_count += 1 - except: - pass - -encoded_count >= 45 - -= Roundtrip 50 random objects -from scapy.cbor import RandCBORObject, CBOR_Codecs -rand = RandCBORObject() -roundtrip_count = 0 -for _ in range(50): - obj = rand._fix() - try: - encoded = bytes(obj) - decoded, remainder = CBOR_Codecs.CBOR.dec(encoded) - if remainder == b'': - roundtrip_count += 1 - except: - pass +class LifeParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, LifeChild) -roundtrip_count >= 45 +pkt = LifeParent(child=LifeChild(n=1)) +cloned = pkt.copy() +deep = copy.deepcopy(pkt) +assert cloned.child.parent is cloned +assert deep.child.parent is deep +assert cloned.child.n == 1 and deep.child.n == 1 diff --git a/test/scapy/layers/cbor_cbor2_interop.uts b/test/scapy/layers/cbor_cbor2_interop.uts new file mode 100644 index 00000000000..8e87cb443f9 --- /dev/null +++ b/test/scapy/layers/cbor_cbor2_interop.uts @@ -0,0 +1,1456 @@ +% CBOR interoperability and differential tests using cbor2 6.1.4 + ++ Shared cbor2 oracle helpers + += Import cbor2 and define differential-test helpers ~ external_cbor2 +import io +import math +import random +import re +import struct +from collections.abc import Mapping +from datetime import date, datetime, timezone +from decimal import Decimal +from email.mime.text import MIMEText +from fractions import Fraction +from importlib.metadata import version as distribution_version +from ipaddress import ( + IPv4Address, + IPv4Interface, + IPv4Network, + IPv6Address, + IPv6Interface, + IPv6Network, +) +from uuid import UUID + +import cbor2 + +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cbor import ( + CBOR_DECODING_ERROR, + CBOR_Decoding_Error, + CBORMapData, + CBOR_ARRAY, + CBOR_BYTE_STRING, + CBOR_FALSE, + CBOR_FLOAT, + CBOR_MAP, + CBOR_NEGATIVE_INTEGER, + CBOR_NULL, + CBOR_Object, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_TEXT_STRING, + CBOR_TRUE, + CBOR_UNDEFINED, + CBOR_UNSIGNED_INTEGER, +) +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + CBORcodec_Object, + MAX_CBOR_NESTING, +) +from scapy.cbor.cborfields import ( + CBOR_ABSENT, + CBORF_ANY, + CBORF_ARRAY, + CBORF_ARRAY_OF, + CBORF_BOOLEAN, + CBORF_BYTE_STRING, + CBORF_FLOAT, + CBORF_NEGATIVE_INTEGER, + CBORF_NULL, + CBORF_SEMANTIC_TAG, + CBORF_TEXT_STRING, + CBORF_UNDEFINED, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +_RR_CBOR2_VERSION = distribution_version("cbor2") +_RR_SCAPY_DECODE_ERRORS = (CBOR_Decoding_Error, CBOR_Codec_Decoding_Error) + + +def _rr_float(value): + value = float(value) + if math.isnan(value): + return ("float", "nan") + if math.isinf(value): + return ("float", "+inf" if value > 0 else "-inf") + if value == 0.0: + return ("float", "-0" if math.copysign(1.0, value) < 0 else "+0") + return ("float", struct.pack(">d", value).hex()) + + +def _rr_map(pairs, norm): + normalized = [(norm(key), norm(value)) for key, value in pairs] + return ("map", tuple(sorted(normalized, key=repr))) + + +def rr_norm_scapy(obj): + if isinstance(obj, CBOR_FALSE): + return ("bool", False) + if isinstance(obj, CBOR_TRUE): + return ("bool", True) + if isinstance(obj, CBOR_NULL): + return ("null",) + if isinstance(obj, CBOR_UNDEFINED): + return ("undefined",) + if isinstance(obj, CBOR_UNSIGNED_INTEGER): + return ("uint", obj.val) + if isinstance(obj, CBOR_NEGATIVE_INTEGER): + return ("nint", obj.val) + if isinstance(obj, CBOR_BYTE_STRING): + return ("bytes", obj.val) + if isinstance(obj, CBOR_TEXT_STRING): + return ("text", obj.val) + if isinstance(obj, CBOR_FLOAT): + return _rr_float(obj.val) + if isinstance(obj, CBOR_SIMPLE_VALUE): + return ("simple", obj.val) + if isinstance(obj, CBOR_ARRAY): + return ("array", tuple(rr_norm_scapy(item) for item in obj.val)) + if isinstance(obj, CBOR_MAP): + if isinstance(obj.val, CBORMapData): + pairs = obj.val.cbor_pairs() + elif isinstance(obj.val, Mapping): + pairs = list(obj.val.items()) + else: + pairs = list(obj.val) + return _rr_map(pairs, rr_norm_scapy) + if isinstance(obj, CBOR_SEMANTIC_TAG): + tag, value = obj.val + return ("tag", tag, rr_norm_scapy(value)) + if isinstance(obj, CBOR_Object): + return ("scapy-object", type(obj).__name__, repr(obj.val)) + return rr_norm_native(obj) + + +def rr_norm_cbor2(value): + if value is cbor2.undefined: + return ("undefined",) + if isinstance(value, cbor2.CBORSimpleValue): + return ("simple", value.value) + if isinstance(value, cbor2.CBORTag): + return ("tag", value.tag, rr_norm_cbor2(value.value)) + if isinstance(value, bool): + return ("bool", value) + if value is None: + return ("null",) + if isinstance(value, int): + return ("uint" if value >= 0 else "nint", value) + if isinstance(value, float): + return _rr_float(value) + if isinstance(value, bytes): + return ("bytes", value) + if isinstance(value, str): + return ("text", value) + if isinstance(value, Mapping): + return _rr_map(list(value.items()), rr_norm_cbor2) + if isinstance(value, (list, tuple)): + return ("array", tuple(rr_norm_cbor2(item) for item in value)) + return ("python", type(value).__module__, type(value).__qualname__, repr(value)) + + +def rr_norm_native(value): + if isinstance(value, CBOR_UNDEFINED): + return ("undefined",) + if isinstance(value, CBOR_SIMPLE_VALUE): + return ("simple", value.val) + if isinstance(value, CBOR_SEMANTIC_TAG): + return ("tag", value.val[0], rr_norm_native(value.val[1])) + if isinstance(value, CBORMapData): + return _rr_map(value.cbor_pairs(), rr_norm_native) + if isinstance(value, bool): + return ("bool", value) + if value is None: + return ("null",) + if isinstance(value, int): + return ("uint" if value >= 0 else "nint", value) + if isinstance(value, float): + return _rr_float(value) + if isinstance(value, bytes): + return ("bytes", value) + if isinstance(value, str): + return ("text", value) + if isinstance(value, Mapping): + return _rr_map(list(value.items()), rr_norm_native) + if isinstance(value, (list, tuple)): + return ("array", tuple(rr_norm_native(item) for item in value)) + if isinstance(value, CBOR_Object): + return rr_norm_scapy(value) + return ("python", type(value).__module__, type(value).__qualname__, repr(value)) + + +def rr_cbor2_load(wire, **kwargs): + kwargs.setdefault("immutable", True) + return cbor2.loads(wire, **kwargs) + + +def rr_scapy_decode(wire): + obj, remainder = CBOR_Codecs.CBOR.dec(wire) + assert remainder == b"", (wire.hex(), remainder.hex()) + return obj + + +def rr_assert_cbor2_value(value, *, canonical=False, + indefinite_containers=False, exact=False): + wire = cbor2.dumps( + value, + canonical=canonical, + indefinite_containers=indefinite_containers, + ) + expected = rr_norm_cbor2(rr_cbor2_load(wire)) + obj = rr_scapy_decode(wire) + actual = rr_norm_scapy(obj) + assert actual == expected, (wire.hex(), expected, actual) + rebuilt = obj.enc() + if exact: + assert rebuilt == wire, (wire.hex(), rebuilt.hex()) + assert rr_norm_cbor2(rr_cbor2_load(rebuilt)) == expected + return wire, obj + + +def rr_assert_extension_wire(value, **dump_kwargs): + wire = cbor2.dumps(value, **dump_kwargs) + obj = rr_scapy_decode(wire) + rebuilt = obj.enc() + assert rebuilt == wire, (wire.hex(), rebuilt.hex()) + expected = cbor2.loads(wire) + actual = cbor2.loads(rebuilt) + if isinstance(expected, float) and math.isnan(expected): + assert isinstance(actual, float) and math.isnan(actual) + else: + assert actual == expected + return wire, obj + + +def rr_to_scapy_native(value): + if value is cbor2.undefined: + return CBOR_UNDEFINED() + if isinstance(value, cbor2.CBORSimpleValue): + return CBOR_SIMPLE_VALUE(value.value) + if isinstance(value, cbor2.CBORTag): + return CBOR_SEMANTIC_TAG( + (value.tag, rr_to_scapy_native(value.value)) + ) + if isinstance(value, Mapping): + return { + rr_to_scapy_native(key): rr_to_scapy_native(item) + for key, item in value.items() + } + if isinstance(value, (list, tuple)): + return [rr_to_scapy_native(item) for item in value] + return value + + +def rr_assert_scapy_native(value): + native = rr_to_scapy_native(value) + wire = CBORcodec_Object.encode_cbor_item(native) + expected = rr_norm_native(native) + actual = rr_norm_cbor2(rr_cbor2_load(wire)) + assert actual == expected, (wire.hex(), expected, actual) + return wire + + +def rr_clear_cache(pkt): + pkt.raw_packet_cache = None + pkt.raw_packet_cache_fields = None + pkt.wirelen = None + + +def rr_scapy_reject(wire): + try: + CBOR_Codecs.CBOR.dec(wire) + except _RR_SCAPY_DECODE_ERRORS: + return + raise AssertionError("Scapy accepted malformed CBOR: %s" % wire.hex()) + + +def rr_cbor2_reject(wire, **kwargs): + try: + cbor2.loads(wire, **kwargs) + except cbor2.CBORDecodeError: + return + raise AssertionError("cbor2 accepted malformed CBOR: %s" % wire.hex()) + + +def rr_both_reject(wire, **cbor2_kwargs): + rr_cbor2_reject(wire, **cbor2_kwargs) + rr_scapy_reject(wire) + + +def rr_scapy_sequence(wire): + values = [] + remainder = wire + while remainder: + before = len(remainder) + obj, remainder = CBOR_Codecs.CBOR.dec(remainder) + assert len(remainder) < before + values.append(rr_norm_scapy(obj)) + return values + + +def rr_cbor2_sequence(wire, count): + stream = io.BytesIO(wire) + decoder = cbor2.CBORDecoder(stream) + return [rr_norm_cbor2(decoder.decode(immutable=True)) for _ in range(count)] + + +def rr_random_key(rng): + kind = rng.randrange(3) + if kind == 0: + return rng.randint(-100000, 100000) + if kind == 1: + return bytes(rng.randrange(256) for _ in range(rng.randrange(0, 8))) + alphabet = "abcXYZ012-_ä" + return "".join(rng.choice(alphabet) for _ in range(rng.randrange(0, 8))) + + +def rr_random_value(rng, depth=0, include_float=True): + scalar_kinds = ["uint", "nint", "bytes", "text", "bool", "null", + "undefined", "simple", "tag"] + if include_float: + scalar_kinds.append("float") + kinds = list(scalar_kinds) + if depth < 4: + kinds.extend(["array", "map"]) + kind = rng.choice(kinds) + if kind == "uint": + return rng.randrange(0, 1 << rng.choice((4, 8, 16, 32, 64))) + if kind == "nint": + return -1 - rng.randrange(0, 1 << rng.choice((4, 8, 16, 32, 63))) + if kind == "bytes": + return bytes(rng.randrange(256) for _ in range(rng.randrange(0, 32))) + if kind == "text": + alphabet = "abcXYZ012-_ä€𐍈\x00" + return "".join(rng.choice(alphabet) for _ in range(rng.randrange(0, 24))) + if kind == "bool": + return bool(rng.getrandbits(1)) + if kind == "null": + return None + if kind == "undefined": + return cbor2.undefined + if kind == "simple": + return cbor2.CBORSimpleValue(rng.choice((0, 1, 16, 19, 32, 64, 127, 255))) + if kind == "float": + special = rng.randrange(12) + if special == 0: + return -0.0 + if special == 1: + return float("inf") + if special == 2: + return float("-inf") + if special == 3: + return float("nan") + return rng.uniform(-1.0e12, 1.0e12) + if kind == "tag": + return cbor2.CBORTag( + 60000 + rng.randrange(1000), + rr_random_value(rng, depth + 1, include_float=include_float), + ) + if kind == "array": + return [ + rr_random_value(rng, depth + 1, include_float=include_float) + for _ in range(rng.randrange(0, 6)) + ] + mapping = {} + target = rng.randrange(0, 6) + while len(mapping) < target: + mapping[rr_random_key(rng)] = rr_random_value( + rng, depth + 1, include_float=include_float + ) + return mapping + + +class RRCbor2AnyRoot(CBOR_Packet): + CBOR_root = CBORF_ANY("value", CBOR_ABSENT) + + +class RRCbor2AnyEnvelope(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", CBOR_ABSENT), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + + +class RRCbor2UInt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + + +class RRCbor2NInt(CBOR_Packet): + CBOR_root = CBORF_NEGATIVE_INTEGER("value", -1) + + +class RRCbor2Bytes(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("value", b"") + + +class RRCbor2DefiniteBytes(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("value", b"", definite_only=True) + + +class RRCbor2Text(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING("value", "") + + +class RRCbor2Bool(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN("value", False) + + +class RRCbor2Null(CBOR_Packet): + CBOR_root = CBORF_NULL("value") + + +class RRCbor2Undefined(CBOR_Packet): + CBOR_root = CBORF_UNDEFINED("value") + + +class RRCbor2Float(CBOR_Packet): + CBOR_root = CBORF_FLOAT("value", 0.0) + + +class RRCbor2UIntArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "values", [], CBORF_UNSIGNED_INTEGER, max_count=4096 + ) + + +class RRCbor2TaggedText(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG(60000, CBORF_TEXT_STRING("value", "")) + ++ Oracle version and API assumptions + += The differential suite is pinned to cbor2 6.1.4 ~ external_cbor2 +assert _RR_CBOR2_VERSION == "6.1.4", _RR_CBOR2_VERSION + += cbor2 exposes canonical and indefinite-container encoders ~ external_cbor2 +canonical = cbor2.dumps({"long": 1, "x": 2}, canonical=True) +indefinite = cbor2.dumps([1, 2], indefinite_containers=True) +assert cbor2.loads(canonical) == {"long": 1, "x": 2} +assert cbor2.loads(indefinite) == [1, 2] +assert indefinite[0] == 0x9f and indefinite[-1] == 0xff + += cbor2 strict decoder options provide independent negative controls ~ external_cbor2 +wire = cbor2.dumps([1, 2], indefinite_containers=True) +rr_cbor2_reject(wire, allow_indefinite=False) +duplicate = b"\xa2\x01\x00\x01\x01" +rr_cbor2_reject(duplicate, allow_duplicate_keys=False) + ++ Integer boundary vectors generated by cbor2 + += Unsigned integer boundaries decode and re-encode exactly ~ external_cbor2 +values = [0, 1, 10, 23, 24, 25, 255, 256, 65535, 65536, + (1 << 32) - 1, 1 << 32, (1 << 64) - 1] + +for value in values: + rr_assert_cbor2_value(value, canonical=True, exact=True) + += Unsigned integer additional-information transitions match cbor2 ~ external_cbor2 +expected = { + 23: b"\x17", + 24: b"\x18\x18", + 255: b"\x18\xff", + 256: b"\x19\x01\x00", + 65535: b"\x19\xff\xff", + 65536: b"\x1a\x00\x01\x00\x00", + (1 << 32) - 1: b"\x1a\xff\xff\xff\xff", + 1 << 32: b"\x1b\x00\x00\x00\x01\x00\x00\x00\x00", +} +for value, wire in expected.items(): + assert cbor2.dumps(value, canonical=True) == wire + assert rr_scapy_decode(wire).enc() == wire + += Negative integer boundaries decode and re-encode exactly ~ external_cbor2 +values = [-1, -10, -24, -25, -256, -257, -65536, -65537, + -(1 << 32), -(1 << 32) - 1, -(1 << 64)] + +for value in values: + rr_assert_cbor2_value(value, canonical=True, exact=True) + += Negative integer additional-information transitions match cbor2 ~ external_cbor2 +values = [-24, -25, -256, -257, -65536, -65537, -(1 << 32), -(1 << 32) - 1] +for value in values: + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert isinstance(obj, CBOR_NEGATIVE_INTEGER) + assert obj.val == value + assert obj.enc() == wire + += Positive bignums generated by cbor2 remain wire-faithful through Scapy ~ external_cbor2 +for value in (1 << 64, 1 << 80, 1 << 128, (1 << 521) - 1): + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert isinstance(obj, CBOR_SEMANTIC_TAG) + assert obj.enc() == wire + assert cbor2.loads(obj.enc()) == value + += Negative bignums generated by cbor2 remain wire-faithful through Scapy ~ external_cbor2 +for value in (-(1 << 64) - 1, -(1 << 80), -(1 << 128), -(1 << 521)): + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert isinstance(obj, CBOR_SEMANTIC_TAG) + assert obj.enc() == wire + assert cbor2.loads(obj.enc()) == value + ++ Byte and text string vectors generated by cbor2 + += Byte-string length boundaries decode and re-encode exactly ~ external_cbor2 +for length in (0, 1, 22, 23, 24, 25, 254, 255, 256, 65535, 65536): + value = bytes((index * 17) & 0xff for index in range(length)) + rr_assert_cbor2_value(value, canonical=True, exact=True) + += Text-string ASCII length boundaries decode and re-encode exactly ~ external_cbor2 +for length in (0, 1, 22, 23, 24, 25, 254, 255, 256, 65535, 65536): + value = "x" * length + rr_assert_cbor2_value(value, canonical=True, exact=True) + += Text-string length headers use UTF-8 byte length rather than characters ~ external_cbor2 +values = [ + "ä" * 11 + "x", # 23 UTF-8 bytes + "ä" * 12, # 24 UTF-8 bytes + "€" * 8, # 24 UTF-8 bytes + "𐍈" * 6, # 24 UTF-8 bytes + "e\u0301" * 12, # combining sequence +] +for value in values: + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert obj.val == value + assert obj.enc() == wire + += Unicode and embedded-NUL strings interoperate in both directions ~ external_cbor2 +values = ["Grüße", "€uro", "𐍈", "e\u0301", "a\x00b", "日本語", "🙂"] +for value in values: + rr_assert_cbor2_value(value, canonical=True, exact=True) + rr_assert_scapy_native(value) + += Indefinite byte strings assembled from cbor2 chunks decode semantically ~ external_cbor2 +wire = b"\x5f" + cbor2.dumps(b"ab") + cbor2.dumps(b"") + cbor2.dumps(b"cd") + b"\xff" +assert cbor2.loads(wire) == b"abcd" +obj = rr_scapy_decode(wire) +assert obj.val == b"abcd" +assert cbor2.loads(obj.enc()) == b"abcd" + += Indefinite text strings assembled from cbor2 chunks decode semantically ~ external_cbor2 +wire = b"\x7f" + cbor2.dumps("Grü") + cbor2.dumps("") + cbor2.dumps("ße") + b"\xff" +assert cbor2.loads(wire) == "Grüße" +obj = rr_scapy_decode(wire) +assert obj.val == "Grüße" +assert cbor2.loads(obj.enc()) == "Grüße" + += Many cbor2-generated byte-string chunks concatenate correctly ~ external_cbor2 +chunks = [bytes([index & 0xff]) for index in range(1024)] +wire = b"\x5f" + b"".join(cbor2.dumps(chunk) for chunk in chunks) + b"\xff" +expected = b"".join(chunks) +assert cbor2.loads(wire) == expected +obj = rr_scapy_decode(wire) +assert obj.val == expected +assert cbor2.loads(obj.enc()) == expected + += Many Unicode text chunks concatenate correctly ~ external_cbor2 +chunks = ["ä", "€", "𐍈", "x"] * 256 +wire = b"\x7f" + b"".join(cbor2.dumps(chunk) for chunk in chunks) + b"\xff" +expected = "".join(chunks) +assert cbor2.loads(wire) == expected +obj = rr_scapy_decode(wire) +assert obj.val == expected +assert cbor2.loads(obj.enc()) == expected + ++ Simple values, tags, and standard cbor2 extensions + += Boolean, null, and undefined values agree between cbor2 and Scapy ~ external_cbor2 +for value in (False, True, None, cbor2.undefined): + rr_assert_cbor2_value(value, canonical=True, exact=True) + rr_assert_scapy_native(value) + += Direct and extended simple values agree between cbor2 and Scapy ~ external_cbor2 +for number in (0, 1, 16, 19, 32, 64, 127, 255): + value = cbor2.CBORSimpleValue(number) + rr_assert_cbor2_value(value, canonical=True, exact=True) + rr_assert_scapy_native(value) + += Unknown semantic-tag number boundaries round-trip exactly ~ external_cbor2 +for tag in (0, 23, 24, 255, 256, 65535, 65536, + (1 << 32) - 1, 1 << 32, (1 << 64) - 1): + # Scapy preserves the generic tag structure even when cbor2 assigns semantics. + wire = cbor2.dumps(cbor2.CBORTag(tag, "payload"), canonical=True) + obj = rr_scapy_decode(wire) + assert isinstance(obj, CBOR_SEMANTIC_TAG) + assert obj.val[0] == tag + assert obj.enc() == wire + += Nested unknown semantic tags preserve structure and bytes ~ external_cbor2 +value = cbor2.CBORTag(60000, cbor2.CBORTag(60001, [1, "x", b"y"])) +rr_assert_cbor2_value(value, canonical=True, exact=True) +rr_assert_scapy_native(value) + += Decimal extension encodings generated by cbor2 are wire-faithful ~ external_cbor2 +for value in (Decimal("0"), Decimal("-1.25"), Decimal("1E+100")): + rr_assert_extension_wire(value, canonical=True) + += Fraction extension encodings generated by cbor2 are wire-faithful ~ external_cbor2 +for value in (Fraction(1, 3), Fraction(-22, 7), Fraction(0, 1)): + rr_assert_extension_wire(value, canonical=True) + += Timezone-aware datetime extension encodings are wire-faithful ~ external_cbor2 +values = [ + datetime(1970, 1, 1, tzinfo=timezone.utc), + datetime(2026, 8, 29, 12, 34, 56, 123456, tzinfo=timezone.utc), +] +for value in values: + rr_assert_extension_wire(value, canonical=True) + += Date extension encodings are wire-faithful ~ external_cbor2 +for value in (date(1970, 1, 1), date(2026, 8, 29), date(9999, 12, 31)): + rr_assert_extension_wire(value, canonical=True) + += UUID extension encodings are wire-faithful ~ external_cbor2 +for value in (UUID(int=0), UUID("12345678-1234-5678-1234-567812345678")): + rr_assert_extension_wire(value, canonical=True) + += Set extension encodings are wire-faithful ~ external_cbor2 +for value in (frozenset(), frozenset({1, 2, 3}), frozenset({"b", "a"})): + rr_assert_extension_wire(value, canonical=True) + += Complex-number extension encodings remain semantically equivalent ~ external_cbor2 +for value in (0j, 1 + 2j, complex(-1.5, 2.25), complex(1.0e100, -1.0e-100)): + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + rebuilt = obj.enc() + assert cbor2.loads(rebuilt) == value + assert rr_norm_scapy(obj)[0] == "tag" + += Regular-expression extension encodings preserve their pattern ~ external_cbor2 +for value in (re.compile(r"a+"), re.compile(r"(?i)^[a-z0-9_]+$")): + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert obj.enc() == wire + expected = cbor2.loads(wire) + actual = cbor2.loads(obj.enc()) + assert isinstance(actual, re.Pattern) + assert actual.pattern == expected.pattern == value.pattern + += IPv4 and IPv6 extension encodings are wire-faithful ~ external_cbor2 +values = [ + IPv4Address("192.0.2.1"), + IPv4Network("192.0.2.0/24"), + IPv4Interface("192.0.2.1/24"), + IPv6Address("2001:db8::1"), + IPv6Network("2001:db8::/64"), + IPv6Interface("2001:db8::1/64"), +] +for value in values: + rr_assert_extension_wire(value, canonical=True) + += MIME text extension encodings remain semantically equivalent ~ external_cbor2 +message = MIMEText("Grüße from CBOR", "plain", "utf-8") +message["Subject"] = "cbor2 interoperability" +wire = cbor2.dumps(message, canonical=True) +obj = rr_scapy_decode(wire) +assert obj.enc() == wire +expected = cbor2.loads(wire) +actual = cbor2.loads(obj.enc()) +assert actual.as_bytes() == expected.as_bytes() +assert actual.get_payload() == expected.get_payload() + += Self-described CBOR tag is retained by Scapy and ignored by cbor2 ~ external_cbor2 +value = {"self-described": [1, 2, 3], "ok": True} +wire = cbor2.dumps(cbor2.CBORTag(55799, value), canonical=True) +obj = rr_scapy_decode(wire) +assert isinstance(obj, CBOR_SEMANTIC_TAG) +assert obj.val[0] == 55799 +assert obj.enc() == wire +# cbor2 may decode tag 55799 into frozendict/tuple; compare semantically. +assert rr_norm_cbor2(rr_cbor2_load(obj.enc())) == rr_norm_cbor2(value) + += bytearray and tuple encoder inputs produce ordinary CBOR values ~ external_cbor2 +cases = [ + (bytearray(b"mutable bytes"), b"mutable bytes"), + ((1, "two", b"three"), [1, "two", b"three"]), +] +for value, expected in cases: + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert cbor2.loads(obj.enc()) == expected + assert obj.enc() == wire + += cbor2 value-sharing tags survive generic Scapy decoding ~ external_cbor2 +shared = [1, 2, 3] +value = [shared, shared] +wire = cbor2.dumps(value, value_sharing=True) +obj = rr_scapy_decode(wire) +assert obj.enc() == wire +actual = cbor2.loads(obj.enc()) +assert actual == value +assert actual[0] is actual[1] + += cbor2 cyclic shared-reference data remains a finite generic CBOR tree ~ external_cbor2 +value = [] +value.append(value) +wire = cbor2.dumps(value, value_sharing=True) +obj = rr_scapy_decode(wire) +assert obj.enc() == wire +actual = cbor2.loads(obj.enc()) +assert actual[0] is actual + += cbor2 string-reference tags survive generic Scapy decoding ~ external_cbor2 +value = ["repeated-value", "repeated-value", {"repeated-value": "repeated-value"}] +wire = cbor2.dumps(value, string_referencing=True) +obj = rr_scapy_decode(wire) +assert obj.enc() == wire +assert cbor2.loads(obj.enc()) == value + ++ Valid non-preferred serializations accepted and normalized + += Overlong integer arguments decode like cbor2 and rebuild minimally ~ external_cbor2 +cases = [ + (b"\x18\x00", 0), + (b"\x19\x00\x17", 23), + (b"\x1a\x00\x00\x00\x18", 24), + (b"\x38\x00", -1), + (b"\x39\x00\x17", -24), + (b"\x3a\x00\x00\x00\x18", -25), +] +for wire, expected in cases: + assert cbor2.loads(wire) == expected + obj = rr_scapy_decode(wire) + assert obj.val == expected + assert obj.enc() == cbor2.dumps(expected, canonical=True) + += Overlong string and container lengths rebuild in shortest form ~ external_cbor2 +cases = [ + (b"\x58\x01x", b"x"), + (b"\x78\x01x", "x"), + (b"\x98\x01\x00", [0]), + (b"\xb8\x01\x00\x01", {0: 1}), +] +for wire, expected in cases: + assert cbor2.loads(wire) == expected + obj = rr_scapy_decode(wire) + assert obj.enc() == cbor2.dumps(expected, canonical=True) + assert cbor2.loads(obj.enc()) == expected + += An overlong semantic-tag header rebuilds in shortest form ~ external_cbor2 +wire = b"\xda\x00\x00\xea\x60\x00" # tag 60000 around integer 0 +expected = cbor2.CBORTag(60000, 0) +assert cbor2.loads(wire) == expected +obj = rr_scapy_decode(wire) +canonical = cbor2.dumps(expected, canonical=True) +assert obj.enc() == canonical +assert cbor2.loads(obj.enc()) == expected + += Mixed non-preferred headers normalize recursively ~ external_cbor2 +wire = ( + b"\x98\x03" # array(3), overlong length + b"\x18\x01" # uint 1, overlong + b"\x78\x01x" # text length 1, overlong + b"\xb8\x01\x18\x02\x18\x03" # {2: 3}, all overlong +) +expected = [1, "x", {2: 3}] +assert cbor2.loads(wire) == expected +obj = rr_scapy_decode(wire) +assert obj.enc() == cbor2.dumps(expected, canonical=True) +assert cbor2.loads(obj.enc()) == expected + ++ Floating-point vectors generated and validated by cbor2 + += Canonical cbor2 chooses half, single, and double float widths ~ external_cbor2 +cases = [(1.5, 0xf9), (100000.0, 0xfa), (1.1, 0xfb)] +for value, initial in cases: + wire = cbor2.dumps(value, canonical=True) + assert wire[0] == initial, (value, wire.hex()) + rr_assert_cbor2_value(value, canonical=True, exact=False) + += Positive and negative zero retain their sign across implementations ~ external_cbor2 +for value in (0.0, -0.0): + wire, obj = rr_assert_cbor2_value(value, canonical=True, exact=False) + assert math.copysign(1.0, obj.val) == math.copysign(1.0, value) + rebuilt = obj.enc() + assert math.copysign(1.0, cbor2.loads(rebuilt)) == math.copysign(1.0, value) + += Positive and negative infinity interoperate ~ external_cbor2 +for value in (float("inf"), float("-inf")): + rr_assert_cbor2_value(value, canonical=True, exact=False) + rr_assert_scapy_native(value) + += NaN remains NaN across width normalization ~ external_cbor2 +wire = cbor2.dumps(float("nan"), canonical=True) +obj = rr_scapy_decode(wire) +assert math.isnan(obj.val) +assert math.isnan(cbor2.loads(obj.enc())) + += Half, single, and double subnormal values interoperate ~ external_cbor2 +for value in (2.0 ** -24, 2.0 ** -149, 2.0 ** -1074): + wire = cbor2.dumps(value, canonical=True) + expected = cbor2.loads(wire) + obj = rr_scapy_decode(wire) + assert _rr_float(obj.val) == _rr_float(expected) + assert _rr_float(cbor2.loads(obj.enc())) == _rr_float(expected) + += Explicit half, single, and double encodings decode identically ~ external_cbor2 +wires = [ + b"\xf9\x3e\x00", + b"\xfa\x3f\xc0\x00\x00", + b"\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00", +] +for wire in wires: + assert cbor2.loads(wire) == 1.5 + obj = rr_scapy_decode(wire) + assert obj.val == 1.5 + assert cbor2.loads(obj.enc()) == 1.5 + += Scapy preferred float encodings are accepted by cbor2 for finite floats ~ external_cbor2 +values = [-1.0e300, -123.5, -0.0, 0.0, 1.5, 1.1, 1.0e300] +for value in values: + wire = rr_assert_scapy_native(value) + # Preferred serialization may use half/single/double; cbor2 must accept it. + assert wire[0] in (0xf9, 0xfa, 0xfb), wire.hex() + loaded = cbor2.loads(wire) + assert loaded == value or ( + math.copysign(1.0, loaded) == math.copysign(1.0, value) + and loaded == 0.0 and value == 0.0 + ) += Different NaN payloads remain semantic NaNs after Scapy re-encoding ~ external_cbor2 +for wire in (b"\xf9\x7e\x00", b"\xfa\x7f\xc0\x00\x01", + b"\xfb\x7f\xf8\x00\x00\x00\x00\x00\x01"): + assert math.isnan(cbor2.loads(wire)) + obj = rr_scapy_decode(wire) + assert math.isnan(obj.val) + assert math.isnan(cbor2.loads(obj.enc())) + ++ Arrays, maps, nesting, and canonical ordering + += Array length boundaries generated by cbor2 decode and re-encode exactly ~ external_cbor2 +for length in (0, 1, 22, 23, 24, 25, 254, 255, 256, 4096): + value = [index & 0x17 for index in range(length)] + rr_assert_cbor2_value(value, canonical=True, exact=True) + += Large array 16-bit to 32-bit length transition interoperates ~ external_cbor2 +for length in (65535, 65536): + value = [None] * length + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert len(obj.val) == length + assert obj.enc() == wire + += Map length boundaries generated by cbor2 decode and re-encode exactly ~ external_cbor2 +for length in (0, 1, 22, 23, 24, 25, 254, 255, 256): + value = {index: index + 1 for index in range(length)} + rr_assert_cbor2_value(value, canonical=True, exact=True) + += Deep heterogeneous containers agree semantically and exactly ~ external_cbor2 +value = { + "array": [1, -2, b"three", "four", None, True, cbor2.undefined], + "map": {"nested": [{"x": 1}, {"y": 2}]}, + "tag": cbor2.CBORTag(60000, [1, {"z": b"q"}]), +} +rr_assert_cbor2_value(value, canonical=True, exact=True) + += Canonical map order emitted by cbor2 is retained by Scapy ~ external_cbor2 +value = {"aa": 1, "b": 2, b"": 3, 10: 4, -1: 5} +wire = cbor2.dumps(value, canonical=True) +obj = rr_scapy_decode(wire) +assert obj.enc() == wire +assert rr_norm_cbor2(rr_cbor2_load(obj.enc())) == rr_norm_cbor2(rr_cbor2_load(wire)) + += A compound array-valued map key generated by cbor2 round-trips ~ external_cbor2 +value = {(1, "x", b"y"): "compound-key"} +wire = cbor2.dumps(value, canonical=True) +obj = rr_scapy_decode(wire) +assert isinstance(obj, CBOR_MAP) +assert obj.enc() == wire +assert rr_norm_cbor2(rr_cbor2_load(obj.enc())) == rr_norm_cbor2(rr_cbor2_load(wire)) + += A map-valued map key validated by immutable cbor2 round-trips ~ external_cbor2 +key_wire = cbor2.dumps({"inner": 1}, canonical=True) +wire = b"\xa1" + key_wire + cbor2.dumps("map-key", canonical=True) +decoded = cbor2.loads(wire, immutable=True) +assert len(decoded) == 1 +obj = rr_scapy_decode(wire) +pairs = obj.val.cbor_pairs() +assert len(pairs) == 1 +assert isinstance(pairs[0][0], CBOR_MAP) +assert obj.enc() == wire + += A semantic-tag-valued map key round-trips exactly ~ external_cbor2 +key_wire = cbor2.dumps(cbor2.CBORTag(60000, "key"), canonical=True) +wire = b"\xa1" + key_wire + cbor2.dumps("tag-key", canonical=True) +cbor2.loads(wire, immutable=True) +obj = rr_scapy_decode(wire) +pairs = obj.val.cbor_pairs() +assert len(pairs) == 1 +assert isinstance(pairs[0][0], CBOR_SEMANTIC_TAG) +assert obj.enc() == wire + += CBOR integer 1 and floating-point 1.0 remain distinct map keys ~ external_cbor2 +wire = ( + b"\xa2" + cbor2.dumps(1) + cbor2.dumps("integer") + + cbor2.dumps(1.0) + cbor2.dumps("float") +) +cbor2.loads(wire, immutable=True) +obj = rr_scapy_decode(wire) +pairs = obj.val.cbor_pairs() +assert len(pairs) == 2 +assert isinstance(pairs[0][0], CBOR_UNSIGNED_INTEGER) +assert isinstance(pairs[1][0], CBOR_FLOAT) +assert obj.enc() == wire + += Positive and negative floating zero are rejected as equivalent map keys ~ external_cbor2 +wire = ( + b"\xa2" + cbor2.dumps(0.0) + cbor2.dumps("positive") + + cbor2.dumps(-0.0) + cbor2.dumps("negative") +) +# cbor2 may accept the wire into a collapsed Python mapping. +cbor2.loads(wire, immutable=True) +try: + rr_scapy_decode(wire) + assert False, "+0.0 and -0.0 were accepted as distinct map keys" +except CBOR_Codec_Decoding_Error: + pass + += Distinct double-precision NaN payloads remain distinct map keys ~ external_cbor2 +first_nan = b"\xfb\x7f\xf8\x00\x00\x00\x00\x00\x01" +second_nan = b"\xfb\x7f\xf8\x00\x00\x00\x00\x00\x02" +wire = b"\xa2" + first_nan + cbor2.dumps(1) + second_nan + cbor2.dumps(2) +cbor2.loads(wire, immutable=True) +obj = rr_scapy_decode(wire) +pairs = obj.val.cbor_pairs() +assert len(pairs) == 2 +assert all(isinstance(key, CBOR_FLOAT) and math.isnan(key.val) for key, _ in pairs) +assert obj.enc() == wire + += CBOR integer 1 and Boolean true remain distinct map keys in Scapy ~ external_cbor2 +wire = ( + b"\xa2" + cbor2.dumps(1) + cbor2.dumps("integer") + + cbor2.dumps(True) + cbor2.dumps("boolean") +) +# cbor2 validates the complete wire, even though a Python mapping cannot +# faithfully expose these two Python-equal keys at the same time. +cbor2.loads(wire) +obj = rr_scapy_decode(wire) +pairs = obj.val.cbor_pairs() +assert len(pairs) == 2 +assert isinstance(pairs[0][0], CBOR_UNSIGNED_INTEGER) +assert isinstance(pairs[1][0], CBOR_TRUE) +assert obj.enc() == wire + += Indefinite arrays generated by cbor2 normalize semantically in Scapy ~ external_cbor2 +for value in ([], [1], [1, "two", [3, 4]], [{"x": 1}, {"y": 2}]): + wire = cbor2.dumps(value, indefinite_containers=True) + assert wire[0] == 0x9f and wire[-1] == 0xff + rr_assert_cbor2_value(value, indefinite_containers=True, exact=False) + += Indefinite maps generated by cbor2 normalize semantically in Scapy ~ external_cbor2 +for value in ({}, {"x": 1}, {"x": [1, 2], "y": {"z": 3}}): + wire = cbor2.dumps(value, indefinite_containers=True) + assert wire[0] == 0xbf and wire[-1] == 0xff + rr_assert_cbor2_value(value, indefinite_containers=True, exact=False) + += Mixed nested indefinite containers generated by cbor2 interoperate ~ external_cbor2 +value = [{"a": [1, 2]}, {"b": {"c": [3, 4]}}] +rr_assert_cbor2_value(value, indefinite_containers=True, exact=False) + += Scapy accepts the maximum configured nesting depth from cbor2 ~ external_cbor2 +value = 0 +for _ in range(MAX_CBOR_NESTING): + value = [value] + +wire = cbor2.dumps(value) +rr_scapy_decode(wire) +assert cbor2.loads(wire, max_depth=MAX_CBOR_NESTING + 2) == value + += Scapy rejects one level beyond its configured nesting depth ~ external_cbor2 +value = 0 +for _ in range(MAX_CBOR_NESTING + 1): + value = [value] + +wire = cbor2.dumps(value) +assert cbor2.loads(wire, max_depth=MAX_CBOR_NESTING + 2) == value +rr_scapy_reject(wire) + ++ CBOR sequence and remainder interoperability + += Scapy leaves the exact cbor2-generated suffix after one decoded item ~ external_cbor2 +first = cbor2.dumps({"first": [1, 2]}, canonical=True) +second = cbor2.dumps(cbor2.CBORTag(60000, "second"), canonical=True) +obj, remainder = CBOR_Codecs.CBOR.dec(first + second) +assert obj.enc() == first +assert remainder == second + += A heterogeneous cbor2 sequence decodes item-by-item in Scapy ~ external_cbor2 +values = [0, -1, b"x", "y", [1, 2], {"z": 3}, True, None, + cbor2.undefined, cbor2.CBORTag(60000, 4)] + +wire = b"".join(cbor2.dumps(value, canonical=True) for value in values) +expected = [rr_norm_cbor2(rr_cbor2_load(cbor2.dumps(value, canonical=True))) + for value in values] + +assert rr_scapy_sequence(wire) == expected +assert rr_cbor2_sequence(wire, len(values)) == expected + += cbor2 decodes a sequence produced by Scapy native encoders ~ external_cbor2 +values = [0, -1, b"x", "y", [1, 2], {"z": 3}, True, None, + CBOR_UNDEFINED(), CBOR_SEMANTIC_TAG((60000, CBOR_UNSIGNED_INTEGER(4)))] + +wire = b"".join(CBORcodec_Object.encode_cbor_item(value) for value in values) +expected = [rr_norm_native(value) for value in values] +assert rr_cbor2_sequence(wire, len(values)) == expected +assert rr_scapy_sequence(wire) == expected + += A 100-item deterministic sequence makes forward progress in both decoders ~ external_cbor2 +rng = random.Random(0xCB020001) +values = [rr_random_value(rng, include_float=False) for _ in range(100)] +wire = b"".join(cbor2.dumps(value, canonical=True) for value in values) +expected = [rr_norm_cbor2(rr_cbor2_load(cbor2.dumps(value, canonical=True))) + for value in values] + +assert rr_scapy_sequence(wire) == expected +assert rr_cbor2_sequence(wire, len(values)) == expected + ++ Typed CBOR packet fields with cbor2-generated wire data + += CBORF_UNSIGNED_INTEGER accepts all cbor2 uint64 boundaries ~ external_cbor2 +for value in (0, 23, 24, 255, 256, 65535, 65536, (1 << 64) - 1): + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2UInt(wire) + assert pkt.value == value + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_NEGATIVE_INTEGER accepts all cbor2 int64 boundaries ~ external_cbor2 +for value in (-1, -24, -25, -256, -257, -65536, -65537, -(1 << 64)): + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2NInt(wire) + assert pkt.value == value + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_BYTE_STRING accepts definite strings generated by cbor2 ~ external_cbor2 +for value in (b"", b"x", bytes(range(256)), b"z" * 65536): + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2Bytes(wire) + assert pkt.value == value + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_BYTE_STRING accepts a valid indefinite string and rebuilds definite ~ external_cbor2 +wire = b"\x5f" + cbor2.dumps(b"ab") + cbor2.dumps(b"cd") + b"\xff" +pkt = RRCbor2Bytes(wire) +assert pkt.value == b"abcd" +rr_clear_cache(pkt) +assert bytes(pkt) == cbor2.dumps(b"abcd") + += CBORF_BYTE_STRING definite-only mode accepts cbor2 definite data ~ external_cbor2 +for value in (b"", b"x", bytes(range(32)), b"z" * 256): + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2DefiniteBytes(wire) + assert pkt.value == value + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_BYTE_STRING definite-only mode rejects an oracle-valid indefinite value ~ external_cbor2 +wire = b"\x5f" + cbor2.dumps(b"ab") + cbor2.dumps(b"cd") + b"\xff" +assert cbor2.loads(wire) == b"abcd" +try: + RRCbor2DefiniteBytes(wire) +except CBOR_Decoding_Error: + pass +else: + raise AssertionError("definite-only byte field accepted an indefinite string") + += CBORF_TEXT_STRING accepts Unicode strings generated by cbor2 ~ external_cbor2 +for value in ("", "hello", "Grüße", "𐍈" * 100, "a\x00b"): + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2Text(wire) + assert pkt.value == value + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_TEXT_STRING accepts a valid indefinite string and rebuilds definite ~ external_cbor2 +wire = b"\x7f" + cbor2.dumps("Grü") + cbor2.dumps("ße") + b"\xff" +pkt = RRCbor2Text(wire) +assert pkt.value == "Grüße" +rr_clear_cache(pkt) +assert bytes(pkt) == cbor2.dumps("Grüße") + += CBORF_BOOLEAN agrees with cbor2 for both Boolean values ~ external_cbor2 +for value in (False, True): + wire = cbor2.dumps(value) + pkt = RRCbor2Bool(wire) + assert pkt.value is value + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_NULL agrees with cbor2 null ~ external_cbor2 +wire = cbor2.dumps(None) +pkt = RRCbor2Null(wire) +assert pkt.value is None +rr_clear_cache(pkt) +assert bytes(pkt) == wire + += CBORF_UNDEFINED agrees with cbor2 undefined ~ external_cbor2 +wire = cbor2.dumps(cbor2.undefined) +pkt = RRCbor2Undefined(wire) +assert pkt.value is None +rr_clear_cache(pkt) +assert bytes(pkt) == wire + += CBORF_FLOAT accepts every cbor2 float width and rebuilds valid double ~ external_cbor2 +for wire in (b"\xf9\x3e\x00", b"\xfa\x3f\xc0\x00\x00", + b"\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00"): + expected = cbor2.loads(wire) + pkt = RRCbor2Float(wire) + assert _rr_float(pkt.value) == _rr_float(expected) + rr_clear_cache(pkt) + assert _rr_float(cbor2.loads(bytes(pkt))) == _rr_float(expected) + += CBORF_ARRAY_OF decodes homogeneous cbor2 arrays at length boundaries ~ external_cbor2 +for length in (0, 1, 23, 24, 255, 256): + values = list(range(length)) + wire = cbor2.dumps(values, canonical=True) + pkt = RRCbor2UIntArray(wire) + assert pkt.values == values + rr_clear_cache(pkt) + assert bytes(pkt) == wire + += CBORF_ARRAY_OF decodes indefinite cbor2 arrays ~ external_cbor2 +wire = cbor2.dumps([1, 2, 3], indefinite_containers=True) +pkt = RRCbor2UIntArray(wire) +assert pkt.values == [1, 2, 3] +assert bytes(pkt) == wire + += CBORF_SEMANTIC_TAG decodes and rebuilds a cbor2-generated tag ~ external_cbor2 +value = cbor2.CBORTag(60000, "tagged") +wire = cbor2.dumps(value, canonical=True) +pkt = RRCbor2TaggedText(wire) +assert pkt.value == "tagged" +rr_clear_cache(pkt) +assert bytes(pkt) == wire + += Typed packet mutation produces wire accepted by cbor2 ~ external_cbor2 +pkt = RRCbor2UInt(cbor2.dumps(23)) +pkt.value = 65536 +wire = bytes(pkt) +assert cbor2.loads(wire) == 65536 +assert wire == cbor2.dumps(65536) + ++ CBORF_ANY differential packet tests + += CBORF_ANY scalar values generated by cbor2 rebuild semantically ~ external_cbor2 +values = [0, (1 << 64) - 1, -1, -(1 << 64), b"bytes", "text", + False, True, None, cbor2.undefined, cbor2.CBORSimpleValue(32), + 1.5, cbor2.CBORTag(60000, "tag")] + +for value in values: + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2AnyRoot(wire) + rr_clear_cache(pkt) + rebuilt = bytes(pkt) + assert rr_norm_cbor2(rr_cbor2_load(rebuilt)) == rr_norm_cbor2(rr_cbor2_load(wire)) + += CBORF_ANY nested arrays generated by cbor2 survive sibling mutation ~ external_cbor2 +value = [1, [2, [3]], "four"] +wire = cbor2.dumps([value, 0], canonical=True) +pkt = RRCbor2AnyEnvelope(wire) +pkt.tail = 1 +assert cbor2.loads(bytes(pkt)) == [value, 1] + += CBORF_ANY non-empty maps remain maps after sibling mutation ~ external_cbor2 +value = {"a": 1, "b": [2, 3]} +wire = cbor2.dumps([value, 0], canonical=True) +pkt = RRCbor2AnyEnvelope(wire) +pkt.tail = 1 +assert cbor2.loads(bytes(pkt)) == [value, 1] + += CBORF_ANY empty maps do not become arrays after sibling mutation ~ external_cbor2 +wire = cbor2.dumps([{}, 0], canonical=True) +pkt = RRCbor2AnyEnvelope(wire) +pkt.tail = 1 +actual = cbor2.loads(bytes(pkt)) +assert actual == [{}, 1] +assert isinstance(actual[0], dict) + += CBORF_ANY unknown nested tags remain tags after rebuild ~ external_cbor2 +value = cbor2.CBORTag(60000, [cbor2.CBORTag(60001, {"x": 1})]) +wire = cbor2.dumps(value, canonical=True) +pkt = RRCbor2AnyRoot(wire) +rr_clear_cache(pkt) +assert rr_norm_cbor2(rr_cbor2_load(bytes(pkt))) == rr_norm_cbor2(rr_cbor2_load(wire)) + += CBORF_ANY preserves simple values and undefined distinctly ~ external_cbor2 +for value in (cbor2.CBORSimpleValue(0), cbor2.CBORSimpleValue(255), cbor2.undefined): + wire = cbor2.dumps(value) + pkt = RRCbor2AnyRoot(wire) + rr_clear_cache(pkt) + assert rr_norm_cbor2(rr_cbor2_load(bytes(pkt))) == rr_norm_cbor2(rr_cbor2_load(wire)) + += CBORF_ANY accepts cbor2 indefinite arrays and rebuilds equivalent data ~ external_cbor2 +value = [1, {"x": [2, 3]}] +wire = cbor2.dumps(value, indefinite_containers=True) +pkt = RRCbor2AnyRoot(wire) +rr_clear_cache(pkt) +assert cbor2.loads(bytes(pkt)) == value + += CBORF_ANY accepts cbor2 indefinite maps and rebuilds a map ~ external_cbor2 +value = {"x": [1, 2], "y": {"z": 3}} +wire = cbor2.dumps(value, indefinite_containers=True) +pkt = RRCbor2AnyRoot(wire) +rr_clear_cache(pkt) +actual = cbor2.loads(bytes(pkt)) +assert actual == value +assert isinstance(actual, dict) + += CBORF_ANY bignums rebuild to the original mathematical integer ~ external_cbor2 +for value in (1 << 100, -(1 << 100)): + wire = cbor2.dumps(value, canonical=True) + pkt = RRCbor2AnyRoot(wire) + rr_clear_cache(pkt) + assert cbor2.loads(bytes(pkt)) == value + += In-place mutation of a CBORF_ANY outer list invalidates cached bytes ~ external_cbor2 +wire = cbor2.dumps([[1, 2], 0], canonical=True) +pkt = RRCbor2AnyEnvelope(wire) +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) +assert cbor2.loads(bytes(pkt)) == [[1, 2, 3], 0] + += In-place mutation of a nested CBORF_ANY list invalidates cached bytes ~ external_cbor2 +wire = cbor2.dumps([[[1]], 0], canonical=True) +pkt = RRCbor2AnyEnvelope(wire) +pkt.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(2)) +assert cbor2.loads(bytes(pkt)) == [[[1, 2]], 0] + += CBORF_ANY map-value mutation invalidates cached bytes ~ external_cbor2 +wire = cbor2.dumps([{"x": [1]}, 0], canonical=True) +pkt = RRCbor2AnyEnvelope(wire) +# The fixed representation must expose a mutable map while retaining major type 5. +map_value = pkt.value +assert isinstance(map_value, CBOR_MAP) +assert isinstance(map_value.val, CBORMapData) +map_value.val["x"].val.append(CBOR_UNSIGNED_INTEGER(2)) +assert cbor2.loads(bytes(pkt)) == [{"x": [1, 2]}, 0] + ++ Seeded randomized differential corpora + += 256 canonical non-float values are byte-identical through generic Scapy ~ external_cbor2 +rng = random.Random(0xCB020101) +for index in range(256): + value = rr_random_value(rng, include_float=False) + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + rebuilt = obj.enc() + assert rebuilt == wire, (index, wire.hex(), rebuilt.hex(), value) + assert rr_norm_cbor2(rr_cbor2_load(rebuilt)) == rr_norm_cbor2(rr_cbor2_load(wire)) + += 256 default cbor2 values including floats agree semantically with Scapy ~ external_cbor2 +rng = random.Random(0xCB020102) +for index in range(256): + value = rr_random_value(rng, include_float=True) + wire = cbor2.dumps(value) + obj = rr_scapy_decode(wire) + expected = rr_norm_cbor2(rr_cbor2_load(wire)) + actual = rr_norm_scapy(obj) + assert actual == expected, (index, wire.hex(), expected, actual, value) + assert rr_norm_cbor2(rr_cbor2_load(obj.enc())) == expected + += 128 cbor2 indefinite-container values agree semantically with Scapy ~ external_cbor2 +rng = random.Random(0xCB020103) +for index in range(128): + value = rr_random_value(rng, include_float=True) + wire = cbor2.dumps(value, indefinite_containers=True) + obj = rr_scapy_decode(wire) + expected = rr_norm_cbor2(rr_cbor2_load(wire)) + actual = rr_norm_scapy(obj) + assert actual == expected, (index, wire.hex(), expected, actual, value) + assert rr_norm_cbor2(rr_cbor2_load(obj.enc())) == expected + += 256 Scapy-native randomized values are accepted by cbor2 ~ external_cbor2 +rng = random.Random(0xCB020104) +for index in range(256): + value = rr_random_value(rng, include_float=True) + native = rr_to_scapy_native(value) + wire = CBORcodec_Object.encode_cbor_item(native) + expected = rr_norm_native(native) + actual = rr_norm_cbor2(rr_cbor2_load(wire)) + assert actual == expected, (index, wire.hex(), expected, actual, value) + += 128 randomized canonical maps retain cbor2 canonical key order ~ external_cbor2 +rng = random.Random(0xCB020105) +for index in range(128): + value = {} + while len(value) < rng.randrange(0, 12): + value[rr_random_key(rng)] = rr_random_value(rng, 2, include_float=False) + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert obj.enc() == wire, (index, wire.hex(), obj.enc().hex(), value) + += 128 randomized unknown-tag trees remain byte-identical ~ external_cbor2 +rng = random.Random(0xCB020106) +for index in range(128): + value = cbor2.CBORTag( + 60000 + index, + rr_random_value(rng, include_float=False), + ) + wire = cbor2.dumps(value, canonical=True) + obj = rr_scapy_decode(wire) + assert obj.enc() == wire, (index, wire.hex(), obj.enc().hex()) + ++ Malformed-input differential tests + += Every proper prefix of cbor2-generated composite values is rejected ~ external_cbor2 +values = [ + b"x" * 32, + "Grüße" * 8, + [1, 2, [3, 4]], + {"a": 1, "b": [2, 3]}, + cbor2.CBORTag(60000, {"x": [1, 2]}), + 1.5, +] +for value in values: + wire = cbor2.dumps(value, canonical=True) + for cut in range(len(wire)): + rr_both_reject(wire[:cut]) + += Invalid UTF-8 text is rejected by both implementations ~ external_cbor2 +for wire in (b"\x61\xff", b"\x62\xc0\x80", b"\x63\xed\xa0\x80"): + rr_both_reject(wire) + += Exact duplicate map keys are rejected by both strict decoders ~ external_cbor2 +wire = b"\xa2" + cbor2.dumps(1) + cbor2.dumps(0) + cbor2.dumps(1) + cbor2.dumps(1) +rr_cbor2_reject(wire, allow_duplicate_keys=False) +rr_scapy_reject(wire) + += Semantically duplicate shortest and overlong map keys are rejected ~ external_cbor2 +wire = b"\xa2\x01\x00\x18\x01\x01" +assert cbor2.loads(wire) == {1: 1} +rr_cbor2_reject(wire, allow_duplicate_keys=False) +rr_scapy_reject(wire) + += Standalone and misplaced break bytes are rejected by Scapy ~ external_cbor2 +# cbor2 6.1.4 decodes a bare/misplaced break as a sentinel object; Scapy must +# still reject these as non-well-formed top-level / container items. +for wire in (b"\xff", b"\x81\xff", b"\xa1\xff\x00"): + rr_scapy_reject(wire) + += Reserved additional-information values are rejected by both ~ external_cbor2 +for major in range(8): + for additional in (28, 29, 30): + rr_both_reject(bytes([(major << 5) | additional])) + += Non-well-formed two-byte simple values below 32 are rejected ~ external_cbor2 +for number in range(32): + rr_both_reject(b"\xf8" + bytes([number])) + += Indefinite byte strings reject text-string chunks ~ external_cbor2 +wire = b"\x5f" + cbor2.dumps("wrong chunk type") + b"\xff" +rr_both_reject(wire) + += Indefinite text strings reject byte-string chunks ~ external_cbor2 +wire = b"\x7f" + cbor2.dumps(b"wrong chunk type") + b"\xff" +rr_both_reject(wire) + += Indefinite byte strings reject nested indefinite chunks ~ external_cbor2 +wire = b"\x5f\x5f" + cbor2.dumps(b"nested") + b"\xff\xff" +rr_both_reject(wire) + += Indefinite text strings reject nested indefinite chunks ~ external_cbor2 +wire = b"\x7f\x7f" + cbor2.dumps("nested") + b"\xff\xff" +rr_both_reject(wire) + += A UTF-8 code point split across text chunks is rejected ~ external_cbor2 +# U+00E4 is UTF-8 C3 A4, but each chunk must independently be valid UTF-8. +wire = b"\x7f\x61\xc3\x61\xa4\xff" +rr_both_reject(wire) + += Indefinite strings reject a break before a chunk payload completes ~ external_cbor2 +for wire in (b"\x5f\x42a\xff", b"\x7f\x62a\xff"): + rr_both_reject(wire) + += Indefinite maps reject a key without a value ~ external_cbor2 +wire = b"\xbf" + cbor2.dumps("key") + b"\xff" +rr_both_reject(wire) + += Semantic tags reject a missing tagged data item ~ external_cbor2 +wire = cbor2.dumps(cbor2.CBORTag(60000, 0), canonical=True) +# Strip the complete encoded value, retaining only the cbor2-generated tag head. +tag_only = wire[:-1] +rr_both_reject(tag_only) + += Truncated half, single, and double floats are rejected by both ~ external_cbor2 +for value in (1.5, 100000.0, 1.1): + wire = cbor2.dumps(value, canonical=True) + for cut in range(1, len(wire)): + rr_both_reject(wire[:cut]) + += Scapy safedec wraps every cbor2-rejected truncation ~ external_cbor2 +wire = cbor2.dumps({"a": [1, 2, 3], "b": "text"}, canonical=True) +for cut in range(len(wire)): + prefix = wire[:cut] + rr_cbor2_reject(prefix) + result, remainder = CBOR_Codecs.CBOR.safedec(prefix) + assert isinstance(result, CBOR_DECODING_ERROR) + assert remainder == b"" + += cbor2 strict mode rejects indefinite data that generic Scapy accepts ~ external_cbor2 +values = [[], [1, 2], {}, {"x": 1}] +for value in values: + wire = cbor2.dumps(value, indefinite_containers=True) + rr_cbor2_reject(wire, allow_indefinite=False) + obj = rr_scapy_decode(wire) + assert rr_norm_cbor2(rr_cbor2_load(obj.enc())) == rr_norm_cbor2(rr_cbor2_load(wire)) + ++ Canonicalization and encode/decode idempotence + += A broad canonical cbor2 corpus is byte-identical in generic Scapy ~ external_cbor2 +values = [ + 0, 23, 24, (1 << 64) - 1, -1, -(1 << 64), b"", b"x" * 256, + "", "Grüße", False, True, None, cbor2.undefined, + cbor2.CBORSimpleValue(255), [1, "x", b"y"], + {"b": 2, "a": 1}, cbor2.CBORTag(60000, {"x": [1, 2]}), +] +for value in values: + wire = cbor2.dumps(value, canonical=True) + assert rr_scapy_decode(wire).enc() == wire + += Scapy generic encoding is idempotent after cbor2 input ~ external_cbor2 +rng = random.Random(0xCB020201) +for index in range(256): + value = rr_random_value(rng, include_float=True) + original = cbor2.dumps(value) + first = rr_scapy_decode(original).enc() + second = rr_scapy_decode(first).enc() + assert second == first, (index, original.hex(), first.hex(), second.hex()) + += Scapy-normalized indefinite data remains stable on a second build ~ external_cbor2 +rng = random.Random(0xCB020202) +for index in range(128): + value = rr_random_value(rng, include_float=True) + indefinite = cbor2.dumps(value, indefinite_containers=True) + first = rr_scapy_decode(indefinite).enc() + second = rr_scapy_decode(first).enc() + assert second == first, (index, indefinite.hex(), first.hex(), second.hex()) + += cbor2 canonicalization of Scapy output preserves semantics ~ external_cbor2 +rng = random.Random(0xCB020203) +for index in range(256): + value = rr_random_value(rng, include_float=True) + native = rr_to_scapy_native(value) + scapy_wire = CBORcodec_Object.encode_cbor_item(native) + decoded = cbor2.loads(scapy_wire) + canonical = cbor2.dumps(decoded, canonical=True) + assert rr_norm_cbor2(rr_cbor2_load(canonical)) == rr_norm_native(native), index + += cbor2 length-header transitions remain exact after Scapy decoding ~ external_cbor2 +values = [ + b"x" * 23, b"x" * 24, b"x" * 255, b"x" * 256, + "x" * 23, "x" * 24, "x" * 255, "x" * 256, + [None] * 23, [None] * 24, [None] * 255, [None] * 256, + {index: None for index in range(23)}, + {index: None for index in range(24)}, + {index: None for index in range(255)}, + {index: None for index in range(256)}, +] +for value in values: + wire = cbor2.dumps(value, canonical=True) + assert rr_scapy_decode(wire).enc() == wire diff --git a/test/scapy/layers/cbor_test_utils.py b/test/scapy/layers/cbor_test_utils.py new file mode 100644 index 00000000000..a4a24df85f6 --- /dev/null +++ b/test/scapy/layers/cbor_test_utils.py @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +"""Test helpers for CBOR deterministic-encoding checks.""" + +import struct +from typing import List, Tuple, Union + +from scapy.cbor.cbor import ( + CBOR_AdditionalInfo, + CBOR_FloatAI, + CBOR_MajorTypes, +) +from scapy.cbor.cborcodec import ( + CBOR_BREAK_BYTE, + CBOR_Codec_Decoding_Error, + CBOR_INDEFINITE, + CBOR_decode_head, + MAX_CBOR_NESTING, + _cbor_float_from_bits, + _cbor_nan_components, + _cbor_nan_preferred_ai, + _cbor_preferred_float_ai, + cbor_is_break, +) + + +def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): + # type: (bytes, bool, int) -> List[Tuple[int, str]] + """Scan one top-level CBOR item for non-core-deterministic encodings. + + Walks a single top-level item (and nested contents). Trailing bytes after + that item are ignored. Returns ``(absolute_offset, message)`` issues. + Indefinite-length items are rejected by default; protocols that permit + them may pass ``allow_indefinite=True``. + """ + issues = [] # type: List[Tuple[int, str]] + index = [0] + + def _argument_is_shortest(ai, value): + # type: (int, Union[int, CBOR_INDEFINITE]) -> bool + if value is CBOR_INDEFINITE: + return ai == CBOR_AdditionalInfo.INDEFINITE + if ai < 24: + return True + if ai == CBOR_AdditionalInfo.ONE_BYTE: + return int(value) >= 24 + if ai == CBOR_AdditionalInfo.TWO_BYTES: + return int(value) >= 256 + if ai == CBOR_AdditionalInfo.FOUR_BYTES: + return int(value) >= 65536 + if ai == CBOR_AdditionalInfo.EIGHT_BYTES: + return int(value) >= (1 << 32) + return ai == CBOR_AdditionalInfo.INDEFINITE + + def _walk(depth=0): + # type: (int) -> None + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=s[index[0]:]) + start = index[0] + if start >= len(s): + raise CBOR_Codec_Decoding_Error( + "Empty CBOR data", remaining=s[start:]) + initial = s[start] + if initial == CBOR_BREAK_BYTE: + issues.append(( + base_offset + start, + "Standalone break byte (0xff)", + )) + index[0] = start + 1 + return + major = initial >> 5 + ai = initial & 0x1f + pos = start + 1 + if ai < 24: + value = ai # type: Union[int, CBOR_INDEFINITE] + elif ai == CBOR_AdditionalInfo.ONE_BYTE: + if pos + 1 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 1-byte value", remaining=s[start:]) + value = s[pos] + pos += 1 + elif ai == CBOR_AdditionalInfo.TWO_BYTES: + if pos + 2 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 2-byte value", remaining=s[start:]) + value = struct.unpack(">H", s[pos:pos + 2])[0] + pos += 2 + elif ai == CBOR_AdditionalInfo.FOUR_BYTES: + if pos + 4 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 4-byte value", remaining=s[start:]) + value = struct.unpack(">I", s[pos:pos + 4])[0] + pos += 4 + elif ai == CBOR_AdditionalInfo.EIGHT_BYTES: + if pos + 8 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 8-byte value", remaining=s[start:]) + value = struct.unpack(">Q", s[pos:pos + 8])[0] + pos += 8 + elif ai == CBOR_AdditionalInfo.INDEFINITE: + value = CBOR_INDEFINITE + elif ai in ( + CBOR_AdditionalInfo.RESERVED_28, + CBOR_AdditionalInfo.RESERVED_29, + CBOR_AdditionalInfo.RESERVED_30, + ): + raise CBOR_Codec_Decoding_Error( + "Reserved additional info: %d" % ai, remaining=s[start:]) + else: + raise CBOR_Codec_Decoding_Error( + "Invalid additional info: %d" % ai, remaining=s[start:]) + index[0] = pos + + # Major type 7: simple values and floats. Check float preferred width. + if major == CBOR_MajorTypes.SIMPLE_AND_FLOAT: + if ( + ai == CBOR_AdditionalInfo.ONE_BYTE + and isinstance(value, int) + and value < 32 + ): + issues.append(( + base_offset + start, + "Non-shortest CBOR simple value encoding " + "(AI=24, value=%d)" % value, + )) + if ai in ( + CBOR_FloatAI.HALF, + CBOR_FloatAI.SINGLE, + CBOR_FloatAI.DOUBLE, + ) and value is not CBOR_INDEFINITE: + comps = _cbor_nan_components(ai, int(value)) + if comps is not None: + preferred = _cbor_nan_preferred_ai(ai, int(value)) + else: + preferred = _cbor_preferred_float_ai( + _cbor_float_from_bits(ai, int(value)) + ) + if preferred < ai: + issues.append(( + base_offset + start, + "Non-shortest CBOR float encoding (AI=%d, preferred AI=%d)" + % (ai, preferred), + )) + return + + if value is CBOR_INDEFINITE: + if not allow_indefinite: + issues.append(( + base_offset + start, + "Indefinite-length item is not allowed", + )) + if major in ( + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, + ): + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + chunk_start = index[0] + chunk_major, chunk_len, rem = CBOR_decode_head(s[chunk_start:]) + consumed = len(s) - chunk_start - len(rem) + if chunk_major != major: + raise CBOR_Codec_Decoding_Error( + "Indefinite string chunk must be major type %d, " + "got %d" % (major, chunk_major), + remaining=s[chunk_start:]) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite string", + remaining=s[chunk_start:]) + chunk_ai = s[chunk_start] & 0x1f + if not _argument_is_shortest(chunk_ai, chunk_len): + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) + if len(rem) < int(chunk_len): + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string chunk", + remaining=s[chunk_start:]) + index[0] = chunk_start + consumed + int(chunk_len) + if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=s[index[0]:]) + index[0] += 1 + return + if major == CBOR_MajorTypes.ARRAY: + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + _walk(depth + 1) + if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=s[index[0]:]) + index[0] += 1 + return + if major == CBOR_MajorTypes.MAP: + key_encodings = [] # type: List[bytes] + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + key_start = index[0] + _walk(depth + 1) + key_encodings.append(bytes(s[key_start:index[0]])) + _walk(depth + 1) + if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=s[index[0]:]) + index[0] += 1 + if key_encodings != sorted(key_encodings): + issues.append(( + base_offset + start, + "CBOR map keys are not in bytewise lexicographic order", + )) + return + raise CBOR_Codec_Decoding_Error( + "Indefinite length not allowed for major type %d" % major, + remaining=s[start:], + ) + + if not _argument_is_shortest(ai, value): + issues.append(( + base_offset + start, + "Non-shortest CBOR argument encoding (AI=%d, value=%r)" + % (ai, value), + )) + + if major in ( + CBOR_MajorTypes.BYTE_STRING, + CBOR_MajorTypes.TEXT_STRING, + ): + length = int(value) + if index[0] + length > len(s): + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string", remaining=s[start:]) + index[0] += length + return + if major == CBOR_MajorTypes.ARRAY: + for _ in range(int(value)): + _walk(depth + 1) + return + if major == CBOR_MajorTypes.MAP: + key_encodings = [] # type: List[bytes] + for _ in range(int(value)): + key_start = index[0] + _walk(depth + 1) + key_encodings.append(bytes(s[key_start:index[0]])) + _walk(depth + 1) + if key_encodings != sorted(key_encodings): + issues.append(( + base_offset + start, + "CBOR map keys are not in bytewise lexicographic order", + )) + return + if major == CBOR_MajorTypes.TAG: + _walk(depth + 1) + return + + try: + _walk() + except CBOR_Codec_Decoding_Error: + # Malformed input is reported by normal decoding, not this checker. + pass + return issues + diff --git a/test/scapy/layers/generate_cbor2_corpus.py b/test/scapy/layers/generate_cbor2_corpus.py new file mode 100755 index 00000000000..9b46b130d66 --- /dev/null +++ b/test/scapy/layers/generate_cbor2_corpus.py @@ -0,0 +1,151 @@ +#!/usr/bin/env python3 +"""Generate a reproducible CBOR corpus with cbor2 6.1.4. + +The UTS campaign performs live differential checks. This helper freezes the +same style of independently generated vectors into JSON for debugging, +minimization, or CI systems that prefer checked-in fixtures. +""" + +from __future__ import annotations + +import argparse +import json +import math +import random +from importlib.metadata import version as distribution_version +from pathlib import Path +from typing import Any + +import cbor2 + +VERSION = "6.1.4" +DEFAULT_SEED = 0xCB020301 + + +def random_key(rng: random.Random) -> Any: + kind = rng.randrange(3) + if kind == 0: + return rng.randint(-100000, 100000) + if kind == 1: + return bytes(rng.randrange(256) for _ in range(rng.randrange(8))) + alphabet = "abcXYZ012-_ä" + return "".join(rng.choice(alphabet) for _ in range(rng.randrange(8))) + + +def random_value(rng: random.Random, depth: int = 0) -> Any: + kinds = [ + "uint", "nint", "bytes", "text", "bool", "null", "undefined", + "simple", "float", "tag", + ] + if depth < 4: + kinds.extend(("array", "map")) + kind = rng.choice(kinds) + if kind == "uint": + return rng.randrange(0, 1 << rng.choice((4, 8, 16, 32, 64))) + if kind == "nint": + return -1 - rng.randrange(0, 1 << rng.choice((4, 8, 16, 32, 63))) + if kind == "bytes": + return bytes(rng.randrange(256) for _ in range(rng.randrange(32))) + if kind == "text": + alphabet = "abcXYZ012-_ä€𐍈\x00" + return "".join(rng.choice(alphabet) for _ in range(rng.randrange(24))) + if kind == "bool": + return bool(rng.getrandbits(1)) + if kind == "null": + return None + if kind == "undefined": + return cbor2.undefined + if kind == "simple": + return cbor2.CBORSimpleValue(rng.choice((0, 1, 16, 19, 32, 64, 127, 255))) + if kind == "float": + special = rng.randrange(12) + if special == 0: + return -0.0 + if special == 1: + return float("inf") + if special == 2: + return float("-inf") + if special == 3: + return float("nan") + return rng.uniform(-1.0e12, 1.0e12) + if kind == "tag": + return cbor2.CBORTag(60000 + rng.randrange(1000), random_value(rng, depth + 1)) + if kind == "array": + return [random_value(rng, depth + 1) for _ in range(rng.randrange(6))] + + result: dict[Any, Any] = {} + target = rng.randrange(6) + while len(result) < target: + result[random_key(rng)] = random_value(rng, depth + 1) + return result + + +def json_repr(value: Any) -> Any: + if value is cbor2.undefined: + return {"type": "undefined"} + if isinstance(value, cbor2.CBORSimpleValue): + return {"type": "simple", "value": value.value} + if isinstance(value, cbor2.CBORTag): + return {"type": "tag", "tag": value.tag, "value": json_repr(value.value)} + if isinstance(value, bytes): + return {"type": "bytes", "hex": value.hex()} + if isinstance(value, float): + if math.isnan(value): + return {"type": "float", "value": "nan"} + if math.isinf(value): + return {"type": "float", "value": "+inf" if value > 0 else "-inf"} + if value == 0.0 and math.copysign(1.0, value) < 0: + return {"type": "float", "value": "-0"} + return value + if isinstance(value, dict): + return { + "type": "map", + "pairs": [[json_repr(key), json_repr(item)] for key, item in value.items()], + } + if isinstance(value, (list, tuple)): + return [json_repr(item) for item in value] + return value + + +def main() -> int: + parser = argparse.ArgumentParser() + parser.add_argument("output", type=Path) + parser.add_argument("--seed", type=lambda value: int(value, 0), default=DEFAULT_SEED) + parser.add_argument("--count", type=int, default=512) + args = parser.parse_args() + + actual_version = distribution_version("cbor2") + if actual_version != VERSION: + parser.error(f"expected cbor2 {VERSION}, found {actual_version}") + + rng = random.Random(args.seed) + vectors = [] + for index in range(args.count): + value = random_value(rng) + default_wire = cbor2.dumps(value) + canonical_wire = cbor2.dumps(value, canonical=True) + indefinite_wire = cbor2.dumps(value, indefinite_containers=True) + vectors.append({ + "index": index, + "value": json_repr(value), + "default_hex": default_wire.hex(), + "canonical_hex": canonical_wire.hex(), + "indefinite_hex": indefinite_wire.hex(), + }) + + document = { + "generator": "cbor2", + "generator_version": actual_version, + "seed": args.seed, + "count": args.count, + "vectors": vectors, + } + args.output.write_text( + json.dumps(document, indent=2, ensure_ascii=False, sort_keys=True) + "\n", + encoding="utf-8", + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/test/scapy/layers/requirements-cbor2.txt b/test/scapy/layers/requirements-cbor2.txt new file mode 100644 index 00000000000..5b05ab19467 --- /dev/null +++ b/test/scapy/layers/requirements-cbor2.txt @@ -0,0 +1,3 @@ +# Optional interoperability-test dependency. Not required by Scapy itself. +# cbor2 6.1.4 requires Python 3.10 or newer. +cbor2==6.1.4 diff --git a/tox.ini b/tox.ini index 495672a8e9d..543ebbc323f 100644 --- a/tox.ini +++ b/tox.ini @@ -33,7 +33,6 @@ deps = cryptography coverage[toml] python-can - cbor2 scapy-rpc # disabled on windows because they require c++ dependencies # brotli 1.1.0 broken https://github.com/google/brotli/issues/1072 @@ -101,6 +100,17 @@ commands = sphinx-apidoc -f --no-toc -d 1 --separate --module-first --templatedir=_templates --output-dir api ../../scapy ../../scapy/modules/voip.py ../../scapy/modules/krack/ ../../scapy/libs/winpcapy.py ../../scapy/libs/ethertypes.py ../../scapy/libs/bluetoothids.py ../../scapy/libs/m*.py ../../scapy/libs/structures.py ../../scapy/libs/test_pyx.py ../../scapy/tools/ ../../scapy/arch/ ../../scapy/contrib/scada/* ../../scapy/contrib/igmp.py ../../scapy/contrib/igmpv3.py ../../scapy/layers/msrpce/raw/ ../../scapy/layers/msrpce/all.py ../../scapy/all.py ../../scapy/layers/all.py ../../scapy/compat.py +[testenv:cbor2] +description = "CBOR differential tests against pinned cbor2 6.1.4 (Python >= 3.10)" +basepython = python3.12 +deps = + cbor2==6.1.4 + coverage[toml] +commands = + {envpython} {env:DISABLE_COVERAGE:-m coverage run} -m scapy.tools.UTscapy \ + -t test/scapy/layers/cbor_cbor2_interop.uts -N {posargs} + + [testenv:mypy] description = "Check Scapy compliance against static typing" skip_install = true