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/README.md b/README.md index 559b8ee5596..83c347e08d9 100644 --- a/README.md +++ b/README.md @@ -92,8 +92,7 @@ follow the instructions to install them. ## Packaging status -[![Packaging status](https://repology.org/badge/vertical-allrepos/scapy.svg?columns=4&exclude_unsupported=1&header= -)](https://repology.org/project/scapy/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/scapy.svg?columns=4&exclude_unsupported=1)](https://repology.org/project/scapy/versions) ## License diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index dcec5d8ed5d..1d9574d7c1a 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -21,15 +21,21 @@ CBOR_TEXT_STRING, CBOR_ARRAY, CBOR_MAP, + CBORMapData, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, CBOR_FALSE, CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, + CBOR_UNDEFINED_VALUE, + CBOR_NO_ITEM, CBOR_FLOAT, + CBORFloatValue, CBOR_DECODING_ERROR, RandCBORObject, + CBORTagValue, + CBORSimpleValue, ) from scapy.cbor.cborcodec import ( @@ -45,8 +51,12 @@ ) from scapy.cbor.cborfields import ( + CBORBuildResult, + CBORParseResult, CBORF_element, CBORF_field, + CBORF_ANY, + CBOR_ABSENT, CBORF_UNSIGNED_INTEGER, CBORF_NEGATIVE_INTEGER, CBORF_INTEGER, @@ -56,12 +66,19 @@ CBORF_NULL, CBORF_UNDEFINED, CBORF_FLOAT, + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, CBORF_ARRAY, CBORF_ARRAY_OF, + CBORF_ARRAY_INDEFINITE, CBORF_MAP, CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_ENUM, + CBORF_UNSIGNED_FLAGS, CBORF_optional, + CBORF_CONDITIONAL, CBORF_PACKET, + CBORF_BYTE_STRING_PACKET, ) __all__ = [ @@ -81,14 +98,20 @@ "CBOR_TEXT_STRING", "CBOR_ARRAY", "CBOR_MAP", + "CBORMapData", "CBOR_SEMANTIC_TAG", "CBOR_SIMPLE_VALUE", "CBOR_FALSE", "CBOR_TRUE", "CBOR_NULL", "CBOR_UNDEFINED", + "CBOR_UNDEFINED_VALUE", + "CBOR_NO_ITEM", "CBOR_FLOAT", + "CBORFloatValue", "CBOR_DECODING_ERROR", + "CBORTagValue", + "CBORSimpleValue", # Random/Fuzzing "RandCBORObject", # Codec classes @@ -101,9 +124,14 @@ "CBORcodec_MAP", "CBORcodec_SEMANTIC_TAG", "CBORcodec_SIMPLE_AND_FLOAT", + # Result types + "CBORBuildResult", + "CBORParseResult", # Field base classes "CBORF_element", "CBORF_field", + "CBORF_ANY", + "CBOR_ABSENT", # Scalar fields "CBORF_UNSIGNED_INTEGER", "CBORF_NEGATIVE_INTEGER", @@ -115,11 +143,18 @@ "CBORF_UNDEFINED", "CBORF_FLOAT", # Structured fields + "CBORF_SEQUENCE", + "CBORF_SEQUENCE_OF", "CBORF_ARRAY", "CBORF_ARRAY_OF", + "CBORF_ARRAY_INDEFINITE", "CBORF_MAP", "CBORF_SEMANTIC_TAG", # Complex fields + "CBORF_UNSIGNED_ENUM", + "CBORF_UNSIGNED_FLAGS", "CBORF_optional", + "CBORF_CONDITIONAL", "CBORF_PACKET", + "CBORF_BYTE_STRING_PACKET", ] diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 1dcff4943f1..1b700217ebc 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -7,7 +7,10 @@ Following the ASN.1 paradigm """ +import copy +import math import random +import struct from typing import ( Any, Dict, @@ -296,11 +299,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 +350,26 @@ 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 + + def __hash__(self): + # type: () -> int + try: + return hash((type(self), self.val)) + except TypeError: + return hash((type(self), id(self))) ####################### @@ -368,6 +391,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 +417,202 @@ 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. + + Preserves full CBOR key objects for faithful ``enc()`` round-trips while + still supporting ``map_data['name']`` / ``'name' in map_data`` for the + common scalar-key cases used by existing tests. + + Lookup uses ``(type(key), key)`` identity so CBOR/Python values that + compare equal under ``==`` but differ by type (``1`` vs ``True``) remain + distinct. + """ + + __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 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()) + + @staticmethod + def _float_key_identity(val, encoded=None): + # type: (float, Optional[bytes]) -> Tuple[Any, ...] + """Identity that distinguishes +0.0 / -0.0 and NaN payloads.""" + fval = float(val) + if math.isnan(fval): + if encoded is not None: + return (float, "nan", bytes(encoded)) + return (float, "nan", struct.pack(">d", fval)) + # struct.pack preserves the IEEE sign bit so +0.0 != -0.0. + return (float, "f", struct.pack(">d", fval)) + + @staticmethod + def _key_identity(key): + # type: (Any) -> Tuple[Any, ...] + """Return a typed identity for map-key lookup.""" + if isinstance(key, CBOR_Object): + # Normalize CBOR wrappers to the native Python type they encode. + if isinstance(key, (CBOR_TRUE, CBOR_FALSE)): + return (bool, bool(key.val)) + if isinstance(key, CBOR_NULL): + return (type(None), None) + if isinstance(key, CBOR_UNDEFINED): + from scapy.cbor.cbor import CBOR_UNDEFINED_VALUE + return (type(CBOR_UNDEFINED_VALUE), CBOR_UNDEFINED_VALUE) + if isinstance(key, CBOR_UNSIGNED_INTEGER): + return (int, int(key.val)) + if isinstance(key, CBOR_NEGATIVE_INTEGER): + return (int, int(key.val)) + if isinstance(key, CBOR_FLOAT): + return CBORMapData._float_key_identity( + key.val, getattr(key, "_encoded", None) + ) + if isinstance(key, CBOR_BYTE_STRING): + return (bytes, bytes(key.val)) + if isinstance(key, CBOR_TEXT_STRING): + return (str, str(key.val)) + if isinstance(key, CBOR_ARRAY): + return (list, key) + if isinstance(key, CBOR_MAP): + return (CBORMapData, key) + if isinstance(key, CBOR_SEMANTIC_TAG): + return (CBOR_SEMANTIC_TAG, key.val) + if isinstance(key, CBOR_SIMPLE_VALUE): + return (CBOR_SIMPLE_VALUE, key.val) + return (type(key), key.val) + # bool is a subclass of int; float includes CBORFloatValue. + if isinstance(key, bool): + return (bool, key) + if isinstance(key, float): + encoded = getattr(key, "cbor_encoded", None) + return CBORMapData._float_key_identity(key, encoded) + if isinstance(key, int): + return (int, key) + return (type(key), key) + + 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 + want = self._key_identity(key) + matches = [] # type: List[Any] + for map_key, value in self._pairs: + if self._key_identity(map_key) == want: + 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. + if len(other) != len(self._pairs): + return False + other_items = list(other.items()) + used = [False] * len(other_items) + for map_key, value in self._pairs: + want = self._key_identity(map_key) + matched = False + for idx, (other_key, other_value) in enumerate(other_items): + if used[idx]: + continue + if self._key_identity(other_key) != want: + continue + if value != other_value: + return False + used[idx] = True + matched = True + break + if not matched: + return False + return True + if isinstance(other, CBORMapData): + return self._pairs == other._pairs + return NotImplemented + + def __repr__(self): + # type: () -> str + return "CBORMapData(%r)" % (self.items(),) + + +class CBOR_MAP(CBOR_Object[Any]): + """CBOR map (major type 5). + + Decoded maps use :class:`CBORMapData` (ordered pairs). Manually + constructed maps may still use a plain ``dict``. + """ tag = CBOR_MajorTypes.MAP def strshow(self, lvl=0): # type: (int) -> str s = (" " * lvl) + ("# CBOR_MAP:") + "\n" - for k, v in self.val.items(): + if isinstance(self.val, CBORMapData): + items = self.val.cbor_pairs() + elif isinstance(self.val, dict): + items = list(self.val.items()) + else: + items = list(self.val) + for k, v in items: s += (" " * (lvl + 1)) + "Key: " if hasattr(k, 'strshow'): s += k.strshow(0).strip() + "\n" @@ -456,10 +672,143 @@ def __init__(self): super(CBOR_UNDEFINED, self).__init__(None) +class CBORTagValue(object): + """Packet-field internal representation of a CBOR semantic tag.""" + __slots__ = ("tag", "value") + + def __init__(self, tag, value): + # type: (int, Any) -> None + self.tag = int(tag) + self.value = value + + def __repr__(self): + # type: () -> str + return "CBORTagValue(tag=%r, value=%r)" % (self.tag, self.value) + + def __eq__(self, other): + # type: (object) -> bool + return ( + isinstance(other, CBORTagValue) and + self.tag == other.tag and + self.value == other.value + ) + + def __hash__(self): + # type: () -> int + return hash((self.tag, self.value)) + + +class CBORSimpleValue(object): + """Packet-field internal representation of a CBOR simple value.""" + __slots__ = ("value",) + + def __init__(self, value): + # type: (int) -> None + self.value = int(value) + + def __repr__(self): + # type: () -> str + return "CBORSimpleValue(%r)" % self.value + + def __eq__(self, other): + # type: (object) -> bool + return isinstance(other, CBORSimpleValue) and self.value == other.value + + def __hash__(self): + # type: () -> int + return hash(self.value) + + +class _CBORUndefined(object): + """Sentinel for CBOR undefined (distinct from Python ``None`` / null).""" + + def __repr__(self): + # type: () -> str + return "CBOR_UNDEFINED" + + def __bool__(self): + # type: () -> bool + return False + + def __copy__(self): + # type: () -> _CBORUndefined + return self + + def __deepcopy__(self, memo): + # type: (dict) -> _CBORUndefined + return self + + +CBOR_UNDEFINED_VALUE = _CBORUndefined() + + +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 enc(self, codec=None): + # type: (Any) -> bytes + if self._encoded is not None: + return self._encoded + return super(CBOR_FLOAT, self).enc(codec) + + +class CBORFloatValue(float): + """Native float that optionally retains the exact CBOR encoding. + + Used by :class:`~scapy.cbor.cborfields.CBORF_FLOAT` and + :class:`~scapy.cbor.cborfields.CBORF_ANY` so dissected half / single / + double (and NaN payloads) survive field storage and rebuild when the + packet raw cache is cleared, until the value is replaced by a plain + ``float``. + """ + + __slots__ = ("_cbor_encoded",) + + def __new__(cls, value, encoded=None): + # type: (float, Optional[bytes]) -> CBORFloatValue + self = float.__new__(cls, value) + object.__setattr__(self, "_cbor_encoded", encoded) + return self + + @property + def cbor_encoded(self): + # type: () -> Optional[bytes] + return getattr(self, "_cbor_encoded", None) + + def __copy__(self): + # type: () -> CBORFloatValue + return CBORFloatValue(float(self), self.cbor_encoded) + + def __deepcopy__(self, memo): + # type: (dict) -> CBORFloatValue + return self.__copy__() + 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..5ad5dbe5a79 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -35,6 +35,9 @@ from scapy.error import log_runtime +MAX_CBOR_NESTING = 128 + + ################## # CBOR encoding # ################## @@ -44,6 +47,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 @@ -74,6 +81,15 @@ def CBOR_encode_head(major_type, value): Encode CBOR initial byte and additional info. Format: 3 bits major type + 5 bits additional info """ + if value is None: + raise CBOR_Codec_Encoding_Error( + "Indefinite length requires CBOR_encode_indefinite_head") + 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 > 0xFFFFFFFFFFFFFFFF: + 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) @@ -91,14 +107,56 @@ def CBOR_encode_head(major_type, value): return chb((major_type << 5) | 27) + struct.pack(">Q", value) +def CBOR_encode_indefinite_head(major_type): + # type: (int) -> bytes + """Encode a CBOR indefinite-length header (additional info 31).""" + if major_type not in (2, 3, 4, 5): + raise CBOR_Codec_Encoding_Error( + "Indefinite length not allowed for major type %d" % major_type + ) + return chb((major_type << 5) | 31) + + +def CBOR_encode_break(): + # type: () -> bytes + """Encode the CBOR break stop code (0xff).""" + return b'\xff' + + +def _cbor_buf_bytes(buf): + # type: (Any) -> bytes + """Materialize a bytes/memoryview slice as ``bytes``.""" + if isinstance(buf, bytes): + return buf + if isinstance(buf, memoryview): + return buf.tobytes() + return bytes(buf) + + +def cbor_is_break(s): + # type: (Any) -> bool + """Return whether *s* begins with a CBOR break byte.""" + return bool(s) and s[0] == 0xff + + +def cbor_consume_break(s): + # type: (Any) -> Any + """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_decode_head(s): - # type: (bytes) -> Tuple[int, int, bytes] + # type: (Any) -> Tuple[int, Union[int, CBOR_INDEFINITE], Any] """ 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 @@ -111,32 +169,360 @@ def CBOR_decode_head(s): # 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: # 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: # 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: # 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 == 31: + if major_type in (0, 1, 6): + raise CBOR_Codec_Decoding_Error( + "Indefinite length not allowed for major type %d" % + major_type, remaining=_cbor_buf_bytes(s)) + if major_type in (2, 3, 4, 5): + 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 (28, 29, 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_argument_is_shortest(additional_info, value): + # type: (int, Union[int, CBOR_INDEFINITE]) -> bool + """Return True when *additional_info* is the shortest encoding for *value*.""" + if value is CBOR_INDEFINITE: + return additional_info == 31 + if additional_info < 24: + return True + if additional_info == 24: + return value >= 24 + if additional_info == 25: + return value >= 256 + if additional_info == 26: + return value >= 65536 + if additional_info == 27: + return value >= (1 << 32) + return additional_info == 31 + + +def _cbor_float_from_bits(ai, bits): + # type: (int, int) -> float + if ai == 25: + sign = (bits >> 15) & 0x1 + exponent = (bits >> 10) & 0x1f + fraction = bits & 0x3ff + if exponent == 0: + if fraction == 0: + return -0.0 if sign else 0.0 + return ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) + if exponent == 31: + return float("nan") if fraction else ( + float("-inf") if sign else float("inf") + ) + return ((-1) ** sign) * (1.0 + fraction / 1024.0) * (2 ** (exponent - 15)) + if ai == 26: + 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 + if value == 0.0: + return sign + value = abs(value) + bits64, = struct.unpack(">Q", struct.pack(">d", value)) + exp64 = ((bits64 >> 52) & 0x7FF) - 1023 + mant64 = bits64 & ((1 << 52) - 1) + if exp64 > 15: + return None + if exp64 < -14: + # Subnormal half + shift = -14 - exp64 + 42 # 52 - 10 + if shift > 52: + return None + mant = ((mant64 | (1 << 52)) >> shift) if exp64 != -1023 else 0 + half = mant & 0x3FF + preferred = math.copysign(value, -1.0 if sign else 1.0) + if _cbor_float_from_bits(25, sign | half) != preferred: + # Compare absolute then restore sign via copysign on left side + decoded = _cbor_float_from_bits(25, sign | half) + if decoded != math.copysign(abs(value), -1.0 if sign else 1.0): + return None + return sign | half + half_exp = exp64 + 15 + half_mant = mant64 >> 42 + # Reject if discarded mantissa bits are nonzero (not exact). + if mant64 & ((1 << 42) - 1): + return None + bits = sign | (half_exp << 10) | half_mant + decoded = _cbor_float_from_bits(25, bits) + if decoded != math.copysign(abs(value), -1.0 if sign else 1.0): + 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. + """ + if ai == 25: + return 25 + if ai == 26: + # 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 25 + return 26 + if ai == 27: + # binary64 NaN: 1+11+52. + mant = int(bits) & ((1 << 52) - 1) + if mant == 0: + # Infinity, not NaN — caller should not use this helper. + return 27 + # Prefer half when only the top 10 significand bits are used. + if (mant & ((1 << 42) - 1)) == 0: + return 25 + # Prefer single when only the top 23 significand bits are used. + if (mant & ((1 << 29) - 1)) == 0: + return 26 + return 27 + return ai + + +def _cbor_preferred_float_ai(value): + # type: (float) -> int + """Return the preferred float AI (25/26/27) for a numeric *value*.""" + import math + 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 25 + if _cbor_float_to_half_bits(value) is not None: + return 25 + try: + single = struct.unpack(">f", struct.pack(">f", value))[0] + except (OverflowError, struct.error): + return 27 + if single == value or (math.isinf(single) and math.isinf(value)): + return 26 + return 27 + + +def _cbor_preferred_float_ai_from_encoded(ai, bits): + # type: (int, int) -> int + """Preferred float AI using the original encoded width and bit pattern.""" + import math + float_val = _cbor_float_from_bits(ai, bits) + if math.isnan(float_val): + return _cbor_nan_preferred_ai(ai, bits) + return _cbor_preferred_float_ai(float_val) + + +def cbor_find_non_deterministic(s, allow_indefinite=True, base_offset=0): + # type: (bytes, bool, int) -> List[Tuple[int, str]] + """Scan *s* for non-shortest CBOR argument encodings. + + Returns a list of ``(absolute_offset, message)`` issues. Indefinite-length + items are accepted only when *allow_indefinite* is true (e.g. a BPv7 + bundle outer array). Callers that require definite-length encoding + (primary/canonical blocks per RFC 9171) must pass ``False``. + """ + issues = [] # type: List[Tuple[int, str]] + index = [0] + + def _walk(): + # type: () -> None + start = index[0] + if start >= len(s): + raise CBOR_Codec_Decoding_Error( + "Empty CBOR data", remaining=s[start:]) + initial = s[start] + if initial == 0xff: + 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 == 24: + 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 == 25: + 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 == 26: + 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 == 27: + 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 == 31: + value = CBOR_INDEFINITE + 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 == 7: + if ai == 24 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 (25, 26, 27) and value is not CBOR_INDEFINITE: + preferred = _cbor_preferred_float_ai_from_encoded(ai, int(value)) + if preferred is not None and 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 (2, 3): + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + _walk() + 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 == 4: + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + _walk() + 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 == 5: + key_encodings = [] # type: List[bytes] + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + key_start = index[0] + _walk() + key_encodings.append(bytes(s[key_start:index[0]])) + _walk() + 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 cbor_argument_is_shortest(ai, value): + issues.append(( + base_offset + start, + "Non-shortest CBOR argument encoding (AI=%d, value=%r)" + % (ai, value), + )) + + if major in (2, 3): + 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 == 4: + for _ in range(int(value)): + _walk() + return + if major == 5: + key_encodings = [] # type: List[bytes] + for _ in range(int(value)): + key_start = index[0] + _walk() + key_encodings.append(bytes(s[key_start:index[0]])) + _walk() + if key_encodings != sorted(key_encodings): + issues.append(( + base_offset + start, + "CBOR map keys are not in bytewise lexicographic order", + )) + return + if major == 6: + _walk() + return + + try: + _walk() + except CBOR_Codec_Decoding_Error: + # Malformed input is reported by normal decoding, not this checker. + pass + return issues # [ CBOR codec classes ] # @@ -189,7 +575,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 _decode_cbor_item(s, safe=False, depth=_depth) @classmethod def dec(cls, @@ -199,10 +585,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: @@ -244,6 +631,9 @@ def enc(cls, obj): raise CBOR_Codec_Encoding_Error( "Cannot encode negative value as unsigned integer. " "Use CBOR_NEGATIVE_INTEGER for negative values.") + if i > 0xFFFFFFFFFFFFFFFF: + raise CBOR_Codec_Encoding_Error( + "Unsigned integer exceeds uint64 range") return CBOR_encode_head(0, i) @classmethod @@ -276,6 +666,9 @@ 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 < -(1 << 64): + raise CBOR_Codec_Encoding_Error( + "Negative integer below CBOR int64 range") # CBOR negative integer: -1 - n return CBOR_encode_head(1, -1 - i) @@ -324,11 +717,36 @@ def do_dec(cls, 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 != 2: + 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]): @@ -360,15 +778,44 @@ def do_dec(cls, 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 != 3: + 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 +828,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(4, len(array))] + parts.extend( + CBORcodec_Object.encode_cbor_item(item) + for item in array + ) + return b"".join(parts) @classmethod def do_dec(cls, @@ -402,31 +851,54 @@ def do_dec(cls, 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, safe=False, 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, safe=False, 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, CBORMapData 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 + if isinstance(mapping, CBORMapData): + pairs = mapping.cbor_pairs() + elif isinstance(mapping, dict): + pairs = list(mapping.items()) + else: + pairs = list(mapping) + parts = [CBOR_encode_head(5, 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,7 +907,8 @@ 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: @@ -443,26 +916,53 @@ def do_dec(cls, "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_keys = set() # type: set[bytes] + + def _add_pair(key, value): + # type: (Any, Any) -> None + # CBOR_FLOAT preserves received wire bytes in enc(), so distinct + # float/NaN encodings remain distinct while semantic duplicates + # (e.g. 1 vs 0x18 0x01) still collapse via preferred encoding. + key_wire = CBORcodec_Object.encode_cbor_item(key) + if key_wire in seen_keys: 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_keys.add(key_wire) + 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, safe=False, 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, safe=False, 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, safe=False, 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, safe=False, 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 +975,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 > 0xFFFFFFFFFFFFFFFF: + raise CBOR_Codec_Encoding_Error( + "Semantic tag number out of uint64 range") + return ( + CBOR_encode_head(6, tag_num) + + CBORcodec_Object.encode_cbor_item(item) + ) @classmethod def do_dec(cls, @@ -499,7 +1003,7 @@ def do_dec(cls, "Tag without following item", remaining=s) item, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=safe) + remainder, safe=False, depth=_depth + 1) return cls.cbor_object((tag_num, item)), remainder @@ -536,11 +1040,26 @@ def enc(cls, obj): elif val is None: return chb(0xf6) # null elif isinstance(val, float): - # Encode as double precision (8 bytes) + # 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 == 25: + half = _cbor_float_to_half_bits(val) + if half is not None: + return chb(0xf9) + struct.pack(">H", half) + ai = 26 + if ai == 26: + try: + return chb(0xfa) + struct.pack(">f", val) + except (OverflowError, struct.error): + pass return chb(0xfb) + struct.pack(">d", val) elif isinstance(val, int) and 0 <= val <= 23: # Simple value 0-23 return CBOR_encode_head(7, val) + elif isinstance(val, int) and 32 <= val <= 255: + return b"\xf8" + chb(val) else: raise CBOR_Codec_Encoding_Error( "Cannot encode value as simple/float: %r" % val) @@ -615,21 +1134,21 @@ def do_dec(cls, (1 + fraction / 1024.0) * (2 ** (exponent - 15))) - return CBOR_FLOAT(float_val), remainder + return CBOR_FLOAT(float_val, encoded=_cbor_buf_bytes(s[:3])), 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:] + return CBOR_FLOAT(float_val, encoded=_cbor_buf_bytes(s[:5])), s[5:] elif additional_info == 27: # Double precision float (8 bytes) if len(s) < 9: 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:] + return CBOR_FLOAT(float_val, encoded=_cbor_buf_bytes(s[:9])), s[9:] elif additional_info < 24: # Simple value 0-23 return CBOR_SIMPLE_VALUE(additional_info), s[1:] @@ -639,7 +1158,13 @@ def do_dec(cls, 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, @@ -652,10 +1177,29 @@ def do_dec(cls, def _encode_cbor_item(item): # type: (Any) -> bytes """Encode a Python value to CBOR bytes""" - from scapy.cbor.cbor import CBOR_Object + from scapy.cbor.cbor import ( + CBOR_Object, + CBOR_UNDEFINED, + CBOR_UNDEFINED_VALUE, + CBORMapData, + CBORTagValue, + CBORSimpleValue, + CBOR_SIMPLE_VALUE, + ) if isinstance(item, CBOR_Object): return item.enc() + elif item is CBOR_UNDEFINED_VALUE: + return CBOR_UNDEFINED().enc() + elif isinstance(item, CBORTagValue): + return ( + CBOR_encode_head(6, item.tag) + + _encode_cbor_item(item.value) + ) + elif isinstance(item, CBORSimpleValue): + return CBORcodec_SIMPLE_AND_FLOAT.enc(CBOR_SIMPLE_VALUE(item.value)) + 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) @@ -673,6 +1217,9 @@ def _encode_cbor_item(item): elif isinstance(item, dict): return CBORcodec_MAP.enc(item) elif isinstance(item, float): + encoded = getattr(item, "cbor_encoded", None) + if encoded is not None: + return encoded return CBORcodec_SIMPLE_AND_FLOAT.enc(item) elif item is None: return CBORcodec_SIMPLE_AND_FLOAT.enc(None) @@ -681,37 +1228,159 @@ def _encode_cbor_item(item): "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""" +def _encode_cbor_map_deterministic(pairs): + # type: (Any) -> bytes + """Encode map pairs in RFC 8949 core-deterministic key order.""" + encoded_pairs = [] # type: List[Tuple[bytes, bytes]] + for key, value in pairs: + key_bytes = _encode_cbor_item_deterministic(key) + value_bytes = _encode_cbor_item_deterministic(value) + encoded_pairs.append((key_bytes, value_bytes)) + encoded_pairs.sort(key=lambda item: item[0]) + parts = [CBOR_encode_head(5, len(encoded_pairs))] + for key_bytes, value_bytes in encoded_pairs: + parts.append(key_bytes) + parts.append(value_bytes) + return b"".join(parts) + + +def _encode_cbor_item_deterministic(item): + # type: (Any) -> bytes + """Encode a Python value using RFC 8949 core-deterministic rules. + + Unlike :func:`_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` wrappers are accepted and reduced to + native values (preferred float encoding, deterministic nested maps). + """ + from scapy.cbor.cbor import ( + CBOR_Object, + CBOR_ARRAY, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_UNDEFINED, + CBOR_UNDEFINED_VALUE, + CBORMapData, + CBORTagValue, + CBORSimpleValue, + ) + + if isinstance(item, CBOR_Object): + if isinstance(item, CBOR_UNDEFINED): + return CBOR_UNDEFINED().enc() + if isinstance(item, CBOR_ARRAY): + return _encode_cbor_item_deterministic(list(item.val)) + if isinstance(item, CBOR_MAP): + if isinstance(item.val, CBORMapData): + return _encode_cbor_map_deterministic(item.val.cbor_pairs()) + if isinstance(item.val, list): + return _encode_cbor_map_deterministic(item.val) + return _encode_cbor_map_deterministic(list(item.val.items())) + if isinstance(item, CBOR_SEMANTIC_TAG): + tag_num, inner = item.val + return ( + CBOR_encode_head(6, tag_num) + + _encode_cbor_item_deterministic(inner) + ) + if isinstance(item, CBOR_SIMPLE_VALUE): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + return _encode_cbor_item_deterministic(item.val) + if item is CBOR_UNDEFINED_VALUE: + return CBOR_UNDEFINED().enc() + if isinstance(item, CBORTagValue): + return ( + CBOR_encode_head(6, item.tag) + + _encode_cbor_item_deterministic(item.value) + ) + if isinstance(item, CBORSimpleValue): + return CBORcodec_SIMPLE_AND_FLOAT.enc(CBOR_SIMPLE_VALUE(item.value)) + if isinstance(item, CBORMapData): + return _encode_cbor_map_deterministic(item.cbor_pairs()) + if isinstance(item, dict): + return _encode_cbor_map_deterministic(list(item.items())) + if isinstance(item, list): + encoded_items = [ + _encode_cbor_item_deterministic(element) for element in item + ] + return CBOR_encode_head(4, len(encoded_items)) + b"".join(encoded_items) + if isinstance(item, bool): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + if isinstance(item, int): + if item >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(item) + return CBORcodec_NEGATIVE_INTEGER.enc(item) + if isinstance(item, bytes): + return CBORcodec_BYTE_STRING.enc(item) + if isinstance(item, str): + return CBORcodec_TEXT_STRING.enc(item) + if isinstance(item, float): + # Preserve dissected wire (e.g. NaN payloads) when known; otherwise + # fall back to preferred-width encoding. + encoded = getattr(item, "cbor_encoded", None) + if encoded is not None: + return encoded + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + if item is None: + return CBORcodec_SIMPLE_AND_FLOAT.enc(None) + raise CBOR_Codec_Encoding_Error( + "Cannot deterministically encode type: %s" % type(item) + ) + + +def _decode_cbor_item(s, safe=False, depth=0): + # type: (Any, bool, int) -> Tuple[CBOR_Object[Any], Any] + """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 = _decode_cbor_item(memoryview(s), safe=False, 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=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 == 0: - return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=safe) + return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=False, _depth=depth) elif major_type == 1: - return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=safe) + return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=False, _depth=depth) elif major_type == 2: - return CBORcodec_BYTE_STRING.dec(s, safe=safe) + return CBORcodec_BYTE_STRING.dec(s, safe=False, _depth=depth) elif major_type == 3: - return CBORcodec_TEXT_STRING.dec(s, safe=safe) + return CBORcodec_TEXT_STRING.dec(s, safe=False, _depth=depth) elif major_type == 4: - return CBORcodec_ARRAY.dec(s, safe=safe) + return CBORcodec_ARRAY.dec(s, safe=False, _depth=depth) elif major_type == 5: - return CBORcodec_MAP.dec(s, safe=safe) + return CBORcodec_MAP.dec(s, safe=False, _depth=depth) elif major_type == 6: - return CBORcodec_SEMANTIC_TAG.dec(s, safe=safe) + return CBORcodec_SEMANTIC_TAG.dec(s, safe=False, _depth=depth) elif major_type == 7: - return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=safe) + return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=False, _depth=depth) else: raise CBOR_Codec_Decoding_Error( - "Invalid major type: %d" % major_type, remaining=s) + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(s)) # Add helper methods to CBORcodec_Object CBORcodec_Object.encode_cbor_item = staticmethod(_encode_cbor_item) +CBORcodec_Object.encode_cbor_item_deterministic = staticmethod( + _encode_cbor_item_deterministic +) CBORcodec_Object.decode_cbor_item = staticmethod(_decode_cbor_item) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 536424728ec..ba9667dc5aa 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -5,32 +5,50 @@ """ 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_result`` / ``dissect_result`` so unframed sequences and array +budgeting can return an item count for raw-cache fidelity; callers outside +this module should prefer ``build`` / ``dissect``. """ import copy -from functools import reduce +from dataclasses import dataclass from scapy.cbor.cbor import ( CBOR_Decoding_Error, - CBOR_Error, + CBOR_Encoding_Error, CBOR_MajorTypes, CBOR_Object, 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_UNDEFINED_VALUE, + CBOR_NO_ITEM, CBOR_FLOAT, + CBOR_MAP, + CBOR_SIMPLE_VALUE, + CBORTagValue, + CBORSimpleValue, ) from scapy.cbor.cborcodec import ( CBOR_Codec_Decoding_Error, + CBOR_INDEFINITE, CBOR_decode_head, CBOR_encode_head, + CBOR_encode_indefinite_head, + CBOR_encode_break, + cbor_is_break, + cbor_consume_break, CBORcodec_Object, CBORcodec_UNSIGNED_INTEGER, CBORcodec_NEGATIVE_INTEGER, @@ -38,7 +56,8 @@ CBORcodec_TEXT_STRING, CBORcodec_SIMPLE_AND_FLOAT, ) -from scapy.base_classes import BasePacket +from scapy.error import log_runtime +from scapy.packet import Packet from scapy.volatile import ( RandChoice, RandFloat, @@ -47,10 +66,11 @@ RandField, ) -from scapy import packet +from scapy import packet, fields, config from typing import ( Any, + Callable, Dict, Generic, List, @@ -71,8 +91,170 @@ 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 cbor_item_span(s): + # type: (bytes) -> Tuple[bytes, bytes] + """Split *s* into the first well-formed CBOR item and the remainder.""" + _obj, remain = CBORcodec_Object.decode_cbor_item(s) + if remain: + return s[:-len(remain)], remain + return s, b"" + + +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. + + Used by packet-valued fields so Raw/bytes/Packet fallbacks cannot claim + ``items=1`` while emitting multiple or malformed CBOR items. + """ + if hasattr(val, "cbor_build_result"): + result = val.cbor_build_result() + if result.items != 1: + raise CBOR_Encoding_Error( + "%s must encode exactly one top-level CBOR item, " + "but encoded %d" + % (getattr(type(val), "__name__", context), result.items) + ) + data = result.data + else: + data = bytes(val) + try: + item, remaining = cbor_item_span(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 + ) + if item != data: + raise CBOR_Encoding_Error( + "%s encoded a CBOR item that does not cover the full payload" + % 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 + + +def _cbor_packet_from_bytes(cls, data, parent): + # type: (Type[Packet], bytes, Optional[Packet]) -> Packet + """Instantiate a nested packet with Scapy field-parent ownership.""" + return cls(data, _parent=parent) # type: ignore + + +def cbor_object_to_python(obj): + # type: (Any) -> Any + """Convert a :class:`CBOR_Object` tree to native Python values.""" + if not isinstance(obj, CBOR_Object): + return obj + if isinstance(obj, CBOR_UNDEFINED): + return CBOR_UNDEFINED_VALUE + if isinstance(obj, CBOR_ARRAY): + return [cbor_object_to_python(item) for item in obj.val] + if isinstance(obj, CBOR_MAP): + # Preserve an explicit map wrapper so rebuild cannot confuse maps + # with arrays of pairs. + from scapy.cbor.cbor import CBORMapData + if isinstance(obj.val, CBORMapData): + pairs = obj.val.cbor_pairs() + elif isinstance(obj.val, list): + pairs = obj.val + else: + pairs = list(obj.val.items()) + return CBORMapData([ + (cbor_object_to_python(k), cbor_object_to_python(v)) + for k, v in pairs + ]) + if isinstance(obj, CBOR_SEMANTIC_TAG): + tag_num, item = obj.val + return CBORTagValue(tag_num, cbor_object_to_python(item)) + if isinstance(obj, CBOR_SIMPLE_VALUE): + return CBORSimpleValue(obj.val) + if isinstance(obj, CBOR_FLOAT): + from scapy.cbor.cbor import CBORFloatValue + return CBORFloatValue(obj.val, encoded=getattr(obj, "_encoded", None)) + return obj.val + + class CBORF_element(object): - pass + """Base class for CBOR packet field elements.""" + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + data = self.build(pkt) + return CBORBuildResult(data, self.min_items(pkt)) + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + remaining = self.dissect(pkt, s) + return CBORParseResult(remaining=remaining, items=self.max_items(pkt)) + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + raise NotImplementedError + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + raise NotImplementedError + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 ########################## @@ -80,35 +262,29 @@ 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 + allows_none = 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 +298,170 @@ def i2h(self, pkt, x): # type: (CBOR_Packet, _I) -> Any return x - def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[_A, bytes] - raise NotImplementedError("Subclasses must implement m2i") + def h2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> _I + return cast(_I, x) - 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 m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[_I, bytes] + raise NotImplementedError( + "Subclasses must implement m2i for %s" % type(self)) - def _encode(self, x): + def encode_value(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. + + Prefer overriding :meth:`i2m` in new code; ``encode_value`` remains + the shared leaf encoder used by the default :meth:`i2m`. + """ + raise NotImplementedError( + "Subclasses must implement encode_value for %s" % type(self)) + + def i2m(self, pkt, 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: + item, remaining = cbor_item_span(data) + except Exception as exc: + raise CBOR_Encoding_Error( + "RawVal for %r is not well-formed CBOR: %s" + % (self.name, exc) + ) + if remaining or item != data: + 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_result(). + return self.encode_value(x) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> _I - return cast(_I, x) + if x is CBOR_ABSENT or x is CBOR_UNDEFINED_VALUE or x is CBOR_NO_ITEM: + return cast(_I, x) + if isinstance(x, CBOR_Object): + x = cbor_object_to_python(x) + return self.h2i(pkt, x) def extract_packet(self, cls, # type: Type[CBOR_Packet] s, # type: bytes - _underlayer=None, # type: Optional[CBOR_Packet] + _parent=None, # type: Optional[CBOR_Packet] ): # type: (...) -> Tuple[CBOR_Packet, bytes] try: - c = cls(s, _underlayer=_underlayer) + c = cls(s, _parent=_parent) except CBORF_badsequence: - c = packet.Raw(s, _underlayer=_underlayer) # type: ignore - cpad = c.getlayer(packet.Raw) + c = packet.Raw(s, _parent=_parent) # type: ignore + craw = c.getlayer(config.conf.raw_layer) + cpad = c.getlayer(config.conf.padding_layer) s = b"" + if craw is not None: + s = craw.load + if craw.underlayer: + del craw.underlayer.payload if cpad is not None: s = cpad.load if cpad.underlayer: del cpad.underlayer.payload return c, s + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + val = pkt.getfieldval(self.name) + if val is None: + if self.allows_none: + return CBORBuildResult(b"", 0) + raise CBOR_Encoding_Error( + "Required field %r is None" % self.name) + return CBORBuildResult(self.i2m(pkt, val), 1) + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + val, remain = self.m2i(pkt, s) + self.set_val(pkt, val) + 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 build(self, pkt): # type: (CBOR_Packet) -> bytes - return self.i2m(pkt, getattr(pkt, self.name)) + return self.build_result(pkt).data def dissect(self, pkt, s): # type: (CBOR_Packet, bytes) -> bytes - v, s = self.m2i(pkt, s) - self.set_val(pkt, v) - return s + return self.dissect_result(pkt, s).remaining + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 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_UNDEFINED_VALUE or x is CBOR_NO_ITEM: return x + if isinstance(x, list): + return copy.deepcopy(x) 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) + if val is CBOR_ABSENT: + # Bypass any2i so presence sentinel is stored verbatim. + pkt.fields[self.name] = CBOR_ABSENT + pkt.explicit = 0 + pkt.raw_packet_cache = None + pkt.raw_packet_cache_fields = None + pkt.wirelen = None + return + pkt.setfieldval(self.name, val) 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,192 +473,395 @@ 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 CBORF_ANY(CBORF_field[Any]): + """Represent any well-formed CBOR value, including recursion.""" + ismutable = True + # Treat composites as atomic values so Packet.__iter__/do_build does not + # expand a decoded CBOR array into individual generator elements. + islist = 1 + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + # Python None is CBOR null; only CBOR_ABSENT means "no item". + 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_UNDEFINED_VALUE or x is CBOR_NO_ITEM: + return x + # Deep-copy composites so in-place nested mutations invalidate cache. + return copy.deepcopy(x) + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + val = pkt.getfieldval(self.name) + if val is CBOR_ABSENT: + return CBORBuildResult(b"", 0) + return CBORBuildResult(self.i2m(pkt, val), 1) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + obj, remain = CBORcodec_Object.decode_cbor_item(s) + return cbor_object_to_python(obj), remain + + def encode_value(self, x): + # type: (Any) -> bytes + if x is CBOR_ABSENT: + return b"" + if isinstance(x, CBOR_Object): + x = cbor_object_to_python(x) + 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 > 0xFFFFFFFFFFFFFFFF: + 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_value(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 < -(1 << 64): + 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_value(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 (0, 1) + + 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 < -(1 << 64) or i > 0xFFFFFFFFFFFFFFFF: + 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 + obj, remain = CBORcodec_UNSIGNED_INTEGER.dec(s) + return obj.val, remain elif major_type == 1: - return CBORcodec_NEGATIVE_INTEGER.dec(s) # type: ignore - raise CBOR_Decoding_Error( + 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_value(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]): +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 m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_BYTE_STRING, bytes] - return CBORcodec_BYTE_STRING.dec(s) # type: ignore + 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 _encode(self, x): + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[bytes, bytes] + if self.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 != 2: + 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 encode_value(self, x): # type: (Any) -> bytes - return CBORcodec_BYTE_STRING.enc( - x if isinstance(x, CBOR_Object) else CBOR_BYTE_STRING(bytes(x)) - ) + data = bytes(x) + if self.definite_only: + # Always emit definite form (codec already does). + pass + return CBORcodec_BYTE_STRING.enc(data) 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. + """ + 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 _resolve_packet_class(self, pkt, data): + # type: (CBOR_Packet, bytes) -> Tuple[Optional[Type[Packet]], bool] + if self.pkt_cls is not None: + return self.pkt_cls, True + if self.cls_cb is not None: + pkt_cls = self.cls_cb(pkt, data) + return pkt_cls, pkt_cls is not None + return None, False + + def _decode_packet_value(self, pkt, data): + # type: (CBOR_Packet, bytes) -> Packet + pkt_cls, registered = self._resolve_packet_class(pkt, data) + if pkt_cls is None: + return _cbor_packet_from_bytes(packet.Raw, data, pkt) + try: + return _cbor_packet_from_bytes(pkt_cls, data, pkt) + except Exception as exc: + if registered: + raise CBOR_Decoding_Error( + "Failed to decode registered block-type-specific data: %s" + % exc + ) + log_runtime.exception( + "Failed to decode byte string content to %s", pkt_cls) + return _cbor_packet_from_bytes(packet.Raw, data, pkt) + + 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] + if self.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 != 2: + 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 self._decode_packet_value(pkt, obj.val), remain + + def encode_value(self, x): + # type: (Any) -> bytes + return CBORcodec_BYTE_STRING.enc(bytes(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_value(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) == 7 and ai in (20, 21) + + 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_value(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 + allows_none = True def __init__(self, name, # type: str @@ -398,30 +870,53 @@ 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] == 0xf6 + + 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_value(self, x): + # type: (Any) -> bytes return CBOR_NULL().enc() + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + if pkt.getfieldval(self.name) is CBOR_ABSENT: + return CBORBuildResult(b"", 0) + return CBORBuildResult(self.encode_value(None), 1) + def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return False + return pkt.getfieldval(self.name) is CBOR_ABSENT + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 -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 + allows_none = True def __init__(self, name, # type: str @@ -430,52 +925,110 @@ 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] == 0xf7 + + 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_value(self, x): + # type: (Any) -> bytes return CBOR_UNDEFINED().enc() + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + if pkt.getfieldval(self.name) is CBOR_ABSENT: + return CBORBuildResult(b"", 0) + return CBORBuildResult(self.encode_value(None), 1) + def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return False + return pkt.getfieldval(self.name) is CBOR_ABSENT + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + +class CBORF_FLOAT(CBORF_field[float]): + """CBOR float field (major type 7). -class CBORF_FLOAT(CBORF_field[float, CBOR_FLOAT]): - """CBOR float field (major type 7, double precision).""" + Dissected values retain the received encoding (half / single / double, + including NaN payloads) via :class:`~scapy.cbor.cbor.CBORFloatValue`. + Assigning a plain ``float`` uses preferred serialization on the next + rebuild. + """ 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) == 7 and ai in (25, 26, 27) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> float + from scapy.cbor.cbor import CBORFloatValue + if x is CBOR_ABSENT: + return CBOR_ABSENT # type: ignore + if x is None: + return None # type: ignore + if isinstance(x, CBORFloatValue): + return x + if isinstance(x, CBOR_FLOAT): + return CBORFloatValue(x.val, encoded=x._encoded) + if isinstance(x, CBOR_Object): + return float(cbor_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] + from scapy.cbor.cbor import CBORFloatValue 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 CBORFloatValue(obj.val, encoded=obj._encoded), remain - def i2m(self, pkt, x): - # type: (CBOR_Packet, Any) -> bytes - if x is None: - return b"" + def encode_value(self, x): + # type: (Any) -> bytes + from scapy.cbor.cbor import CBORFloatValue if isinstance(x, CBOR_FLOAT): return x.enc() - return CBORcodec_SIMPLE_AND_FLOAT.enc(CBOR_FLOAT(float(x))) + if isinstance(x, CBORFloatValue) and x.cbor_encoded is not None: + return x.cbor_encoded + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(x)) + + def i2h(self, pkt, x): + # type: (CBOR_Packet, Any) -> Any + if isinstance(x, CBOR_FLOAT): + return x.val + return x + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + if isinstance(x, CBOR_FLOAT): + return repr(x.val) + return repr(x) def randval(self): # type: () -> RandFloat @@ -486,33 +1039,60 @@ def randval(self): # Structured CBOR Fields # ############################## -class CBORF_ARRAY(CBORF_field[List[Any], List[Any]]): +class CBORF_UNSIGNED_ENUM(CBORF_UNSIGNED_INTEGER): """ - 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. + Display like EnumField, codec like CBORF + """ + def __init__(self, + name, # type: str + default, # type: Optional[int] + enum, # type: fields._EnumType[int] + ): + # type: (...) -> None + self._enum = fields.EnumField(name, default, enum, "Q") + CBORF_UNSIGNED_INTEGER.__init__(self, name, default) - Example:: + def i2repr(self, pkt, x): + return self._enum.i2repr(pkt, x) - class MyCBOR(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("version", 1), - CBORF_TEXT_STRING("name", ""), - ) + def any2i(self, pkt, x): + if isinstance(x, CBOR_Object): + x = x.val + x = self._enum.any2i(pkt, x) + return super().any2i(pkt, x) + + +class CBORF_UNSIGNED_FLAGS(CBORF_UNSIGNED_INTEGER): """ - CBOR_tag = CBOR_MajorTypes.ARRAY + Display like FlagsField, codec like CBORF + """ + def __init__(self, + name, # type: str + default, # type: Optional[Union[int, fields.FlagValue]] + size, # type: int + names, # type: Union[List[str], str, Dict[int, str]] + ): + # type: (...) -> None + self._flags = fields.FlagsField(name, default, size, names) + CBORF_UNSIGNED_INTEGER.__init__(self, name, default) + + def i2repr(self, pkt, x): + return self._flags.i2repr(pkt, x) + + def any2i(self, pkt, x): + if isinstance(x, CBOR_Object): + x = x.val + x = self._flags.any2i(pkt, x) + return super().any2i(pkt, x) + + +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,63 +1105,485 @@ 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]] + return [ + child + for field in self.seq + for child in field.get_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_result(pkt) + parts.append(result.data) + total_items += result.items + return b"".join(parts), total_items + + def _mark_absent(self, pkt, field): + # type: (CBOR_Packet, Any) -> None + """Record that an optional/conditional field was not present.""" + if isinstance(field, CBORF_optional): + field._field.set_val(pkt, CBOR_ABSENT) + elif isinstance(field, CBORF_CONDITIONAL): + # Condition false or skipped: leave value untouched. + pass + + def _dissect_children(self, pkt, s, count): + # type: (CBOR_Packet, bytes, Union[int, CBOR_INDEFINITE]) -> bytes + remaining = s + if count is CBOR_INDEFINITE: + # Count items with a memoryview cursor (no suffix copies / span). + if not isinstance(remaining, memoryview): + view = memoryview(remaining) + else: + view = remaining + probe = view + item_count = 0 + while probe and not cbor_is_break(probe): + _obj, probe = CBORcodec_Object.decode_cbor_item(probe) + item_count += 1 + remaining = self._dissect_children_budgeted( + pkt, remaining, item_count + ) + return cbor_consume_break(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``). - """ + return self._dissect_children_budgeted(pkt, remaining, count) + + 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: + raise CBOR_Decoding_Error("CBOR item count mismatch") + if available < needed: + raise CBOR_Decoding_Error("CBOR item count mismatch") + if available == 0: + if needed > 0: + raise CBOR_Decoding_Error("CBOR item count mismatch") + # Zero budget: later required fields reserved every remaining + # item. Optionals stay absent for reservation, but a *matching* + # optional must still be well-formed — otherwise a malformed + # present value would silently migrate into a trailing ANY. + if ( + isinstance(field, CBORF_optional) + and remaining + and field._field.matches_next_item(pkt, remaining) + ): + probe = pkt.__class__() + try: + field.dissect_result(probe, remaining) + except CBORF_badsequence: + pass + # CBOR_Decoding_Error / Type_Mismatch propagate. + self._mark_absent(pkt, field) + continue + try: + if isinstance(field, CBORF_SEQUENCE_OF): + result = field.dissect_result( + pkt, remaining, max_items=available + ) + elif isinstance(field, CBORF_optional): + if not field._field.matches_next_item(pkt, remaining): + self._mark_absent(pkt, field) + continue + result = field.dissect_result(pkt, remaining) + else: + result = field.dissect_result(pkt, remaining) + except CBORF_badsequence: + if needed > 0: + raise CBOR_Decoding_Error("CBOR item count mismatch") + self._mark_absent(pkt, field) + continue + if result.items > items_left: + raise CBOR_Decoding_Error( + "CBOR field consumed more items than remaining" + ) + if result.items == 0: + self._mark_absent(pkt, field) + remaining = result.remaining + items_left -= result.items + if items_left != 0: + raise CBOR_Decoding_Error("CBOR item count mismatch") + return remaining + + +class CBORF_SEQUENCE(_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. Use it when a schema is a field list without a major-type-4 + envelope (ASN.1 SEQUENCE analogy belongs on :class:`CBORF_ARRAY`). + + Example:: + + class MyCBOR(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_INTEGER("version", 1), + CBORF_TEXT_STRING("name", ""), + ) + """ + + def __init__(self, *seq, **kwargs): + # type: (*Any, **Any) -> None + super(CBORF_SEQUENCE, self).__init__(*seq, **kwargs) + self._reject_ambiguous_unbounded_sequences() + + def _reject_ambiguous_unbounded_sequences(self): + # type: () -> None + CBORF_ARRAY._reject_ambiguous_unbounded_sequences(self) + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + data, total_items = self._build_children(pkt) + return CBORBuildResult(data, total_items) + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + # Count only up to this schema's max so trailing CBOR items remain for + # a parent (e.g. Raw / Padding), matching definite ARRAY roots. + view = memoryview(s) if not isinstance(s, memoryview) else s + probe = view + item_count = 0 + max_count = self.max_items(pkt) + while probe and not cbor_is_break(probe) and item_count < max_count: + _obj, probe = CBORcodec_Object.decode_cbor_item(probe) + item_count += 1 + remaining = self._dissect_children_budgeted(pkt, s, item_count) + return CBORParseResult(remaining=remaining, items=item_count) + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + return self.build_result(pkt).data + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + return self.dissect_result(pkt, s).remaining + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return sum(f.min_items(pkt) for f in self.seq) + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return sum(f.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_SEQUENCE` 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_ambiguous_unbounded_sequences() + + def _reject_ambiguous_unbounded_sequences(self): + # type: () -> None + def _unbounded(field): + # type: (Any) -> bool + if isinstance(field, CBORF_optional): + return False + if isinstance(field, CBORF_CONDITIONAL): + return False + return ( + isinstance(field, CBORF_SEQUENCE_OF) + or ( + hasattr(field, "min_items") + and hasattr(field, "max_items") + and field.min_items(None) == 0 # type: ignore[arg-type] + and field.max_items(None) > 1 # type: ignore[arg-type] + ) + ) + + def _skippable(field): + # type: (Any) -> bool + return isinstance(field, (CBORF_optional, CBORF_CONDITIONAL)) + + unbounded_indexes = [ + index for index, field in enumerate(self.seq) if _unbounded(field) + ] + for left, right in zip(unbounded_indexes, unbounded_indexes[1:]): + # Adjacent unbounded fields, or unbounded fields separated only by + # optional/conditional fillers, cannot be partitioned uniquely. + if all(_skippable(self.seq[i]) for i in range(left + 1, right)): + raise ValueError( + "Ambiguous unbounded CBOR sequences in array schema" + ) + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + items_data, total_items = self._build_children(pkt) + if self.encode_indefinite: + data = ( + CBOR_encode_indefinite_head(int(CBOR_MajorTypes.ARRAY)) + + items_data + + CBOR_encode_break() + ) + else: + data = CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), total_items) + data += items_data + return CBORBuildResult(data, 1) + + def dissect_result(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( + 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: - try: - s = obj.dissect(pkt, s) - except CBORF_badsequence: - break - return [], s - - def dissect(self, pkt, s): - # type: (Any, bytes) -> bytes - _, x = self.m2i(pkt, s) - return x + remaining = self._dissect_children(pkt, remaining, count) + return CBORParseResult(remaining=remaining, items=1) 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 + return self.build_result(pkt).data + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + return self.dissect_result(pkt, s).remaining + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + +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['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_SEQUENCE_OF(CBORF_field[List[Any]]): + """ + Unframed sequence of homogeneous elements (no CBOR array head). + + Preferred constructors (ASN1F_SEQUENCE_OF / PacketListField style):: + + CBORF_SEQUENCE_OF("items", [], pkt_cls=MyPacket) + CBORF_SEQUENCE_OF("items", [], pkt_cls=CBORF_UNSIGNED_INTEGER) + CBORF_SEQUENCE_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``. + """ + CBOR_tag = None + islist = 1 + + 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 + ): + # type: (...) -> None + self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] + self.cls = None + self.item_field = None + self.holds_packets = 0 + + 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 + else: + chosen = pkt_cls + if isinstance(chosen, type) and issubclass(chosen, CBORF_field) or \ + isinstance(chosen, CBORF_field): + if isinstance(chosen, type): + self.item_field = chosen("_item", None) # type: ignore + else: + self.item_field = chosen + self.holds_packets = 0 + elif hasattr(chosen, "CBOR_root") or callable(chosen): + self.cls = cast("Type[CBOR_Packet]", chosen) + self.holds_packets = 1 + else: + raise ValueError( + "Provide pkt_cls or next_cls_cb" + ) + super(CBORF_SEQUENCE_OF, self).__init__(name, default) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> List[Any] + if x is None: + return None # type: ignore + if self.holds_packets: + items = list(x) + for item in items: + _cbor_attach_parent(pkt, item) + return items + return [self.item_field.any2i(pkt, item) for item in x] + + 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 + before_len = len(remaining) + if self.holds_packets: + pkt_cls = self.cls + if self.next_cls_cb is not None: + pkt_cls = self.next_cls_cb( + pkt, + values, + values[-1] if values else None, + remaining, + ) + if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: + break + item_bytes, next_remaining = cbor_item_span(remaining) + if len(next_remaining) >= before_len: + raise CBOR_Decoding_Error( + "Sequence decoder did not consume input") + try: + child = _cbor_packet_from_bytes(pkt_cls, item_bytes, pkt) + except CBOR_Decoding_Error: + raise + except Exception as exc: + raise CBOR_Decoding_Error(str(exc)) + values.append(child) + consumed += 1 + remaining = next_remaining + else: + result = self.item_field.parse_value(pkt, remaining) + if result.items != 1: + raise CBOR_Decoding_Error( + "SEQUENCE_OF element must consume exactly one item" + ) + if len(result.remaining) >= before_len: + raise CBOR_Decoding_Error( + "Sequence decoder did not consume input") + values.append(result.value) + consumed += 1 + remaining = result.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_result(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 + ) + self.set_val(pkt, values) + return CBORParseResult(remaining=remaining, items=consumed) + + def build_result(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 = [] # type: List[bytes] + total_items = 0 + for item in val: + if self.holds_packets: + parts.append( + _encode_exactly_one_cbor_item( + item, context="SEQUENCE_OF element" + ) + ) + total_items += 1 + else: + result = self.item_field.build_value(pkt, item) + if result.items != 1: + raise CBOR_Encoding_Error( + "SEQUENCE_OF element must emit exactly one item" + ) + parts.append(result.data) + total_items += 1 + return CBORBuildResult(b"".join(parts), total_items) + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 << 30 + + 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.item_field.i2repr(pkt, item) for item in x + ) + + def __repr__(self): + # type: () -> str + return "<%s %s>" % (self.__class__.__name__, self.name) + + +class CBORF_ARRAY_OF(CBORF_field[List[Any]]): """ 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. """ CBOR_tag = CBOR_MajorTypes.ARRAY islist = 1 @@ -589,30 +1591,36 @@ class CBORF_ARRAY_OF(CBORF_field[List[_ARRAY_T], List[CBOR_Object[Any]]]): def __init__(self, name, # type: str default, # type: Any - cls, # type: _ARRAY_T + pkt_cls=None, # type: _ARRAY_T ): # 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 + chosen = pkt_cls + if chosen is None: + raise ValueError("Provide pkt_cls") + if isinstance(chosen, type) and issubclass(chosen, CBORF_field) or \ + isinstance(chosen, CBORF_field): + if isinstance(chosen, type): + self.item_field = chosen("_item", None) # type: ignore else: - self.fld = cls - self._extract_item = lambda s, pkt: self.fld.m2i(pkt, s) + self.item_field = chosen 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) + elif hasattr(chosen, "CBOR_root") or callable(chosen): + self.cls = cast("Type[CBOR_Packet]", chosen) 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 + raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") + super(CBORF_ARRAY_OF, self).__init__(name, default) - def is_empty(self, pkt): - # type: (CBOR_Packet) -> bool - return CBORF_field.is_empty(self, pkt) + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> List[Any] + if x is None: + return None # type: ignore + if self.holds_packets: + items = list(x) + for item in items: + _cbor_attach_parent(pkt, item) + return items + return [self.item_field.any2i(pkt, item) for item in x] def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[List[Any], bytes] @@ -621,22 +1629,66 @@ def m2i(self, pkt, s): except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) if major_type != 4: - raise CBOR_Decoding_Error( + 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] + + def _decode_element(): + # type: () -> None + nonlocal s + if self.holds_packets: + item_bytes, s = cbor_item_span(s) + try: + child = _cbor_packet_from_bytes(self.cls, item_bytes, pkt) + except CBOR_Decoding_Error: + raise + except Exception as exc: + raise CBOR_Decoding_Error(str(exc)) + lst.append(child) + else: + result = self.item_field.parse_value(pkt, s) + if result.items != 1: + raise CBOR_Decoding_Error( + "ARRAY_OF element must consume exactly one item" + ) + lst.append(result.value) + s = result.remaining + + if count is CBOR_INDEFINITE: + while True: + if cbor_is_break(s): + s = cbor_consume_break(s) + break + _decode_element() + else: + for _ in range(count): + _decode_element() return lst, s - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - val = getattr(pkt, self.name) + def build_result(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 = [] # type: List[bytes] + for item in val: + if self.holds_packets: + parts.append( + _encode_exactly_one_cbor_item( + item, context="ARRAY_OF element" + ) + ) + else: + result = self.item_field.build_value(pkt, item) + if result.items != 1: + raise CBOR_Encoding_Error( + "ARRAY_OF element must emit exactly one item" + ) + parts.append(result.data) + items = b"".join(parts) + data = CBOR_encode_head(4, len(val)) + items + return CBORBuildResult(data, 1) def i2repr(self, pkt, x): # type: (CBOR_Packet, Any) -> str @@ -646,7 +1698,7 @@ def i2repr(self, pkt, x): return "[]" else: return "[%s]" % ", ".join( - self.fld.i2repr(pkt, item) for item in x # type: ignore + self.item_field.i2repr(pkt, item) for item in x ) def __repr__(self): @@ -654,14 +1706,29 @@ def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.name) -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 on the packet + (``_cbor_unknown_map_pairs``) as decoded semantic ``(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. + Example:: class MyCBOR(CBOR_Packet): @@ -672,19 +1739,23 @@ 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 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 def __repr__(self): # type: () -> str @@ -695,66 +1766,182 @@ 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() + ] + + def build_result(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_result(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)) + unknown = getattr(pkt, "_cbor_unknown_map_pairs", None) or [] + for key, value in unknown: + 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(5, len(pairs)) + b"".join(parts) + return CBORBuildResult(data, 1) + + def dissect_result(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( + 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 _map_text_key(key_obj): + # type: (Any) -> str + 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) + return key + + def _collect_pair(): + # type: () -> None + nonlocal remaining + # Keep encoded key bytes so unknown extensions round-trip exactly. + key_bytes, after_key = cbor_item_span(remaining) + key_obj, key_rest = CBORcodec_Object.decode_cbor_item(key_bytes) + if key_rest: + raise CBOR_Decoding_Error( + "CBOR map key did not decode to a single item" + ) + key = _map_text_key(key_obj) + val_bytes, remaining = cbor_item_span(after_key) + if key in field_map: + 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) + val_obj, val_rest = CBORcodec_Object.decode_cbor_item(val_bytes) + if val_rest: + raise CBOR_Decoding_Error( + "CBOR map value did not decode to a single item" + ) + unknown_pairs.append( + (key, cbor_object_to_python(val_obj)) + ) + + if count is CBOR_INDEFINITE: + while True: + if cbor_is_break(remaining): + remaining = cbor_consume_break(remaining) + break + _collect_pair() + else: + 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 + value_fld = fld + result = value_fld.dissect_result(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: + self._mark_map_field_absent(pkt, fld) + 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 dissect(self, pkt, s): - # type: (Any, bytes) -> bytes - _, x = self.m2i(pkt, s) - return x + for fld in self.seq: + 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._cbor_unknown_map_pairs = unknown_pairs # type: ignore[attr-defined] + return CBORParseResult(remaining=remaining, items=1) + + def _mark_map_field_absent(self, pkt, fld): + # type: (CBOR_Packet, Any) -> None + if isinstance(fld, CBORF_optional): + fld._field.set_val(pkt, CBOR_ABSENT) 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 + return self.build_result(pkt).data + + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + return self.dissect_result(pkt, s).remaining + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 -class CBORF_SEMANTIC_TAG(CBORF_field[Tuple[int, Any], - CBOR_SEMANTIC_TAG]): + +class CBORF_SEMANTIC_TAG(CBORF_field[int]): """ CBOR semantic tag field (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. + (named ``name``) stores the tag number, while the inner field stores its + value under its own name on the packet. Example:: @@ -764,51 +1951,105 @@ class TimestampPkt(CBOR_Packet): ) """ 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 self.tag_num = tag_num + if tag_num < 0 or tag_num > 0xFFFFFFFFFFFFFFFF: + raise CBOR_Encoding_Error( + "Semantic tag number out of uint64 range") self.inner_field = inner_field + # Honour an explicit default (e.g. CBOR_ABSENT); otherwise the field + # stores the configured tag number when present. + if default is None: + default = tag_num 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)) - - def m2i(self, pkt, s): - # type: (CBOR_Packet, bytes) -> Tuple[CBOR_SEMANTIC_TAG, bytes] + def _parse_tag_head(self, s, require_match=True): + # type: (bytes, bool) -> Tuple[int, 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( + 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 require_match and tag_num != self.tag_num: + raise CBOR_Type_Mismatch( + "Expected tag %d, got %d" % (self.tag_num, tag_num)) + return tag_num, remaining + + def _encode_tagged(self, inner_data): + # type: (bytes) -> bytes + return CBOR_encode_head(6, self.tag_num) + inner_data + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[int, bytes] + return self._parse_tag_head(s, require_match=True) + + 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 == 6 and tag_num == self.tag_num + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + tag_num, remaining = self._parse_tag_head(s) + inner = self.inner_field.dissect_result(pkt, remaining) + if inner.items != 1: + raise CBOR_Decoding_Error( + "Semantic tag content must be exactly one CBOR item") + self.set_val(pkt, tag_num) + return CBORParseResult(remaining=inner.remaining, items=1) 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) + return self.dissect_result(pkt, s).remaining + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + inner = self.inner_field.build_result(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 + _tag_num, 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]] + # type: () -> List[CBORF_field[Any]] return [self] + self.inner_field.get_fields_list() + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return pkt.getfieldval(self.name) is CBOR_ABSENT + ############################## # Complex CBOR Fields # @@ -818,86 +2059,185 @@ class CBORF_optional(CBORF_element): """ Wrapper making a :class:`CBORF_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. + 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 (the value is present but malformed). """ def __init__(self, field): - # type: (CBORF_field[Any, Any]) -> None + # type: (CBORF_field[Any]) -> None 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_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + if pkt.getfieldval(self._field.name) is CBOR_ABSENT: + return CBORBuildResult(b"", 0) + if self._field.is_empty(pkt): + return CBORBuildResult(b"", 0) + return self._field.build_result(pkt) + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + if not self._field.matches_next_item(pkt, s): + self._field.set_val(pkt, CBOR_ABSENT) + return CBORParseResult(remaining=s, items=0) + return self._field.dissect_result(pkt, s) + + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + return self.build_result(pkt).data 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 + return self.dissect_result(pkt, s).remaining + + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 0 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return self._field.max_items(pkt) + + +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 + fields.ConditionalField.__init__(self, fld, cond) + + def __repr__(self): + # type: () -> str + return "<%s%r>" % (self.__class__.__name__, self.fld) + + @property + def owners(self): + return self.fld.owners + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + if self._evalcond(pkt): + return self.fld.build_result(pkt) + return CBORBuildResult(b"", 0) + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + if self._evalcond(pkt): + return self.fld.dissect_result(pkt, s) + return CBORParseResult(remaining=s, items=0) def build(self, pkt): # type: (CBOR_Packet) -> bytes - if self._field.is_empty(pkt): - return b"" - return self._field.build(pkt) + return self.build_result(pkt).data - def any2i(self, pkt, x): - # type: (CBOR_Packet, Any) -> Any - return self._field.any2i(pkt, x) + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + return self.dissect_result(pkt, s).remaining - def i2repr(self, pkt, x): - # type: (CBOR_Packet, Any) -> str - return self._field.i2repr(pkt, x) + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + if self._evalcond(pkt): + return self.fld.min_items(pkt) + return 0 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + if self._evalcond(pkt): + return self.fld.max_items(pkt) + return 0 -class CBORF_PACKET(CBORF_field['CBOR_Packet', Optional['CBOR_Packet']]): +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 = pkt_cls + super(CBORF_PACKET, self).__init__(name, default) + + def _parse_packet_item(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[CBOR_Packet, bytes] + """Decode exactly one CBOR item into a nested packet.""" + item_bytes, remain = cbor_item_span(s) + try: + child = _cbor_packet_from_bytes(self.cls, item_bytes, pkt) + except CBOR_Decoding_Error: + raise + except Exception as exc: + raise CBOR_Decoding_Error(str(exc)) + return child, remain + + def _build_packet_item(self, pkt, val): + # type: (CBOR_Packet, Any) -> CBORBuildResult + """Encode a nested packet and enforce one top-level CBOR item.""" + if val is None: + raise CBOR_Encoding_Error( + "Required field %r is None" % self.name) + data = _encode_exactly_one_cbor_item( + val, context="field %r" % self.name + ) + return CBORBuildResult(data, 1) 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] + return self._parse_packet_item(pkt, s) 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) + return self._build_packet_item(pkt, x).data 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 encode_value(self, x): + # type: (Any) -> bytes + return self._build_packet_item(None, x).data # type: ignore + + def parse_value(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + child, remain = self._parse_packet_item(pkt, s) + return CBORParseResult(value=child, remaining=remain, items=1) + + def build_value(self, pkt, value): + # type: (CBOR_Packet, Any) -> CBORBuildResult + return self._build_packet_item(pkt, value) + + def build_result(self, pkt): + # type: (CBOR_Packet) -> CBORBuildResult + return self._build_packet_item(pkt, pkt.getfieldval(self.name)) + + def dissect_result(self, pkt, s): + # type: (CBOR_Packet, bytes) -> CBORParseResult + child, remain = self._parse_packet_item(pkt, s) + self.set_val(pkt, child) + return CBORParseResult(remaining=remain, items=1) def randval(self): # type: ignore # type: () -> CBOR_Packet diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index eb12bedaea9..8c066f6366c 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -6,24 +6,24 @@ 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``), mutable ANY values, and nested item counts. """ from scapy.base_classes import Packet_metaclass from scapy.packet import Packet +import copy + from typing import ( Any, 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, @@ -40,26 +40,181 @@ def __new__(cls, ) +def _finalize_cbor_raw_cache(pkt, raw, remain, items): + # type: (Packet, bytes, bytes, int) -> None + """Record raw cache, item count, and mutable-field snapshot after dissect. + + CBOR-specific Packet cache integration: mirrors ``Packet.do_dissect`` + bookkeeping and also stores ``_cbor_raw_cache_items`` so unframed sequence + roots can return the exact received bytes without rebuilding. + """ + from scapy.cbor.cborfields import CBOR_ABSENT + pkt.raw_packet_cache = raw[:-len(remain)] if remain else raw + pkt._cbor_raw_cache_items = items # type: ignore[attr-defined] + pkt.raw_packet_cache_fields = {} + for f in pkt.fields_desc: + if f.name not in pkt.fields: + continue + fval = pkt.fields[f.name] + if fval is CBOR_ABSENT: + pkt.raw_packet_cache_fields[f.name] = 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: + pkt.raw_packet_cache_fields[f.name] = \ + pkt._raw_packet_cache_field_value(f, fval, copy=True) + pkt.explicit = 1 + + +def _cbor_raw_cache_is_valid(pkt): + # type: (Packet) -> bool + """Return True if ``raw_packet_cache`` still matches nested field state.""" + if pkt.raw_packet_cache is None or pkt.raw_packet_cache_fields is None: + return False + for fname, fval in pkt.raw_packet_cache_fields.items(): + fld, val = pkt.getfield_and_val(fname) + if pkt._raw_packet_cache_field_value(fld, val) != fval: + pkt.raw_packet_cache = None + pkt.raw_packet_cache_fields = None + pkt._cbor_raw_cache_items = None # type: ignore[attr-defined] + pkt.wirelen = None + return False + return True + + 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. - def self_build(self): - # type: () -> bytes - """Build this CBOR packet to wire bytes using CBOR_root. + Field flags (``islist`` / ``ismutable`` / ``holds_packets``) drive + Scapy's mutation detection. This class additionally deepens ``ismutable`` + defaults and stores parsed root item counts for exact-wire rebuilds. + """ + + CBOR_root = None # type: Optional[Any] + + def cbor_build_result(self): + # type: () -> Any + """Return ``CBORBuildResult`` for this packet's root schema. - 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. + When the raw cache is valid, return the exact received bytes together + with the dissected top-level item count. Never rebuild an unchanged + packet merely to recover cardinality. """ - if self.raw_packet_cache is not None: + from scapy.cbor.cborfields import CBORBuildResult + if _cbor_raw_cache_is_valid(self): + items = getattr(self, "_cbor_raw_cache_items", None) + if items is None: + items = 1 + return CBORBuildResult(self.raw_packet_cache, items) + result = self.CBOR_root.build_result(self) + self._cbor_raw_cache_items = result.items # type: ignore[attr-defined] + return result + + 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 only deep-copies list/dict/set defaults; deepen ismutable. + for f in self.fields_desc: + if getattr(f, "ismutable", False) and f.name in self.fields: + self.fields[f.name] = f.do_copy(self.fields[f.name]) + # Packet-valued defaults are copied in Packet.__init__ with + # parent=None; re-run any2i so this instance becomes the parent. + if f.holds_packets and f.name in self.fields: + self.fields[f.name] = f.any2i(self, self.fields[f.name]) + + def getfield_and_val(self, attr): + # type: (str) -> Tuple[Any, Any] + if attr not in self.fields and attr in self.default_fields: + fld = self.get_field(attr) + if fld is not None and ( + getattr(fld, "ismutable", False) or fld.holds_packets + ): + val = fld.do_copy(self.default_fields[attr]) + # Re-run any2i so packet-valued defaults attach this instance + # as parent (defaults were normalized with pkt=None). + if fld.holds_packets: + val = fld.any2i(self, val) + self.fields[attr] = val + return fld, self.fields[attr] + return super(CBOR_Packet, self).getfield_and_val(attr) + + def getfieldval(self, attr): + # type: (str) -> Any + if attr not in self.fields and attr in self.default_fields: + fld = self.get_field(attr) + if fld is not None and ( + getattr(fld, "ismutable", False) or fld.holds_packets + ): + val = fld.do_copy(self.default_fields[attr]) + if fld.holds_packets: + val = fld.any2i(self, val) + self.fields[attr] = val + return self.fields[attr] + return super(CBOR_Packet, self).getfieldval(attr) + + def self_build(self): + # type: () -> bytes + if _cbor_raw_cache_is_valid(self): return self.raw_packet_cache return self.CBOR_root.build(self) + def do_build(self): + # type: () -> bytes + # Packet.do_build() expands via __iter__ when explicit=0 (setfieldval). + # That would drop CBOR-only packet state such as unknown map pairs. + pkt = self.self_build() + for t in self.post_transforms: + pkt = t(pkt) + pay = self.do_build_payload() + if self.raw_packet_cache is None: + return self.post_build(pkt, pay) + return pkt + pay + def do_dissect(self, x): # type: (bytes) -> bytes - """Dissect CBOR-encoded bytes into packet fields. + result = self.CBOR_root.dissect_result(self, x) + _finalize_cbor_raw_cache(self, x, result.remaining, result.items) + return result.remaining + + 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 attr in ( + "_cbor_raw_cache_items", + "_cbor_unknown_map_pairs", + "_crc_content_span", + ): + if hasattr(self, attr): + val = getattr(self, attr) + if attr == "_cbor_unknown_map_pairs": + setattr( + clone, + attr, + copy.deepcopy(val), + ) + else: + setattr(clone, attr, val) + from scapy.cbor.cborfields import _cbor_attach_parent + for f in clone.fields_desc: + if not f.holds_packets or f.name not in clone.fields: + continue + fval = clone.fields[f.name] + if isinstance(fval, Packet): + _cbor_attach_parent(clone, fval) + elif isinstance(fval, list): + for item in fval: + if isinstance(item, Packet): + _cbor_attach_parent(clone, item) + return clone 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..dbef36fb59d 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -2357,3 +2357,15 @@ p assert p.indent == 0xf assert p.pcount == 4 assert [p.x for p in p.plist] == [0x41, 0x42, 0x43, 0x44] + +############ +############ ++ ConditionalField __getattr__ + += ConditionalField __getattr__ has no try/except AttributeError wrapper +~ core field +import inspect +from scapy import fields + +src = inspect.getsource(fields.ConditionalField.__getattr__) +assert "except AttributeError" not in src diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index f65c75d89ce..c902235bbbb 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,2680 @@ 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 ++ CBORF_SEQUENCE_OF packet item cardinality + += CBORF_SEQUENCE_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_SEQUENCE, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class TwoItemSequenceDecodeElement(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_UNSIGNED_INTEGER("first", 0), + CBORF_UNSIGNED_INTEGER("second", 0), + ) -+ CBOR Interoperability - Basic Types (Scapy encode, cbor2 decode) +class PacketSequenceDecode(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_OF( + "elements", + [], + TwoItemSequenceDecodeElement, + ) -= Check cbor2 availability try: - import cbor2 - cbor2_available = True -except ImportError: - cbor2_available = False + PacketSequenceDecode(b"\x01\x02") + assert False, "SEQUENCE_OF accepted two CBOR items as one packet element" +except CBOR_Decoding_Error: + pass -cbor2_available ++ Optional lookahead distinguishes absence from malformed presence -= Interop: Scapy encode unsigned integer, cbor2 decode -import cbor2 -obj = CBOR_UNSIGNED_INTEGER(42) -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded == 42 += 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 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 +class OptionalTaggedThenFallback(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_UNSIGNED_INTEGER("tagged_value", None), + ) + ), + CBORF_ANY("fallback", None), + ) + +pkt = OptionalTaggedThenFallback(b"\x81\x07") +from scapy.cbor.cborfields import CBOR_ABSENT +assert pkt.tag_number is CBOR_ABSENT +assert pkt.tagged_value is None or pkt.tagged_value is CBOR_ABSENT +assert pkt.fallback == 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_SEQUENCE, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet + +class OptionalTaggedUnsigned(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_UNSIGNED_INTEGER("tagged_value", None), + ) + ) + ) + +pkt = OptionalTaggedUnsigned() +try: + OptionalTaggedUnsigned.CBOR_root.dissect_result( + 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_SEQUENCE, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet + +class OptionalTruncatedTaggedUnsigned(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_UNSIGNED_INTEGER("tagged_value", None), + ) + ) + ) + +pkt = OptionalTruncatedTaggedUnsigned() +try: + OptionalTruncatedTaggedUnsigned.CBOR_root.dissect_result(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 +# Finding 2: when the optional has available==0 because a required trailing +# field reserved the only item, mark the optional absent if the item does not +# match the optional type. A *matching* but malformed optional must still +# raise (see "Malformed optional semantic tag does not migrate..."). +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 + +class OptionalTaggedBeforeAny(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_UNSIGNED_INTEGER("tagged_value", None), + ) + ), + CBORF_ANY("fallback", None), + ) + +# Tag 2 does not match optional tag 1 → absent; ANY consumes the item. +pkt = OptionalTaggedBeforeAny(b"\x81\xc2\x01") +assert pkt.getfieldval("tag_number") 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 + +class OptionalAnyWithTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", None)), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + +pkt = OptionalAnyWithTail(b"\x82\xf6\x01") +assert pkt.value is None +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 + +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 + +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_SEQUENCE_OF runs child hooks and retains each item span +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +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_SEQUENCE_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 + +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 + +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, + ), + ) -= Interop: Scapy encode false, cbor2 decode -obj = CBOR_FALSE() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded is False +kind_first = b"\xa2\x64kind\x01\x64name\x61x" +name_first = b"\xa2\x64name\x61x\x64kind\x01" -= Interop: Scapy encode null, cbor2 decode -obj = CBOR_NULL() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -decoded is None +first = ConditionalMap(kind_first) +second = ConditionalMap(name_first) +assert first.kind == second.kind == 1 +assert first.getfieldval("name") == second.getfieldval("name") == "x" -= Interop: Scapy encode undefined, cbor2 decode -obj = CBOR_UNDEFINED() -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -from cbor2 import undefined -decoded is undefined ++ Generic CBOR map key identity -= Interop: Scapy encode float, cbor2 decode -obj = CBOR_FLOAT(3.14159) -encoded = bytes(obj) -decoded = cbor2.loads(encoded) -abs(decoded - 3.14159) < 0.0001 += Generic CBOR maps preserve integer 1 and boolean true as distinct keys +from scapy.cbor import CBOR_Codecs -+ CBOR Interoperability - Collections (Scapy encode, cbor2 decode) +wire = b"\xa2\x01\x61a\xf5\x61b" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +assert obj.enc() == wire -= 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] += Generic CBOR maps round-trip a map-valued key +from scapy.cbor import CBOR_Codecs -= 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]]] +wire = b"\xa1\xa1\x01\x02\x03" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +assert obj.enc() == wire -= 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 += Generic CBOR maps still reject duplicate data-item keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error -= 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 +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 -= 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'' ++ CBORF_ANY map identity and mutability -+ CBOR Map Specific Encodings += 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 CBORMapData +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 AnyMapPkt(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("a", None), + CBORF_UNSIGNED_INTEGER("b", 0), + ) -= 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'' +pkt = AnyMapPkt(b"\x82\xa0\x00") +assert isinstance(pkt.a, CBORMapData) +assert len(pkt.a) == 0 +pkt.b = 1 +assert bytes(pkt) == b"\x82\xa0\x01" -+ CBOR Float Specific Encodings += 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 CBORMapData +from scapy.cborpacket import CBOR_Packet -= float64 encoding specific value -obj = CBOR_FLOAT(1.5e20) -data = bytes(obj) -data == bytes.fromhex('FB442043561A882930') +class AnyMapPkt2(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("a", None), + CBORF_UNSIGNED_INTEGER("b", 0), + ) -= 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'' +pkt = AnyMapPkt2(b"\x82\xa1\x01\x02\x00") +assert isinstance(pkt.a, CBORMapData) +assert pkt.a[1] == 2 +pkt.b = 1 +assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" -+ CBOR Multiple Item Decoding += In-place mutation of CBORF_ANY list invalidates raw cache +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet -= 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'' +class AnyRoot(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= 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) +pkt = AnyRoot(b"\x82\x01\x02") +assert pkt.raw_packet_cache == b"\x82\x01\x02" +pkt.value.append(3) +assert bytes(pkt) == b"\x83\x01\x02\x03" -+ CBOR Boundary Value Tests += Typed map lookup distinguishes integer 1 from boolean True +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cbor import CBORMapData -= encode maximum value that fits in each size -# Maximum for size 0 (0-23) -obj = CBOR_UNSIGNED_INTEGER(23) -bytes(obj) == bytes.fromhex('17') +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] -= encode minimum value needing size 1 -obj = CBOR_UNSIGNED_INTEGER(24) -bytes(obj) == bytes.fromhex('1818') += CBORMapData equality with dict keeps True and 1 distinct +from scapy.cbor.cbor import CBORMapData, CBOR_TRUE, CBOR_UNSIGNED_INTEGER -= encode maximum value for size 1 -obj = CBOR_UNSIGNED_INTEGER(255) -bytes(obj) == bytes.fromhex('18ff') +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 -= encode minimum value needing size 2 -obj = CBOR_UNSIGNED_INTEGER(256) -bytes(obj) == bytes.fromhex('190100') ++ optional major-type-7 lookahead and absence -= negative integer boundary at -24 -obj = CBOR_NEGATIVE_INTEGER(-24) -bytes(obj) == bytes.fromhex('37') += 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 -= negative integer boundary at -25 -obj = CBOR_NEGATIVE_INTEGER(-25) -bytes(obj) == bytes.fromhex('3818') +class OptBoolFloat(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("flag", None)), + CBORF_FLOAT("num", 0.0), + ) -+ CBOR Empty Container Tests +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 -= 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 += 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 -= 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 +class OptBoolNull(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("flag", None)), + CBORF_NULL("nil"), + ) -= encode empty byte string -obj = CBOR_BYTE_STRING(b'') -data = bytes(obj) -data == bytes.fromhex('40') +pkt = OptBoolNull(b"\x81\xf6") +assert pkt.flag is CBOR_ABSENT +assert pkt.nil is None -= 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 - -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 - -= 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 - -roundtrip_count >= 45 - -########### CBOR Fields ########################################### - -+ CBORF scalar fields - CBORF_UNSIGNED_INTEGER - -= CBORF_UNSIGNED_INTEGER basic encode/decode -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER += 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 -class PktUInt(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER("value", 42) +class OptNullBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_NULL("nil")), + CBORF_BOOLEAN("flag", False), + ) -pkt = PktUInt() -assert pkt.value.val == 42 -raw_data = bytes(pkt) -pkt2 = PktUInt(raw_data) -assert pkt2.value.val == 42 +pkt = OptNullBool(b"\x81\xf5") +assert pkt.nil is CBOR_ABSENT +assert pkt.flag is True -= CBORF_UNSIGNED_INTEGER zero value -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER += 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 PktUIntZero(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) +class OptUndefFloat(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNDEFINED("u")), + CBORF_FLOAT("num", 0.0), + ) -pkt = PktUIntZero() -raw_data = bytes(pkt) -assert raw_data == b'\x00' -pkt2 = PktUIntZero(raw_data) -assert pkt2.value.val == 0 +pkt = OptUndefFloat(b"\x81\xf9\x3e\x00") +assert pkt.u is CBOR_ABSENT +assert abs(pkt.num - 1.5) < 1e-6 -= CBORF_UNSIGNED_INTEGER large value roundtrip -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER += 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 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 +class OptFloatBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_FLOAT("num", None)), + CBORF_BOOLEAN("flag", False), + ) -+ CBORF scalar fields - CBORF_NEGATIVE_INTEGER +pkt = OptFloatBool(b"\x81\xf4") +assert pkt.num is CBOR_ABSENT +assert pkt.flag is False -= CBORF_NEGATIVE_INTEGER basic encode/decode -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER += 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 -class PktNInt(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER("value", -1) +class OptAnyTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_optional(CBORF_ANY("extra", None)), + ) + +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 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("tag", None, 1, CBORF_INTEGER("ts", 0))), + CBORF_optional(CBORF_TEXT_STRING("endpoint", "default")), + ) + +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.tag is CBOR_ABSENT +assert pkt.endpoint is CBOR_ABSENT +pkt.n = 1 +assert bytes(pkt) == b"\xa1\x61n\x01" + ++ array item reservation + += Nonterminal SEQUENCE_OF reserves items for a later required field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class SeqThenReq(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -pkt = PktNInt() -assert pkt.value.val == -1 -raw_data = bytes(pkt) -pkt2 = PktNInt(raw_data) -assert pkt2.value.val == -1 +pkt = SeqThenReq(b"\x83\x01\x02\x03") +assert pkt.vals == [1, 2] +assert pkt.tail == 3 -= CBORF_NEGATIVE_INTEGER -100 roundtrip -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER += 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 PktNInt100(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER("value", -100) +class OptThenReq(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + CBORF_UNSIGNED_INTEGER("req", 0), + ) + +pkt = OptThenReq(b"\x81\x07") +assert pkt.opt is CBOR_ABSENT +assert pkt.req == 7 + += Indefinite array reserves SEQUENCE_OF items for a required tail +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class IndefSeqThenReq(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + +pkt = IndefSeqThenReq(b"\x9f\x01\x02\x03\xff") +assert pkt.vals == [1, 2] +assert pkt.tail == 3 + ++ 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, + CBORTagValue, + CBORSimpleValue, +) +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_SEQUENCE, + CBORF_SEQUENCE_OF, + CBORF_TEXT_STRING, + CBORF_UNDEFINED, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet + +_RR_FLOAT_1_5 = b"\xfb\x3f\xf8\x00\x00\x00\x00\x00\x00" + + ++ Finding 1: CBORF_ANY must preserve map identity + += A non-empty CBORF_ANY map remains a map after a sibling field changes +class RRAnyNonEmptyMap(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -pkt = PktNInt100() -raw_data = bytes(pkt) -pkt2 = PktNInt100(raw_data) -assert pkt2.value.val == -100 +wire = b"\x82\xa1\x01\x02\x00" +pkt = RRAnyNonEmptyMap(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" -+ CBORF scalar fields - CBORF_INTEGER += 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), + ) -= CBORF_INTEGER positive value -from scapy.cbor.cborfields import CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet +pkt = RRAnyEmptyMap(b"\x82\xa0\x00") +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa0\x01" -class PktInt(CBOR_Packet): - CBOR_root = CBORF_INTEGER("value", 7) += 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), + ) -pkt = PktInt() -raw_data = bytes(pkt) -pkt2 = PktInt(raw_data) -assert pkt2.value.val == 7 +wire = b"\x82\x81\xa1\x01\x02\x00" +pkt = RRAnyNestedMap(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\x81\xa1\x01\x02\x01" -= CBORF_INTEGER negative value -from scapy.cbor.cborfields import CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet += 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), + ) -class PktIntNeg(CBOR_Packet): - CBOR_root = CBORF_INTEGER("value", -5) +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" -pkt = PktIntNeg() -raw_data = bytes(pkt) -pkt2 = PktIntNeg(raw_data) -assert pkt2.value.val == -5 += 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), + ) -+ CBORF scalar fields - CBORF_BYTE_STRING +wire = b"\x82\xa1\x81\x01\x02\x00" +pkt = RRAnyCompoundMapKey(wire) +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xa1\x81\x01\x02\x01" -= CBORF_BYTE_STRING basic encode/decode -from scapy.cbor.cborfields import CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet += 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), + ) -class PktBStr(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING("data", b"hello") +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" -pkt = PktBStr() -assert pkt.data.val == b"hello" -raw_data = bytes(pkt) -pkt2 = PktBStr(raw_data) -assert pkt2.data.val == b"hello" -= CBORF_BYTE_STRING empty bytes -from scapy.cbor.cborfields import CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet ++ Finding 2: optional major-type-7 lookahead must be exact -class PktBStrEmpty(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING("data", b"") += Optional Boolean does not consume a following floating-point value +class RROptionalBooleanThenFloat(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_FLOAT("value", None), + ) -pkt = PktBStrEmpty() -raw_data = bytes(pkt) -assert raw_data == b'\x40' -pkt2 = PktBStrEmpty(raw_data) -assert pkt2.data.val == b"" +pkt = RROptionalBooleanThenFloat(_RR_FLOAT_1_5) +assert pkt.value == 1.5 -+ CBORF scalar fields - CBORF_TEXT_STRING += Optional Boolean does not consume a following null +class RROptionalBooleanThenNull(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_NULL("value"), + ) -= CBORF_TEXT_STRING basic encode/decode -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +pkt = RROptionalBooleanThenNull(b"\xf6") +assert bytes(pkt) == b"\xf6" -class PktTStr(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING("title", "hello") += Optional null does not consume a following Boolean +class RROptionalNullThenBoolean(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_NULL("maybe")), + CBORF_BOOLEAN("value", None), + ) -pkt = PktTStr() -assert pkt.title.val == "hello" -raw_data = bytes(pkt) -pkt2 = PktTStr(raw_data) -assert pkt2.title.val == "hello" +pkt = RROptionalNullThenBoolean(b"\xf5") +assert pkt.value is True -= CBORF_TEXT_STRING empty string -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet += Optional undefined does not consume a following float +class RROptionalUndefinedThenFloat(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_UNDEFINED("maybe")), + CBORF_FLOAT("value", None), + ) -class PktTStrEmpty(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING("title", "") +pkt = RROptionalUndefinedThenFloat(_RR_FLOAT_1_5) +assert pkt.value == 1.5 -pkt = PktTStrEmpty() -raw_data = bytes(pkt) -assert raw_data == b'\x60' -pkt2 = PktTStrEmpty(raw_data) -assert pkt2.title.val == "" += Optional float does not consume a following Boolean +class RROptionalFloatThenBoolean(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_FLOAT("maybe", None)), + CBORF_BOOLEAN("value", None), + ) -+ CBORF scalar fields - CBORF_BOOLEAN +pkt = RROptionalFloatThenBoolean(b"\xf4") +assert pkt.value is False -= CBORF_BOOLEAN true value -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cbor.cbor import CBOR_TRUE -from scapy.cborpacket import CBOR_Packet += Exact major-type-7 matches are still consumed by optional fields +class RROptionalBooleanPresent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_UNSIGNED_INTEGER("tail", None), + ) -class PktBool(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN("flag", True) +class RROptionalFloatPresent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_FLOAT("maybe", None)), + CBORF_UNSIGNED_INTEGER("tail", None), + ) -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) +boolean_pkt = RROptionalBooleanPresent(b"\xf5\x07") +assert boolean_pkt.maybe is True +assert boolean_pkt.tail == 7 -= CBORF_BOOLEAN false value -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cbor.cbor import CBOR_FALSE -from scapy.cborpacket import CBOR_Packet +float_pkt = RROptionalFloatPresent(_RR_FLOAT_1_5 + b"\x07") +assert float_pkt.maybe == 1.5 +assert float_pkt.tail == 7 -class PktBoolFalse(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN("flag", False) += 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 -pkt = PktBoolFalse() -raw_data = bytes(pkt) -assert raw_data == b'\xf4' -pkt2 = PktBoolFalse(raw_data) -assert isinstance(pkt2.flag, CBOR_FALSE) += Optional Boolean leaves direct and extended simple values for CBORF_ANY +class RROptionalBooleanThenAny(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("value", None), + ) -+ CBORF scalar fields - CBORF_FLOAT +for wire, expected in ((b"\xf0", 16), (b"\xf8\x20", 32)): + pkt = RROptionalBooleanThenAny(wire) + assert isinstance(pkt.value, CBORSimpleValue) + assert pkt.value.value == expected -= CBORF_FLOAT encode/decode -from scapy.cbor.cborfields import CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet += Optional null and undefined do not consume each other's wire values +class RROptionalNullThenUndefined(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_NULL("maybe")), + CBORF_UNDEFINED("value"), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -class PktFloat(CBOR_Packet): - CBOR_root = CBORF_FLOAT("value", 1.5) +class RROptionalUndefinedThenNull(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_UNDEFINED("maybe")), + CBORF_NULL("value"), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) -pkt = PktFloat() -raw_data = bytes(pkt) -pkt2 = PktFloat(raw_data) -assert abs(pkt2.value.val - 1.5) < 1e-9 +undefined_pkt = RROptionalNullThenUndefined(b"\xf7\x00") +undefined_pkt.tail = 1 +assert bytes(undefined_pkt) == b"\xf7\x01" -= CBORF_NULL encode/decode -from scapy.cbor.cborfields import CBORF_NULL -from scapy.cbor.cbor import CBOR_NULL -from scapy.cborpacket import CBOR_Packet +null_pkt = RROptionalUndefinedThenNull(b"\xf6\x00") +null_pkt.tail = 1 +assert bytes(null_pkt) == b"\xf6\x01" -class PktNull(CBOR_Packet): - CBOR_root = CBORF_NULL("nothing") -pkt = PktNull() -raw_data = bytes(pkt) -assert raw_data == b'\xf6' -pkt2 = PktNull(raw_data) -assert isinstance(pkt2.nothing, CBOR_NULL) ++ Finding 3: optional absence must be represented on every decode path -+ CBORF scalar fields - CBORF_UNDEFINED += 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)), + ) -= 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 = RRAbsentAnyDefinite(b"\x81\x00") +pkt.head = 1 +assert bytes(pkt) == b"\x81\x01" -class PktUndef(CBOR_Packet): - CBOR_root = CBORF_UNDEFINED("undef") += 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)), + ) -pkt = PktUndef() -raw_data = bytes(pkt) -assert raw_data == b'\xf7' -pkt2 = PktUndef(raw_data) -assert isinstance(pkt2.undef, CBOR_UNDEFINED) +pkt = RRAbsentAnyIndefinite(b"\x9f\x00\xff") +pkt.head = 1 +assert bytes(pkt) == b"\x9f\x01\xff" -+ CBORF structured fields - CBORF_ARRAY += 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)), + ) -= CBORF_ARRAY two-field encode/decode -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +pkt = RRAbsentAnyMap(b"\xa1\x61a\x00") +pkt.a = 1 +assert bytes(pkt) == b"\xa1\x61a\x01" -class MyCBOR(CBOR_Packet): += Optional null undefined and semantic-tag fields stay absent at array end +class RRAbsentNull(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("version", 1), - CBORF_TEXT_STRING("title", "test"), + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_NULL("value")), ) -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 -from scapy.cborpacket import CBOR_Packet +class RRAbsentUndefined(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional(CBORF_UNDEFINED("value")), + ) -class Multi(CBOR_Packet): +class RRAbsentTag(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("id", 99), - CBORF_TEXT_STRING("label", "x"), - CBORF_BOOLEAN("active", True), + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_UNSIGNED_INTEGER("tagged_value", 7), + ) + ), + ) + +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__ + += 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)), ) -pkt = Multi() -raw_data = bytes(pkt) -pkt2 = Multi(raw_data) -assert pkt2.id.val == 99 -assert pkt2.label.val == "x" +pkt = RRAbsentDefaultScalar(b"\x81\x00") +pkt.head = 1 +assert bytes(pkt) == b"\x81\x01" -= CBORF_ARRAY single integer roundtrip -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER -from scapy.cborpacket import CBOR_Packet += An absent optional packet does not reappear from its packet default +class RRAbsentPacketChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 9) -class Single(CBOR_Packet): +class RRAbsentPacketParent(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER("count", 5), + CBORF_UNSIGNED_INTEGER("head", 0), + CBORF_optional( + CBORF_PACKET( + "child", + RRAbsentPacketChild(value=9), + RRAbsentPacketChild, + ) + ), + ) + +pkt = RRAbsentPacketParent(b"\x81\x00") +pkt.head = 1 +assert bytes(pkt) == b"\x81\x01" + += 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)), ) -pkt = Single() -raw_data = bytes(pkt) -pkt2 = Single(raw_data) -assert pkt2.count.val == 5 - -+ CBORF structured fields - CBORF_ARRAY_OF +pkt = RRAbsentDefaultMapScalar(b"\xa1\x61a\x00") +pkt.a = 1 +assert bytes(pkt) == b"\xa1\x61a\x01" -= 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 -from scapy.cborpacket import CBOR_Packet - -class ArrOfInt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF("items", [], CBORF_INTEGER) += 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), + ) -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 +pkt = RRPresentOptionalNull(b"\x82\xf6\x00") +pkt.tail = 1 +assert bytes(pkt) == b"\x82\xf6\x01" -+ CBORF structured fields - CBORF_MAP -= CBORF_MAP basic encode/decode -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet ++ Finding 4: positional arrays must reserve items for later required fields -class MyMap(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER("version", 2), - CBORF_TEXT_STRING("title", "cbor"), += Definite arrays reserve the final item after a nonterminal SEQUENCE_OF +class RRSequenceThenTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_SEQUENCE_OF( + "values", + [], + CBORF_UNSIGNED_INTEGER("item", None), + ), + CBORF_UNSIGNED_INTEGER("tail", None), + ) + +pkt = RRSequenceThenTail(b"\x83\x01\x02\x03") +assert pkt.values == [1, 2] +assert pkt.tail == 3 + += Indefinite arrays reserve the final item after a nonterminal SEQUENCE_OF +class RRIndefiniteSequenceThenTail(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_SEQUENCE_OF( + "values", + [], + CBORF_UNSIGNED_INTEGER("item", None), + ), + CBORF_UNSIGNED_INTEGER("tail", None), + ) + +pkt = RRIndefiniteSequenceThenTail(b"\x9f\x01\x02\x03\xff") +assert pkt.values == [1, 2] +assert pkt.tail == 3 + += 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), ) -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" +pkt = RROptionalThenRequiredUnsigned(b"\x81\x07") +assert pkt.required_value == 7 +pkt.required_value = 8 +assert bytes(pkt) == b"\x81\x08" -= CBORF_MAP byte string value -from scapy.cbor.cborfields import CBORF_MAP, CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet += An optional packet yields a sole item to a required packet +class RRBudgetChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", None) -class BinMap(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_BYTE_STRING("data", b"\xde\xad\xbe\xef"), +class RROptionalThenRequiredPacket(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_PACKET("optional_child", None, RRBudgetChild)), + CBORF_PACKET("required_child", None, RRBudgetChild), ) -pkt = BinMap() -raw_data = bytes(pkt) -pkt2 = BinMap(raw_data) -assert pkt2.data.val == b"\xde\xad\xbe\xef" - -+ CBORF complex fields - CBORF_optional +pkt = RROptionalThenRequiredPacket(b"\x81\x07") +assert pkt.required_child.value == 7 -= CBORF_optional present field -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_optional -from scapy.cborpacket import CBOR_Packet - -class OptPkt(CBOR_Packet): += A SEQUENCE_OF reserves an item for a later required conditional field +class RRSequenceThenConditionalTail(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("version", 1), - CBORF_optional(CBORF_TEXT_STRING("title", "")), + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_SEQUENCE_OF( + "values", + [], + CBORF_UNSIGNED_INTEGER("item", None), + ), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("tail", None), + lambda pkt: pkt.getfieldval("flag") == 1, + ), ) -pkt = OptPkt() -raw_data = bytes(pkt) -pkt2 = OptPkt(raw_data) -assert pkt2.version.val == 1 -assert pkt2.title.val == "" +pkt = RRSequenceThenConditionalTail(b"\x83\x01\x02\x03") +assert pkt.flag == 1 +assert pkt.values == [2] +assert pkt.tail == 3 -+ CBORF_PACKET nested packet -= CBORF_PACKET basic nesting -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_PACKET -from scapy.cborpacket import CBOR_Packet ++ Finding 5: recursive CBORF_ANY mutations must invalidate the raw cache -class Inner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("x", 10), - ) += Appending to a decoded root CBORF_ANY array changes serialized bytes +class RRMutableAnyRoot(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -class Outer(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING("label", "outer"), - CBORF_PACKET("inner", None, Inner), - ) +pkt = RRMutableAnyRoot(b"\x82\x01\x02") +pkt.value.append(3) +assert bytes(pkt) == b"\x83\x01\x02\x03" -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" += 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), + ) + +pkt = RRMutableAnyNested(b"\x82\x82\x01\x02\x00") +pkt.value.append(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, CBORTagValue) +pkt.value.value.append(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, CBORTagValue) +pkt.value.tag = 43 +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, CBORSimpleValue) +pkt.value.value = 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, CBORMapData) +pkt.value["a"].append(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_result(self, pkt): + type(self).calls += 1 + return super().build_result(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) + ) + +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_SEQUENCE_OF builds each child root exactly once +class RRCountedSequenceParent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_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 -+ CBORF_SEMANTIC_TAG += 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 -= 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 -from scapy.cborpacket import CBOR_Packet += Nested mutable CBORF_ANY defaults are isolated between packet instances +class RRNestedMutableDefault(CBOR_Packet): + CBOR_root = CBORF_ANY("value", [[0]]) -class TaggedPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG("tag_info", None, 1, CBORF_INTEGER("ts", 0)) +a = RRNestedMutableDefault() +b = RRNestedMutableDefault() +a.value[0].append(1) +assert b.value == [[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' += Mutable semantic-tag defaults are isolated between packet instances +class RRMutableTagDefault(CBOR_Packet): + CBOR_root = CBORF_ANY("value", CBORTagValue(1, [])) -+ CBOR_Packet / CBORF field integration +a = RRMutableTagDefault() +b = RRMutableTagDefault() +a.value.value.append(1) +assert b.value == CBORTagValue(1, []) -= CBOR_Packet fields_desc built from CBORF_ARRAY -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet += 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 -class Demo(CBOR_Packet): += Two adjacent unbounded positional sequences are rejected as ambiguous +try: + CBORF_ARRAY( + CBORF_SEQUENCE_OF( + "left", + [], + CBORF_UNSIGNED_INTEGER("left_item", None), + ), + CBORF_SEQUENCE_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("id", 1), - CBORF_TEXT_STRING("desc", "demo"), + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("conditional_value", 9), + lambda pkt: pkt.getfieldval("flag") == 1, + ), + CBORF_UNSIGNED_INTEGER("tail", 0), ) -# 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 +pkt = RRConditionalDefaultAbsent(b"\x82\x00\x07") +pkt.tail = 8 +assert bytes(pkt) == b"\x82\x00\x08" -= CBOR_Packet roundtrip preserves raw bytes -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet += 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), + ) -class Simple(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER("a", 3), - CBORF_INTEGER("b", 7), +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 +class RRMutableMapDefault(CBOR_Packet): + CBOR_root = CBORF_ANY( + "value", + CBORMapData([(CBOR_TEXT_STRING("a"), [])]), ) -pkt = Simple() -raw_data = bytes(pkt) -pkt2 = Simple(raw_data) -assert bytes(pkt2) == raw_data +a = RRMutableMapDefault() +b = RRMutableMapDefault() +a.value["a"].append(1) +assert b.value["a"] == [] -########### Additional Unit Tests #################################### += 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 -+ CBOR Simple Values ++ Finding 1 - CBOR sentinel identity survives Scapy copying -= 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'' - -= 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'' - -= 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'' - -+ CBOR Float Encodings - RFC 8949 Test Vectors - -= 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') += CBOR_ABSENT and CBOR_UNDEFINED_VALUE survive Packet.copy and deepcopy +import copy +from scapy.cbor import CBOR_UNDEFINED_VALUE +from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_optional, CBOR_ABSENT +from scapy.cborpacket import CBOR_Packet -= RFC 8949: encode 1 -obj = CBOR_UNSIGNED_INTEGER(1) -bytes(obj) == bytes.fromhex('01') +class OptionalAnyCopy(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", CBOR_ABSENT)), + ) -= RFC 8949: encode 10 -obj = CBOR_UNSIGNED_INTEGER(10) -bytes(obj) == bytes.fromhex('0a') +class UndefinedAnyCopy(CBOR_Packet): + CBOR_root = CBORF_ANY("value", CBOR_UNDEFINED_VALUE) -= RFC 8949: encode 23 -obj = CBOR_UNSIGNED_INTEGER(23) -bytes(obj) == bytes.fromhex('17') +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: encode 24 -obj = CBOR_UNSIGNED_INTEGER(24) -bytes(obj) == bytes.fromhex('1818') +undefined = UndefinedAnyCopy(b"\xf7") +assert undefined.getfieldval("value") is CBOR_UNDEFINED_VALUE +assert undefined.copy().getfieldval("value") is CBOR_UNDEFINED_VALUE +assert copy.deepcopy(undefined).getfieldval("value") is CBOR_UNDEFINED_VALUE +assert bytes(undefined.copy()) == b"\xf7" -= RFC 8949: encode 25 -obj = CBOR_UNSIGNED_INTEGER(25) -bytes(obj) == bytes.fromhex('1819') += CBOR structural sentinels preserve singleton identity under copy operations +import copy +from scapy.cbor import CBOR_NO_ITEM, CBOR_UNDEFINED_VALUE +from scapy.cbor.cborfields import CBOR_ABSENT -= RFC 8949: encode 100 -obj = CBOR_UNSIGNED_INTEGER(100) -bytes(obj) == bytes.fromhex('1864') +for sentinel in (CBOR_ABSENT, CBOR_UNDEFINED_VALUE, CBOR_NO_ITEM): + assert copy.copy(sentinel) is sentinel + assert copy.deepcopy(sentinel) is sentinel -= RFC 8949: encode 1000 -obj = CBOR_UNSIGNED_INTEGER(1000) -bytes(obj) == bytes.fromhex('1903e8') += 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 -= RFC 8949: encode 1000000 -obj = CBOR_UNSIGNED_INTEGER(1000000) -bytes(obj) == bytes.fromhex('1a000f4240') +class OptionalAnyFreshDefault(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("value", CBOR_ABSENT)), + ) -= RFC 8949: encode 1000000000000 -obj = CBOR_UNSIGNED_INTEGER(1000000000000) -bytes(obj) == bytes.fromhex('1b000000e8d4a51000') +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" -= RFC 8949: encode 18446744073709551615 (2^64-1) -obj = CBOR_UNSIGNED_INTEGER(18446744073709551615) -bytes(obj) == bytes.fromhex('1bffffffffffffffff') += Undefined values nested in a generic map survive packet copies +from scapy.cbor import CBOR_UNDEFINED_VALUE +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet -= RFC 8949: encode -1 -obj = CBOR_NEGATIVE_INTEGER(-1) -bytes(obj) == bytes.fromhex('20') +class AnyUndefinedMap(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= RFC 8949: encode -10 -obj = CBOR_NEGATIVE_INTEGER(-10) -bytes(obj) == bytes.fromhex('29') +wire = b"\xa1\x61u\xf7" +pkt = AnyUndefinedMap(wire) +assert pkt.value["u"] is CBOR_UNDEFINED_VALUE +clone = pkt.copy() +assert clone.value["u"] is CBOR_UNDEFINED_VALUE +assert bytes(clone) == wire -= RFC 8949: encode -100 -obj = CBOR_NEGATIVE_INTEGER(-100) -bytes(obj) == bytes.fromhex('3863') ++ Finding 2 - Positional reservation must protect trailing required fields -= RFC 8949: encode -1000 -obj = CBOR_NEGATIVE_INTEGER(-1000) -bytes(obj) == bytes.fromhex('3903e7') += 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_SEQUENCE, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -= RFC 8949: decode 0 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('00')) -obj.val == 0 and remainder == b'' +class OptionalBoolThenAnyArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("required", CBOR_ABSENT), + ) -= RFC 8949: decode 23 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('17')) -obj.val == 23 and remainder == b'' +class OptionalBoolThenAnySequence(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("required", CBOR_ABSENT), + ) -= RFC 8949: decode 24 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('1818')) -obj.val == 24 and remainder == b'' +# One item is available and the required trailing field needs exactly one item. +# Therefore the optional Boolean must be absent even though the item is Boolean. +arr = OptionalBoolThenAnyArray(b"\x81\xf5") +assert arr.getfieldval("maybe") is CBOR_ABSENT +assert arr.required is True +assert bytes(arr) == b"\x81\xf5" -= RFC 8949: decode 1000000000000 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('1b000000e8d4a51000')) -obj.val == 1000000000000 and remainder == b'' +seq = OptionalBoolThenAnySequence(b"\xf5") +assert seq.getfieldval("maybe") is CBOR_ABSENT +assert seq.required is True +assert bytes(seq) == b"\xf5" -= RFC 8949: decode -1000 -obj, remainder = CBOR_Codecs.CBOR.dec(bytes.fromhex('3903e7')) -obj.val == -1000 and remainder == b'' += 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 -+ CBOR Byte String with All Byte Values +class OptionalBoolThenAnyIndefinite(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_ANY("required", CBOR_ABSENT), + ) -= 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'' +pkt = OptionalBoolThenAnyIndefinite(b"\x9f\xf5\xff") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required is True +assert bytes(pkt) == b"\x9f\xf5\xff" -= 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 += 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_SEQUENCE, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -+ CBOR Map with Integer Keys +class OptionalAnyThenBoolArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_ANY("maybe", CBOR_ABSENT)), + CBORF_BOOLEAN("required", None), + ) -= 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'' +class OptionalAnyThenBoolSequence(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_optional(CBORF_ANY("maybe", CBOR_ABSENT)), + CBORF_BOOLEAN("required", None), + ) -= 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'} +arr = OptionalAnyThenBoolArray(b"\x81\xf5") +assert arr.getfieldval("maybe") is CBOR_ABSENT +assert arr.required is True -= 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'' +seq = OptionalAnyThenBoolSequence(b"\xf5") +assert seq.getfieldval("maybe") is CBOR_ABSENT +assert seq.required is True -+ CBOR Multiple Items in Stream += 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 -= 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'' +class BooleanChild(CBOR_Packet): + CBOR_root = CBORF_BOOLEAN("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'' +class OptionalPacketThenAny(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_PACKET("child", None, BooleanChild)), + CBORF_ANY("required", CBOR_ABSENT), + ) -+ CBOR Nested Structures Unit Tests +pkt = OptionalPacketThenAny(b"\x81\xf5") +assert pkt.getfieldval("child") is CBOR_ABSENT +assert pkt.required is True -= 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'' += Nonterminal SEQUENCE_OF stops at a typed delimiter in an unframed sequence +from scapy.cbor.cborfields import ( + CBORF_SEQUENCE, + CBORF_SEQUENCE_OF, + CBORF_TEXT_STRING, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -= 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'' +class IntSequenceThenText(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_SEQUENCE_OF("items", [], CBORF_UNSIGNED_INTEGER), + CBORF_TEXT_STRING("tail", ""), + ) -= 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 = IntSequenceThenText(b"\x01\x02\x61x") +assert pkt.items == [1, 2] +assert pkt.tail == "x" +assert bytes(pkt) == b"\x01\x02\x61x" -########### Extended Interoperability Tests with cbor2 ################ += Ambiguous unbounded array schema is rejected even with an optional field between sequences +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_BOOLEAN, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) -+ CBOR Interoperability - RFC 8949 Appendix B (Scapy encode, cbor2 decode) +try: + CBORF_ARRAY( + CBORF_SEQUENCE_OF("left", [], CBORF_UNSIGNED_INTEGER), + CBORF_optional(CBORF_BOOLEAN("middle", None)), + CBORF_SEQUENCE_OF("right", [], CBORF_UNSIGNED_INTEGER), + ) +except ValueError: + pass +else: + raise AssertionError("ambiguous separated unbounded sequences were accepted") -= RFC 8949 Appendix B: 0 -import cbor2 -obj = CBOR_UNSIGNED_INTEGER(0) -cbor2.loads(bytes(obj)) == 0 ++ Finding 8 - Nested cbor_build_result must preserve a valid child raw cache -= RFC 8949 Appendix B: 1 -obj = CBOR_UNSIGNED_INTEGER(1) -cbor2.loads(bytes(obj)) == 1 += 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 -= RFC 8949 Appendix B: 10 -obj = CBOR_UNSIGNED_INTEGER(10) -cbor2.loads(bytes(obj)) == 10 +class OverlongUintChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= RFC 8949 Appendix B: 23 -obj = CBOR_UNSIGNED_INTEGER(23) -cbor2.loads(bytes(obj)) == 23 +class ParentWithRawCachedChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_PACKET("child", None, OverlongUintChild), + ) -= RFC 8949 Appendix B: 24 -obj = CBOR_UNSIGNED_INTEGER(24) -cbor2.loads(bytes(obj)) == 24 +# 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" -= RFC 8949 Appendix B: 1000 -obj = CBOR_UNSIGNED_INTEGER(1000) -cbor2.loads(bytes(obj)) == 1000 +# 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 pkt.child.cbor_build_result().data == bytes(pkt.child) -= RFC 8949 Appendix B: 1000000000000 -obj = CBOR_UNSIGNED_INTEGER(1000000000000) -cbor2.loads(bytes(obj)) == 1000000000000 += 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 -= RFC 8949 Appendix B: 18446744073709551615 (max u64) -obj = CBOR_UNSIGNED_INTEGER(18446744073709551615) -cbor2.loads(bytes(obj)) == 18446744073709551615 +class IndefiniteArrayChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("value", 0), + ) -= RFC 8949 Appendix B: -1 -obj = CBOR_NEGATIVE_INTEGER(-1) -cbor2.loads(bytes(obj)) == -1 +class ParentWithIndefiniteChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_PACKET("child", None, IndefiniteArrayChild), + ) -= RFC 8949 Appendix B: -1000 -obj = CBOR_NEGATIVE_INTEGER(-1000) -cbor2.loads(bytes(obj)) == -1000 +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" -= RFC 8949 Appendix B: false -obj = CBOR_FALSE() -cbor2.loads(bytes(obj)) is False += SEQUENCE_OF preserves raw representations of untouched packet children +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet -= RFC 8949 Appendix B: true -obj = CBOR_TRUE() -cbor2.loads(bytes(obj)) is True +class SequenceArrayChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("value", 0), + ) -= RFC 8949 Appendix B: null -obj = CBOR_NULL() -cbor2.loads(bytes(obj)) is None +class ParentWithChildSequence(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_SEQUENCE_OF("children", [], SequenceArrayChild), + ) -= RFC 8949 Appendix B: undefined -obj = CBOR_UNDEFINED() -decoded = cbor2.loads(bytes(obj)) -from cbor2 import undefined -decoded is undefined +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" -= RFC 8949 Appendix B: empty byte string -obj = CBOR_BYTE_STRING(b'') -cbor2.loads(bytes(obj)) == b'' += 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 -= 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' +class MutableUintChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= RFC 8949 Appendix B: empty text string -obj = CBOR_TEXT_STRING('') -cbor2.loads(bytes(obj)) == '' +class ParentWithMutableChild(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("sibling", 0), + CBORF_PACKET("child", None, MutableUintChild), + ) -= RFC 8949 Appendix B: 'a' -obj = CBOR_TEXT_STRING('a') -cbor2.loads(bytes(obj)) == 'a' +pkt = ParentWithMutableChild(b"\x82\x00\x18\x01") +pkt.child.value = 2 +assert bytes(pkt) == b"\x82\x00\x02" -= RFC 8949 Appendix B: 'IETF' -obj = CBOR_TEXT_STRING('IETF') -cbor2.loads(bytes(obj)) == 'IETF' ++ Additional CBOR API blind spots -= RFC 8949 Appendix B: u00fc (ü) -obj = CBOR_TEXT_STRING('\u00fc') -cbor2.loads(bytes(obj)) == '\u00fc' += Optional semantic tag honors an absent default instead of forcing tag presence +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEMANTIC_TAG, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, + CBOR_ABSENT, +) +from scapy.cborpacket import CBOR_Packet -= RFC 8949 Appendix B: u6c34 (water in Chinese) -obj = CBOR_TEXT_STRING('\u6c34') -cbor2.loads(bytes(obj)) == '\u6c34' +class OptionalSemanticTagDefault(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag", + CBOR_ABSENT, + 1, + CBORF_UNSIGNED_INTEGER("value", 0), + ) + ) + ) -= RFC 8949 Appendix B: empty array -from scapy.cbor.cborcodec import CBORcodec_ARRAY -enc = CBORcodec_ARRAY.enc([]) -cbor2.loads(enc) == [] +pkt = OptionalSemanticTagDefault() +assert pkt.getfieldval("tag") is CBOR_ABSENT +assert bytes(pkt) == b"\x80" -= RFC 8949 Appendix B: [1, 2, 3] -enc = CBORcodec_ARRAY.enc([1, 2, 3]) -cbor2.loads(enc) == [1, 2, 3] += 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 -= 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]] +class OneKeyMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("x", 0), + ) -= RFC 8949 Appendix B: empty map -from scapy.cbor.cborcodec import CBORcodec_MAP -enc = CBORcodec_MAP.enc({}) -cbor2.loads(enc) == {} +try: + OneKeyMap(b"\xa2\x61x\x01\x61x\x02") +except CBOR_Decoding_Error: + pass +else: + raise AssertionError("duplicate fixed-map key was silently accepted") -= RFC 8949 Appendix B: {1: 2, 3: 4} -enc = CBORcodec_MAP.enc({1: 2, 3: 4}) -cbor2.loads(enc) == {1: 2, 3: 4} ++ Finding 10 - Indefinite arrays should scale linearly without repeated suffix pre-decodes -= 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]} += 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 -+ CBOR Interoperability - RFC 8949 Appendix B (cbor2 encode, Scapy decode) +many_fields = [CBORF_UNSIGNED_INTEGER("v%d" % i, 0) for i in range(64)] -= 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) +class IndefiniteManyInts(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE(*many_fields) -= RFC 8949 Appendix B decode: 23 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(23)) -obj.val == 23 and isinstance(obj, CBOR_UNSIGNED_INTEGER) +orig_span = cborfields.cbor_item_span +span_input_sizes = [] -= RFC 8949 Appendix B decode: 24 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(24)) -obj.val == 24 and isinstance(obj, CBOR_UNSIGNED_INTEGER) +def counted_span(data): + span_input_sizes.append(len(data)) + return orig_span(data) -= RFC 8949 Appendix B decode: -1 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(-1)) -obj.val == -1 and isinstance(obj, CBOR_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 OptionalBoolThenFloat(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_BOOLEAN("maybe", None)), + CBORF_FLOAT("required", 0.0), + ) -= RFC 8949 Appendix B decode: -1000 -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(-1000)) -obj.val == -1000 and isinstance(obj, CBOR_NEGATIVE_INTEGER) +class OptionalNullThenBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_NULL("maybe")), + CBORF_BOOLEAN("required", None), + ) -= RFC 8949 Appendix B decode: false -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(False)) -isinstance(obj, CBOR_FALSE) and obj.val is False +class OptionalUndefinedThenBool(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_optional(CBORF_UNDEFINED("maybe")), + CBORF_BOOLEAN("required", None), + ) -= RFC 8949 Appendix B decode: true -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(True)) -isinstance(obj, CBOR_TRUE) and obj.val is True +# 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 -= RFC 8949 Appendix B decode: null -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps(None)) -isinstance(obj, CBOR_NULL) and obj.val is None +pkt = OptionalNullThenBool(b"\x81\xf5") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required is True -= RFC 8949 Appendix B decode: empty string -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('')) -isinstance(obj, CBOR_TEXT_STRING) and obj.val == '' +pkt = OptionalUndefinedThenBool(b"\x81\xf4") +assert pkt.getfieldval("maybe") is CBOR_ABSENT +assert pkt.required is False -= RFC 8949 Appendix B decode: 'IETF' -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('IETF')) -isinstance(obj, CBOR_TEXT_STRING) and obj.val == 'IETF' += 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 -= RFC 8949 Appendix B decode: u00fc -obj, _ = CBOR_Codecs.CBOR.dec(cbor2.dumps('\u00fc')) -isinstance(obj, CBOR_TEXT_STRING) and obj.val == '\u00fc' +class AnyMapWithSibling(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("sibling", 0), + ) -= 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' +wire = b"\x82\xa1\x01\x02\x00" +pkt = AnyMapWithSibling(wire) +assert pkt.value[1] == 2 +pkt.sibling = 1 +assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" -= 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 += 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 -= 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) +class AnyArrayWithSibling(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_ANY("value", None), + CBORF_UNSIGNED_INTEGER("sibling", 0), + ) -= 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) +pkt = AnyArrayWithSibling(b"\x82\x82\x01\x02\x00") +pkt.value.append(3) +assert bytes(pkt) == b"\x82\x83\x01\x02\x03\x00" -+ CBOR Interoperability - Byte-exact Comparison += 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 -= Scapy and cbor2 produce identical bytes for integer 0 -import cbor2 -bytes(CBOR_UNSIGNED_INTEGER(0)) == cbor2.dumps(0) +class TypedKeyMap(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) -= Scapy and cbor2 produce identical bytes for integer 255 -bytes(CBOR_UNSIGNED_INTEGER(255)) == cbor2.dumps(255) +pkt = TypedKeyMap(b"\xa2\x01\x61i\xf5\x61b") +assert pkt.value[1] == "i" +assert pkt.value[True] == "b" +assert len(pkt.value.cbor_pairs()) == 2 +assert bytes(pkt) == b"\xa2\x01\x61i\xf5\x61b" -= Scapy and cbor2 produce identical bytes for -1 -bytes(CBOR_NEGATIVE_INTEGER(-1)) == cbor2.dumps(-1) ++ Deterministic CBOR and float edge cases -= Scapy and cbor2 produce identical bytes for -1000 -bytes(CBOR_NEGATIVE_INTEGER(-1000)) == cbor2.dumps(-1000) ++ Deterministic CBOR: large binary64 and indefinite map key order -= Scapy and cbor2 produce identical bytes for empty byte string -bytes(CBOR_BYTE_STRING(b'')) == cbor2.dumps(b'') += Large binary64 values do not crash the deterministic scanner +import struct +from scapy.cbor.cborcodec import cbor_find_non_deterministic -= Scapy and cbor2 produce identical bytes for 'hello' -bytes(CBOR_TEXT_STRING('hello')) == cbor2.dumps('hello') +# RFC 8949 Appendix A example: 1.0e+300 as binary64 +wire = bytes.fromhex("fb7e37e43c8800759c") +assert cbor_find_non_deterministic(wire) == [] -= Scapy and cbor2 produce identical bytes for true -bytes(CBOR_TRUE()) == cbor2.dumps(True) +wire = struct.pack(">B", 0xfb) + struct.pack(">d", -1e300) +assert cbor_find_non_deterministic(wire) == [] -= Scapy and cbor2 produce identical bytes for false -bytes(CBOR_FALSE()) == cbor2.dumps(False) += Indefinite maps require bytewise lexicographic key order +from scapy.cbor.cborcodec import cbor_find_non_deterministic -= Scapy and cbor2 produce identical bytes for null -bytes(CBOR_NULL()) == cbor2.dumps(None) +assert not cbor_find_non_deterministic(bytes.fromhex("bf616101616202ff")) +assert cbor_find_non_deterministic(bytes.fromhex("bf616201616102ff")) -= Scapy and cbor2 produce identical bytes for undefined -from cbor2 import undefined -bytes(CBOR_UNDEFINED()) == cbor2.dumps(undefined) += NaN preferred width uses the original payload bit pattern +from scapy.cbor.cborcodec import cbor_find_non_deterministic -= Scapy and cbor2 produce identical bytes for empty array -from scapy.cbor.cborcodec import CBORcodec_ARRAY -CBORcodec_ARRAY.enc([]) == cbor2.dumps([]) +# binary64 NaN with a low payload bit cannot shorten to binary16/32 +assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000000000001")) == [] -= 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 +# binary64 quiet NaN with only top significand bits set prefers binary16 +assert cbor_find_non_deterministic(bytes.fromhex("fb7ffc000000000000")) -= 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 -########### CBORF Fields Interoperability Tests with cbor2 ############ ++ Cache item counts and packet-field cardinality -+ CBORF Fields - Interop: CBORF_ARRAY packet to cbor2 ++ Cached unframed packets preserve exact bytes and item counts -= CBORF_ARRAY packet to cbor2 list (version info) -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER, CBORF_TEXT_STRING += Unframed SEQUENCE cache returns exact bytes without rebuild +from scapy.cbor.cborfields import ( + CBORF_PACKET, + CBORF_SEQUENCE, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet -class VersionInfo(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER('major', 1), - CBORF_UNSIGNED_INTEGER('minor', 2), - CBORF_UNSIGNED_INTEGER('patch', 3), +class SeqChild(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_UNSIGNED_INTEGER("b", 0), ) -pkt = VersionInfo() -raw = bytes(pkt) -dec = cbor2.loads(raw) -isinstance(dec, list) and dec == [1, 2, 3] +# 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 child._cbor_raw_cache_items == 2 +result = child.cbor_build_result() +assert result.data == overlong +assert result.items == 2 +assert result.data == bytes(child) + +# 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 -= cbor2 list to CBORF_ARRAY packet -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_UNSIGNED_INTEGER += CBORF_PACKET build_value enforces one-item cardinality like build_result +from scapy.cbor.cborfields import ( + CBORF_PACKET, + CBORF_SEQUENCE, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet -class VersionInfo2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_UNSIGNED_INTEGER('major', 0), - CBORF_UNSIGNED_INTEGER('minor', 0), - CBORF_UNSIGNED_INTEGER('patch', 0), +class TwoItemChild(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_UNSIGNED_INTEGER("a", 1), + CBORF_UNSIGNED_INTEGER("b", 2), ) -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 - -= CBORF_ARRAY packet roundtrip through cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +class OneItemChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("a", 1) -class MsgPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 200), - CBORF_TEXT_STRING('status', 'ok'), - ) +fld = CBORF_PACKET("x", None, pkt_cls=OneItemChild) +ok = fld.build_value(None, OneItemChild(a=7)) +assert ok.items == 1 -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' +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_ARRAY with boolean and null fields to cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_BOOLEAN, CBORF_NULL, CBORF_INTEGER += CBORF_PACKET rejects non-CBOR Packet/bytes that are not exactly one item +from scapy.cbor.cborfields import CBORF_PACKET +from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet +from scapy.packet import Raw -class FlagPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('id', 7), - CBORF_BOOLEAN('active', True), - CBORF_NULL('reserved'), - ) - -pkt = FlagPkt() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec[0] == 7 and dec[1] is True and dec[2] is None - -= cbor2 list with mixed types to CBORF_ARRAY packet -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_BOOLEAN, CBORF_NULL -from scapy.cborpacket import CBOR_Packet - -class Mixed(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('num', 0), - CBORF_BOOLEAN('flag', False), - CBORF_NULL('nval'), - ) - -cbor2_data = cbor2.dumps([42, False, None]) -pkt = Mixed(cbor2_data) -pkt.num.val == 42 +fld = CBORF_PACKET("x", None, pkt_cls=CBOR_Packet) -+ CBORF Fields - Interop: CBORF_MAP packet to cbor2 - -= CBORF_MAP packet to cbor2 dict -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +# 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 -class ClaimSet(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', 'scapy'), - CBORF_INTEGER('exp', 9999999), - ) +# Illegal standalone break +try: + fld.build_value(None, Raw(b"\xff")) + assert False, "bare break must be rejected" +except CBOR_Encoding_Error: + pass -pkt = ClaimSet() -raw = bytes(pkt) -dec = cbor2.loads(raw) -isinstance(dec, dict) and dec.get('iss') == 'scapy' and dec.get('exp') == 9999999 +# Truncated CBOR +try: + fld.build_value(None, Raw(b"\x18")) + assert False, "truncated CBOR must be rejected" +except CBOR_Encoding_Error: + pass -= cbor2 dict to CBORF_MAP packet -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet +# 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" -class Claims(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', ''), - CBORF_INTEGER('exp', 0), - ) -cbor2_data = cbor2.dumps({'iss': 'myapp', 'exp': 12345}) -pkt = Claims(cbor2_data) -pkt.iss.val == 'myapp' and pkt.exp.val == 12345 ++ Scapy-native packet ownership -= CBORF_MAP packet roundtrip through cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING += 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 -class BinHeader(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('alg', 'ES256'), - CBORF_BYTE_STRING('kid', b'\x01\x02\x03\x04'), - ) - -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' - -= CBORF_MAP with boolean values to cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_BOOLEAN, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet +class OwnedChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -class Flags(CBOR_Packet): +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 + ++ 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_SEQUENCE_OF construction attaches every packet child to parent +from scapy.cbor.cborfields import CBORF_SEQUENCE_OF + +class SequenceParent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_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_SEQUENCE_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 scapy.cbor.cborcodec import cbor_find_non_deterministic +from scapy.cbor.cborfields import CBORF_MAP + +class ReverseDeclaredMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_BOOLEAN('enabled', True), - CBORF_INTEGER('count', 5), + CBORF_UNSIGNED_INTEGER("b", 1), + CBORF_UNSIGNED_INTEGER("a", 2), ) -pkt = Flags() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec.get('enabled') is True and dec.get('count') == 5 +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) == [] -= cbor2 dict with unknown keys: CBORF_MAP skips them -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +########### Scapy-native conversion pipeline ################# -class SimpleMap(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('known', 'default'), - ) ++ Field defaults and i2m / RawVal -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 = 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 -from scapy.cborpacket import CBOR_Packet - -class IntList2(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) - -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 - -+ 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 -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 - -= CBORF_UNSIGNED_INTEGER boundary values - Scapy encode, cbor2 decode -import cbor2 += Field defaults are normalized through any2i like native Scapy from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class UIntPkt(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) +class DefaultNormPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", "12") -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) +assert DefaultNormPkt().value == 12 +assert DefaultNormPkt(value="34").value == 34 +assert bytes(DefaultNormPkt()) == b"\x0c" -all(results) - -= 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 - -class UIntPkt2(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) - -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) - -= CBORF_UNSIGNED_INTEGER byte-exact comparison with cbor2 -import cbor2 += 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 UIntExact(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) - -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)) - -all(results) - -+ CBORF Fields - Interop: CBORF_NEGATIVE_INTEGER with cbor2 - -= CBORF_NEGATIVE_INTEGER boundary values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_NEGATIVE_INTEGER -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) - -all(results) - -= 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 NIntPkt2(CBOR_Packet): - CBOR_root = CBORF_NEGATIVE_INTEGER('n', -1) - -results = [] -for val in [-1, -24, -25, -256, -257, -65536, -4294967296]: - pkt = NIntPkt2(cbor2.dumps(val)) - results.append(pkt.n.val == val) - -all(results) - -= CBORF_NEGATIVE_INTEGER byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_NEGATIVE_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) - -+ CBORF Fields - Interop: CBORF_INTEGER with cbor2 - -= CBORF_INTEGER positive values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet - -class IntPkt(CBOR_Packet): - CBOR_root = CBORF_INTEGER('n', 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) - -all(results) - -= CBORF_INTEGER negative values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet - -class IntNegPkt(CBOR_Packet): - CBOR_root = CBORF_INTEGER('n', -1) - -results = [] -for val in [-1, -10, -100, -1000, -1000000]: - pkt = IntNegPkt() - pkt.n.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) - -all(results) - -= CBORF_INTEGER - cbor2 encode positive and negative, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_INTEGER -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) - -all(results) - -+ CBORF Fields - Interop: CBORF_BYTE_STRING with cbor2 - -= CBORF_BYTE_STRING empty bytes - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING -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'' - -= CBORF_BYTE_STRING all 256 byte values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING -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)) - -= CBORF_BYTE_STRING - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BYTE_STRING -from scapy.cborpacket import CBOR_Packet - -class BytePkt3(CBOR_Packet): - CBOR_root = CBORF_BYTE_STRING('data', b'') +class RawValPkt(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 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 +assert bytes(RawValPkt(value=RawVal(b"\x18\x64"))) == b"\x18\x64" -True +try: + bytes(RawValPkt(value=RawVal(b"\x01\x02"))) + assert False, "multi-item RawVal must be rejected" +except CBOR_Encoding_Error: + pass -= CBORF_BYTE_STRING byte-exact comparison with cbor2 -import cbor2 += 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 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) - -+ CBORF Fields - Interop: CBORF_TEXT_STRING with cbor2 - -= CBORF_TEXT_STRING empty string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +class BstrPkt(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("blob", b"ABC") -class TextPkt(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') +assert BstrPkt().blob == b"ABC" +assert bytes(BstrPkt()) == b"\x43" + b"ABC" -pkt = TextPkt() -dec = cbor2.loads(bytes(pkt)) -dec == '' ++ CBORF_BYTE_STRING_PACKET default normalization -= CBORF_TEXT_STRING ASCII string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING += 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 TextPkt2(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') - -pkt = TextPkt2() -pkt.txt.val = 'Hello, World!' -dec = cbor2.loads(bytes(pkt)) -dec == 'Hello, World!' - -= CBORF_TEXT_STRING unicode string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet - -class TextUniPkt(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') - -pkt = TextUniPkt() -pkt.txt.val = u'Hello, \u4e16\u754c' -dec = cbor2.loads(bytes(pkt)) -dec == u'Hello, \u4e16\u754c' - -= CBORF_TEXT_STRING - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet - -class TextPkt3(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') - -for s in ['', 'hello', 'Hello, World!', u'caf\u00e9', u'\u4e16\u754c']: - pkt = TextPkt3(cbor2.dumps(s)) - assert pkt.txt.val == s - -True - -= CBORF_TEXT_STRING byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet - -class TextExact(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') - -results = [] -for s in ['', 'a', 'hello', 'IETF', u'\u6c34']: - pkt = TextExact() - pkt.txt.val = s - results.append(bytes(pkt) == cbor2.dumps(s)) - -all(results) - -+ CBORF Fields - Interop: CBORF_BOOLEAN with cbor2 - -= CBORF_BOOLEAN true - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet - -class BoolPkt(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', True) - -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 - -class BoolFalsePkt(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', False) - -pkt = BoolFalsePkt() -dec = cbor2.loads(bytes(pkt)) -dec is False - -= CBORF_BOOLEAN - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet - -class BoolPkt2(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', False) - -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 - -= CBORF_BOOLEAN byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet - -class BoolExactTrue(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', True) - -class BoolExactFalse(CBOR_Packet): - CBOR_root = CBORF_BOOLEAN('flag', False) - -pkt_t = BoolExactTrue() -pkt_f = BoolExactFalse() -bytes(pkt_t) == cbor2.dumps(True) and bytes(pkt_f) == cbor2.dumps(False) - -+ CBORF Fields - Interop: CBORF_NULL with cbor2 - -= CBORF_NULL - Scapy encode, cbor2 decode gives None -import cbor2 -from scapy.cbor.cborfields import CBORF_NULL -from scapy.cborpacket import CBOR_Packet - -class NullPkt(CBOR_Packet): - CBOR_root = CBORF_NULL('n') - -pkt = NullPkt() -dec = cbor2.loads(bytes(pkt)) -dec is None - -= CBORF_NULL byte-exact comparison with cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_NULL -from scapy.cborpacket import CBOR_Packet - -class NullExact(CBOR_Packet): - CBOR_root = CBORF_NULL('n') - -pkt = NullExact() -bytes(pkt) == cbor2.dumps(None) - -= 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 -from scapy.cborpacket import CBOR_Packet - -class NullPkt2(CBOR_Packet): - CBOR_root = CBORF_NULL('n') - -pkt = NullPkt2(cbor2.dumps(None)) -isinstance(pkt.n, CBOR_NULL) - -+ CBORF Fields - Interop: CBORF_FLOAT with cbor2 - -= CBORF_FLOAT basic values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet - -class FloatPkt(CBOR_Packet): - CBOR_root = CBORF_FLOAT('f', 0.0) - -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) - -all(results) - -= 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 FloatSpecialPkt(CBOR_Packet): - CBOR_root = CBORF_FLOAT('f', 0.0) - -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') - -= CBORF_FLOAT special values - cbor2 encode, Scapy decode -import cbor2, math -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_FLOAT -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), - ) - -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') - -= CBORF_FLOAT - cbor2 encode, Scapy decode roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_FLOAT -from scapy.cborpacket import CBOR_Packet - -class FloatPkt2(CBOR_Packet): - CBOR_root = CBORF_FLOAT('f', 0.0) - -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) - -all(results) - -+ CBORF Fields - Interop: CBORF_ARRAY with cbor2 - -= CBORF_ARRAY with integer fields - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_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] - -= 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 - -class MixedPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('id', 99), - CBORF_TEXT_STRING('label', 'test'), - CBORF_BOOLEAN('active', True), - ) - -pkt = MixedPkt() -dec = cbor2.loads(bytes(pkt)) -dec[0] == 99 and dec[1] == 'test' and dec[2] is True - -= CBORF_ARRAY - cbor2 encode, Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet - -class RecordPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('msg', ''), +class ByteStringDefaultParent(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING_PACKET( + "child", + b"abc", + pkt_cls=Raw, ) -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 +pkt = ByteStringDefaultParent() +assert isinstance(pkt.child, Raw) +assert pkt.child.load == b"abc" +assert pkt.child.parent is pkt +assert bytes(pkt) == b"\x43abc" -class RTPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('seq', 1), - CBORF_TEXT_STRING('data', 'payload'), - ) -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' ++ PacketListField-style next_cls_cb semantics -= CBORF_ARRAY with null elements - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_NULL += CBORF_SEQUENCE_OF next_cls_cb selects packet classes dynamically +from scapy.cbor.cborfields import CBORF_SEQUENCE_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'), - ) - -pkt = NullArrPkt() -dec = cbor2.loads(bytes(pkt)) -dec[0] == 5 and dec[1] is None - -+ CBORF Fields - Interop: CBORF_ARRAY_OF with cbor2 - -= 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 - -class TextListPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) - -pkt = TextListPkt(cbor2.dumps(['hello', 'world', 'foo'])) -len(pkt.items) == 3 and pkt.items[0].val == 'hello' and pkt.items[2].val == 'foo' - -= 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 - -class TextListPkt2(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_TEXT_STRING) - -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'] - -= 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 - -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' - -= 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 -from scapy.cborpacket import CBOR_Packet - -class ByteListPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_BYTE_STRING) - -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' - -= 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 - -class ByteListPkt2(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_BYTE_STRING) - -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 -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 - -= 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 IntListPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], CBORF_INTEGER) - -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 - -= CBORF_MAP with text string values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet - -class HeaderPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('alg', 'ES256'), - CBORF_TEXT_STRING('typ', 'JWT'), - CBORF_INTEGER('ver', 1), - ) - -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 - -= 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 - -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 -from scapy.cborpacket import CBOR_Packet - -class CoseHeaderPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('alg', 'ES256'), - CBORF_BYTE_STRING('kid', b'\x01\x02\x03\x04'), - CBORF_INTEGER('crit', 1), - ) - -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 -from scapy.cborpacket import CBOR_Packet - -class OptionalPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('id', 7), - CBORF_NULL('optional_data'), - ) - -pkt = OptionalPkt() -dec = cbor2.loads(bytes(pkt)) -dec.get('id') == 7 and dec.get('optional_data') is None - -= 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 - -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' +class DynamicSequenceChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= CBORF_MAP skip unknown keys from cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER -from scapy.cborpacket import CBOR_Packet +NextClsCalls = [] +def choose_dynamic_child(pkt, lst, cur, remain): + NextClsCalls.append(len(lst)) + return DynamicSequenceChild -class KnownKeysPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('known', 'default'), - CBORF_INTEGER('count', 0), +class DynamicSequenceParent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_OF( + "children", + [], + next_cls_cb=choose_dynamic_child, ) -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 +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 -= 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 += CBORF_SEQUENCE_OF rejects combining next_cls_cb with pkt_cls +from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER 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), - ) - -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 -from scapy.cborpacket import CBOR_Packet +class FixedSequenceChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -class DeviceInfo(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('id', 0), - CBORF_TEXT_STRING('label', ''), - CBORF_BOOLEAN('online', False), - CBORF_BYTE_STRING('hwaddr', b''), +try: + CBORF_SEQUENCE_OF( + "children", + [], + pkt_cls=FixedSequenceChild, + next_cls_cb=lambda *a: FixedSequenceChild, ) +except ValueError: + pass +else: + raise AssertionError("conflicting SEQUENCE_OF selectors accepted") -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' -= CBOR_Packet CBORF_MAP full cbor2 roundtrip -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet - -class ClaimsPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', ''), - CBORF_TEXT_STRING('sub', ''), - CBORF_INTEGER('exp', 0), - CBORF_BOOLEAN('admin', False), - ) ++ TypeError must not be used for callback signature probing -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 += CBORF_SEQUENCE_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_SEQUENCE_OF 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), - ) - -pkt = OffsetPkt(cbor2.dumps({'name': 'delta', 'offset': -1024, 'count': 512})) -pkt.name.val == 'delta' and pkt.offset.val == -1024 and pkt.count.val == 512 - -+ 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 -from scapy.cborpacket import CBOR_Packet +BrokenNextClsCalls = [] +def broken_next_cls(*args): + BrokenNextClsCalls.append(len(args)) + raise TypeError("intentional next_cls_cb failure") -class NestInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('x', 0), - CBORF_INTEGER('y', 0), +class BrokenCallbackParent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_OF( + "children", + [], + next_cls_cb=broken_next_cls, ) -class NestMiddle(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('zone', ''), - CBORF_PACKET('point', None, NestInner), +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 NestOuter(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_PACKET('region', None, NestMiddle), - ) +assert BrokenNextClsCalls == [4], BrokenNextClsCalls -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 += Nested packet construction is not retried when the child constructor raises TypeError +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 NestInner2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('x', 0), - CBORF_INTEGER('y', 0), - ) - -class NestMiddle2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('zone', ''), - CBORF_PACKET('point', None, NestInner2), - ) - -class NestOuter2(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_PACKET('region', None, NestMiddle2), - ) +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") -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]]] +class TypeErrorParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, TypeErrorChild) -= 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 -from scapy.cborpacket import CBOR_Packet +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") -class MapInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('px', 0), - CBORF_INTEGER('py', 0), - ) +assert TypeErrorChild.init_calls == [True], TypeErrorChild.init_calls -class MapWithNestedPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('label', ''), - CBORF_PACKET('coords', None, MapInner), - ) -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] ++ fixed-map unknown member preservation -= 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_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 CoordsInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('px', 0), - CBORF_INTEGER('py', 0), - ) - -class CoordsOuter(CBOR_Packet): +class ExtensibleMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('label', ''), - CBORF_PACKET('coords', None, CoordsInner), + CBORF_UNSIGNED_INTEGER("a", 0), ) -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 -from scapy.cborpacket import CBOR_Packet - -class DocMeta(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('creator', ''), - CBORF_INTEGER('version', 0), - ) +# {"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 -class DocPacket(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('title', ''), - CBORF_BYTE_STRING('body', b''), - CBORF_PACKET('metadata', None, DocMeta), - ) +# 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" -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 += 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 DocMeta2(CBOR_Packet): +class ExtensibleNestedMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('creator', ''), - CBORF_INTEGER('version', 0), + CBORF_UNSIGNED_INTEGER("a", 0), ) -class DocPacket2(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('title', ''), - CBORF_BYTE_STRING('body', b''), - CBORF_PACKET('metadata', None, DocMeta2), - ) +# 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 -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 +# 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 - CBORF_ARRAY_OF with CBOR_Packet elements ++ CBOR_Packet.copy re-parents embedded children -= 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 += 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 StatusItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('msg', ''), - ) +class CopyChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -class StatusList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('statuses', [], StatusItem) +class CopyParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, pkt_cls=CopyChild) -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 +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 -= 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', ''), - ) ++ CBORF_MAP deterministic unknown rebuild -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']] - -= 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_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 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 -from scapy.cborpacket import CBOR_Packet - -class EmptyItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('val', 0), - ) - -class EmptyItemList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], EmptyItem) - -pkt = EmptyItemList() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec == [] and len(EmptyItemList(raw).items) == 0 - -= 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 -from scapy.cborpacket import CBOR_Packet - -class EventItem(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): +class DeterministicUnknownMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('source', ''), - CBORF_INTEGER('count', 0), - CBORF_PACKET('log', None, EventLog), + CBORF_UNSIGNED_INTEGER("z", 0), ) -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]] - -+ CBOR_Packet - CBORF_optional extended tests - -= 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 -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 OptPresentPkt(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('version', 0), - CBORF_optional(CBORF_TEXT_STRING('description', '')), - ) - -raw = cbor2.dumps([3, 'hello world']) -pkt = OptPresentPkt(raw) -pkt.version.val == 3 and pkt.description.val == 'hello world' - -= 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 -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 -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'] - -= 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 -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), - ) +# 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 = 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 += 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 ConfigNoOpt(CBOR_Packet): +class NestedUnknownMap(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_INTEGER('timeout', 30), - CBORF_optional(CBORF_TEXT_STRING('endpoint', '')), - CBORF_INTEGER('retries', 3), + CBORF_UNSIGNED_INTEGER("z", 0), ) -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 -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 -from scapy.cborpacket import CBOR_Packet - -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 -from scapy.cborpacket import CBOR_Packet - -class TagRTPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) - -pkt = TagRTPkt() -pkt.ts.val = 1700000000 -raw = bytes(pkt) -pkt2 = TagRTPkt(raw) -pkt2.ts.val == 1700000000 - -= 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 - -class TagBigNum(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 2, CBORF_BYTE_STRING('n', b'')) - -pkt = TagBigNum() -pkt.n.val = b'\x01\x00\x00\x00\x00\x00\x00\x00\x00' -raw = bytes(pkt) -raw[0:1] == b'\xc2' - -= 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 - -class TagCmpPkt(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)) - -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 - -class EventPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('event_type', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) +# 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" -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) -= 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 += CBORF_MAP copy isolates nested unknown extension values from the original packet +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class EventRTPkt(CBOR_Packet): +class MapCopyIsolation(CBOR_Packet): CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('event_type', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), + CBORF_UNSIGNED_INTEGER("a", 0), ) -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 - -= 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 - -class TimedEventArr(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('evt', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) +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._cbor_unknown_map_pairs is not orig._cbor_unknown_map_pairs +assert clone._cbor_unknown_map_pairs[0][1] is not orig._cbor_unknown_map_pairs[0][1] +clone._cbor_unknown_map_pairs[0][1].append(3) +assert len(orig._cbor_unknown_map_pairs[0][1]) == 2 +assert clone._cbor_unknown_map_pairs[0][1] == [1, {"k": 2}, 3] -pkt = TimedEventArr() -pkt.evt.val = 'start' -pkt.ts.val = 1700000000 -dec = cbor2.loads(bytes(pkt)) -dec[0] == 'start' and isinstance(dec[1], datetime.datetime) -+ CBOR_Packet - realistic models ++ CBORF_ARRAY_OF indefinite decoding -= Realistic model: EAT-like attestation token -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BYTE_STRING += 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 EATToken(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('nonce', 0), - CBORF_TEXT_STRING('ueid', ''), - CBORF_BYTE_STRING('boot_seed', b''), - CBORF_INTEGER('hwver', 0), - ) +class IndefUIntArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], pkt_cls=CBORF_UNSIGNED_INTEGER) -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 +wire = b"\x9f\x01\x02\x03\xff" +pkt = IndefUIntArray(wire) +assert pkt.values == [1, 2, 3] +assert bytes(pkt) == wire -= 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 -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 += 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 SensorData(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('sensor_id', 0), - CBORF_FLOAT('temperature', 0.0), - ) +class IndefChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -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 -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 IndefPacketArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("children", [], pkt_cls=IndefChild) -class SensorReport2(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('station', 0), - CBORF_TEXT_STRING('unit', ''), - CBORF_PACKET('reading', None, SensorData2), - ) +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 -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 ++ Medium-severity review follow-ups -= 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 += CBORF_FLOAT preserves received half-float wire after cache clear +from scapy.cbor.cbor import CBORFloatValue +from scapy.cbor.cborfields import CBORF_FLOAT from scapy.cborpacket import CBOR_Packet -class HttpStatus(CBOR_Packet): +class FloatPkt(CBOR_Packet): + CBOR_root = CBORF_FLOAT("value", 0.0) + +wire = b"\xf9\x3e\x00" # 1.5 as half +pkt = FloatPkt(wire) +assert isinstance(pkt.value, CBORFloatValue) +assert abs(pkt.value - 1.5) < 1e-6 +assert pkt.value.cbor_encoded == wire +pkt.raw_packet_cache = None +pkt.raw_packet_cache_fields = None +assert bytes(pkt) == wire +pkt.value = 1.5 +assert bytes(pkt) == wire # preferred half for 1.5 + += Unframed CBORF_SEQUENCE leaves trailing CBOR items +from scapy.cbor.cborfields import CBORF_SEQUENCE, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class TwoInts(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_UNSIGNED_INTEGER("b", 0), + ) + +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_result(TwoInts(), b"\x01\x02\x03").remaining +assert remain == b"\x03" + += CBORF_SEMANTIC_TAG.m2i rejects the wrong tag number +from scapy.cbor.cborfields import ( + CBORF_SEMANTIC_TAG, CBORF_INTEGER, CBOR_Type_Mismatch, +) + +fld = CBORF_SEMANTIC_TAG("tag", None, 1, CBORF_INTEGER("ts", 0)) +try: + fld.m2i(None, b"\xc2\x00") # tag 2 +except CBOR_Type_Mismatch: + pass +else: + raise AssertionError("wrong tag accepted by m2i") + += 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 scapy.cbor.cborcodec import 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 distinguishes +0.0 from -0.0 map keys +from scapy.cbor import CBOR_Codecs + +# {+0.0: 1, -0.0: 2} as half floats +wire = b"\xa2\xf9\x00\x00\x01\xf9\x80\x00\x02" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +md = obj.val +assert md[0.0].val == 1 +assert md[-0.0].val == 2 +assert md[0.0] is not md[-0.0] + += Malformed optional semantic tag does not migrate into trailing CBORF_ANY +from scapy.cbor.cbor import CBOR_Decoding_Error +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 OptionalTaggedBeforeAny(CBOR_Packet): CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('phrase', ''), + CBORF_optional( + CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_UNSIGNED_INTEGER("tagged_value", None), + ) + ), + CBORF_ANY("fallback", None), ) -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): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('status', 0), - CBORF_TEXT_STRING('content_type', ''), - CBORF_INTEGER('content_length', 0), - CBORF_BYTE_STRING('body', b''), - ) +try: + OptionalTaggedBeforeAny(b"\x81\xc1\x61x") +except CBOR_Decoding_Error: + pass +else: + raise AssertionError("Malformed tagged data migrated into the fallback field") -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' +# Matching well-formed optional still yields to reserved trailing ANY. +ok = OptionalTaggedBeforeAny(b"\x81\xc1\x01") +assert ok.getfieldval("tag_number") is CBOR_ABSENT +assert ok.fallback is not None -= 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 += 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 HttpResponse2(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''), - ) +class NamedMap(CBOR_Packet): + CBOR_root = CBORF_MAP(CBORF_UNSIGNED_INTEGER("a", 0)) -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 +# {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") + +# 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_FLOAT preserves non-preferred NaN payload wire after cache clear +from scapy.cbor.cbor import CBORFloatValue +from scapy.cbor.cborfields import CBORF_FLOAT from scapy.cborpacket import CBOR_Packet -class CoseHeader(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('alg', 0), - CBORF_TEXT_STRING('kid', ''), - CBORF_BYTE_STRING('x5t', b''), - ) - -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 +class FloatPkt(CBOR_Packet): + CBOR_root = CBORF_FLOAT("value", 0.0) + +# binary64 NaN with a low payload bit (preferred width stays binary64) +wire = bytes.fromhex("fb7ff8000000000001") +pkt = FloatPkt(wire) +assert isinstance(pkt.value, CBORFloatValue) +assert pkt.value != pkt.value # NaN +assert pkt.value.cbor_encoded == wire +pkt.raw_packet_cache = None +pkt.raw_packet_cache_fields = None +assert bytes(pkt) == wire + += 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 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 TextPkt(CBOR_Packet): + CBOR_root = CBORF_TEXT_STRING("label", "") -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 - -= 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 - -roundtrip_count >= 45 +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 after cache clear +from scapy.cbor.cbor import CBORFloatValue +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet + +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, CBORFloatValue) +assert pkt.value != pkt.value # NaN +assert pkt.value.cbor_encoded == wire +pkt.raw_packet_cache = None +pkt.raw_packet_cache_fields = None +assert bytes(pkt) == wire + += 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) + += 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, + CBORSimpleValue, + CBORTagValue, + CBOR_UNDEFINED_VALUE, + 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 "CBORTagValue" in repr(CBORTagValue(1, "x")) +assert "CBORSimpleValue" in repr(CBORSimpleValue(41)) +assert repr(CBOR_UNDEFINED_VALUE) == "CBOR_UNDEFINED" +assert not CBOR_UNDEFINED_VALUE +assert copy.copy(CBOR_UNDEFINED_VALUE) is CBOR_UNDEFINED_VALUE +assert copy.deepcopy(CBOR_UNDEFINED_VALUE) is CBOR_UNDEFINED_VALUE +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 diff --git a/test/scapy/layers/cbor_cbor2_interop.uts b/test/scapy/layers/cbor_cbor2_interop.uts new file mode 100644 index 00000000000..226d73a5082 --- /dev/null +++ b/test/scapy/layers/cbor_cbor2_interop.uts @@ -0,0 +1,1465 @@ +% 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_UNDEFINED_VALUE, + CBOR_UNSIGNED_INTEGER, + CBORSimpleValue, + CBORTagValue, +) +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 value is CBOR_UNDEFINED_VALUE: + return ("undefined",) + if isinstance(value, CBORSimpleValue): + return ("simple", value.value) + if isinstance(value, CBORTagValue): + return ("tag", value.tag, rr_norm_native(value.value)) + 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_VALUE + if isinstance(value, cbor2.CBORSimpleValue): + return CBORSimpleValue(value.value) + if isinstance(value, cbor2.CBORTag): + return CBORTagValue(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) + + +class RRCbor2TaggedText(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + "tag_number", None, 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 remain distinct encoded map keys ~ external_cbor2 +wire = ( + b"\xa2" + cbor2.dumps(0.0) + cbor2.dumps("positive") + + cbor2.dumps(-0.0) + cbor2.dumps("negative") +) +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_FLOAT) +assert isinstance(pairs[1][0], CBOR_FLOAT) +assert math.copysign(1.0, pairs[0][0].val) == 1.0 +assert math.copysign(1.0, pairs[1][0].val) == -1.0 +assert obj.enc() == wire + += Distinct double-precision NaN payloads survive as separate 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_VALUE, CBORTagValue(60000, 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.tag_number == 60000 +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.append(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[0].append(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 +if isinstance(map_value, CBORMapData): + stored = map_value["x"] +elif isinstance(map_value, dict): + stored = map_value["x"] +else: + stored = dict(map_value)["x"] + +stored.append(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/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