From 773c60b79a7520b6a0bb08799131f8faf9947715 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 13:19:11 +0200 Subject: [PATCH 01/48] cbor: Scapy-native CBOR fields, codec hardening, and cbor2 CI Rewrite the CBOR packet/field layer toward ASN.1-style Scapy APIs, harden maps/floats/optionals, and add pinned cbor2 differential coverage behind an isolated tox/CI job. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- .github/workflows/unittests.yml | 17 + README.md | 3 +- scapy/cbor/__init__.py | 35 + scapy/cbor/cbor.py | 367 +- scapy/cbor/cborcodec.py | 823 ++- scapy/cbor/cborfields.py | 2095 ++++++-- scapy/cborpacket.py | 189 +- test/configs/bsd.utsc | 6 +- test/configs/linux.utsc | 6 +- test/configs/solaris.utsc | 6 +- test/configs/windows.utsc | 6 +- test/configs/windows2.utsc | 6 +- test/fields.uts | 12 + test/scapy/layers/cbor.uts | 5583 ++++++++------------ test/scapy/layers/cbor_cbor2_interop.uts | 1465 +++++ test/scapy/layers/generate_cbor2_corpus.py | 151 + test/scapy/layers/requirements-cbor2.txt | 3 + tox.ini | 12 +- 18 files changed, 7021 insertions(+), 3764 deletions(-) create mode 100644 test/scapy/layers/cbor_cbor2_interop.uts create mode 100755 test/scapy/layers/generate_cbor2_corpus.py create mode 100644 test/scapy/layers/requirements-cbor2.txt 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..55eee021d5e 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,484 @@ 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", [], cls=MyPacket) + CBORF_SEQUENCE_OF("items", [], cls=CBORF_UNSIGNED_INTEGER) + CBORF_SEQUENCE_OF("items", [], next_cls_cb=choose_next) + + ``pkt_cls`` is accepted as an alias of ``cls`` for PacketListField + familiarity. Pass only one of ``cls`` / ``pkt_cls`` / ``next_cls_cb``. + """ + CBOR_tag = None + islist = 1 + + def __init__(self, + name, # type: str + default, # type: Any + cls=None, # type: _ARRAY_T + pkt_cls=None, # type: Optional[Type[Packet]] + 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 cls is not None or pkt_cls is not None: + raise ValueError( + "Pass only next_cls_cb, or only cls/pkt_cls" + ) + self.next_cls_cb = next_cls_cb + self.holds_packets = 1 + else: + if cls is not None and pkt_cls is not None: + raise ValueError("Pass only one of cls or pkt_cls") + chosen = pkt_cls if pkt_cls is not None else 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 cls, 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", [], cls=MyPacket) + CBORF_ARRAY_OF("items", [], cls=CBORF_UNSIGNED_INTEGER) + + ``pkt_cls`` is accepted as an alias of ``cls``. Pass only one of them. """ CBOR_tag = CBOR_MajorTypes.ARRAY islist = 1 @@ -589,30 +1590,39 @@ 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 + cls=None, # type: _ARRAY_T + pkt_cls=None, # type: Optional[Type[Packet]] ): # 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 + if cls is not None and pkt_cls is not None: + raise ValueError("Pass only one of cls or pkt_cls") + chosen = pkt_cls if pkt_cls is not None else cls + if chosen is None: + raise ValueError("Provide cls or 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 + 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 +1631,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 +1700,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 +1708,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 +1741,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 +1768,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 +1953,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,51 +2061,106 @@ 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`. @@ -878,26 +2176,67 @@ def __init__(self, ): # type: (...) -> None self.cls = cls - super(CBORF_PACKET, self).__init__(name, None) - self.default = default + 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..0e4f60dca12 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,2694 @@ 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, 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, 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, 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 - -+ 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 - -class ClaimSet(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', 'scapy'), - CBORF_INTEGER('exp', 9999999), - ) - -pkt = ClaimSet() -raw = bytes(pkt) -dec = cbor2.loads(raw) -isinstance(dec, dict) and dec.get('iss') == 'scapy' and dec.get('exp') == 9999999 +fld = CBORF_PACKET("x", None, cls=CBOR_Packet) -= 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 +# 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 Claims(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', ''), - CBORF_INTEGER('exp', 0), - ) +# Illegal standalone break +try: + fld.build_value(None, Raw(b"\xff")) + assert False, "bare break must be rejected" +except CBOR_Encoding_Error: + pass -cbor2_data = cbor2.dumps({'iss': 'myapp', 'exp': 12345}) -pkt = Claims(cbor2_data) -pkt.iss.val == 'myapp' and pkt.exp.val == 12345 +# Truncated CBOR +try: + fld.build_value(None, Raw(b"\x18")) + assert False, "truncated CBOR must be rejected" +except CBOR_Encoding_Error: + pass -= CBORF_MAP packet roundtrip through cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_BYTE_STRING -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 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' ++ Scapy-native packet ownership -= CBORF_MAP with boolean values to cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_BOOLEAN, CBORF_INTEGER += 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 Flags(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_BOOLEAN('enabled', True), - CBORF_INTEGER('count', 5), - ) - -pkt = Flags() -raw = bytes(pkt) -dec = cbor2.loads(raw) -dec.get('enabled') is True and dec.get('count') == 5 - -= cbor2 dict with unknown keys: CBORF_MAP skips them -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +class OwnedChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -class SimpleMap(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_TEXT_STRING('known', 'default'), + CBORF_UNSIGNED_INTEGER("b", 1), + CBORF_UNSIGNED_INTEGER("a", 2), ) -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)) +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 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'' +########### Scapy-native conversion pipeline ################# -= 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 ++ Field defaults and i2m / RawVal -+ CBORF Fields - Interop: CBORF_UNSIGNED_INTEGER with cbor2 - -= CBORF_UNSIGNED_INTEGER boundary values - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_UNSIGNED_INTEGER -from scapy.cborpacket import CBOR_Packet - -class UIntPkt(CBOR_Packet): - CBOR_root = CBORF_UNSIGNED_INTEGER('n', 0) - -results = [] -for val in [0, 23, 24, 255, 256, 65535, 65536, 4294967295, 4294967296, 18446744073709551615]: - pkt = UIntPkt() - pkt.n.val = val - dec = cbor2.loads(bytes(pkt)) - results.append(dec == val) - -all(results) - -= CBORF_UNSIGNED_INTEGER boundary values - cbor2 encode, Scapy 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 UIntPkt2(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]: - pkt = UIntPkt2(cbor2.dumps(val)) - results.append(pkt.n.val == val) +assert DefaultNormPkt().value == 12 +assert DefaultNormPkt(value="34").value == 34 +assert bytes(DefaultNormPkt()) == b"\x0c" -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) +class BstrPkt(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING("blob", b"ABC") -+ CBORF Fields - Interop: CBORF_TEXT_STRING with cbor2 +assert BstrPkt().blob == b"ABC" +assert bytes(BstrPkt()) == b"\x43" + b"ABC" -= CBORF_TEXT_STRING empty string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING += BundleEidField defaults and assignments are always EidStruct +from scapy.contrib.bpv7 import BundleEidField, EidStruct, PrimaryBlock from scapy.cborpacket import CBOR_Packet -class TextPkt(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') - -pkt = TextPkt() -dec = cbor2.loads(bytes(pkt)) -dec == '' - -= CBORF_TEXT_STRING ASCII string - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet +class EidPkt(CBOR_Packet): + CBOR_root = BundleEidField("eid", "dtn:none") -class TextPkt2(CBOR_Packet): - CBOR_root = CBORF_TEXT_STRING('txt', '') +# Internal representation (getfieldval) is always EidStruct; attribute access +# returns the human form via i2h(), matching native Scapy Field semantics. +assert isinstance(EidPkt().getfieldval("eid"), EidStruct) +assert EidPkt().eid == "dtn:none" +assert isinstance(PrimaryBlock().getfieldval("source"), EidStruct) +assert PrimaryBlock().source == "dtn:none" -pkt = TextPkt2() -pkt.txt.val = 'Hello, World!' -dec = cbor2.loads(bytes(pkt)) -dec == 'Hello, World!' ++ CBORF_BYTE_STRING_PACKET default normalization -= CBORF_TEXT_STRING unicode 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 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), +class ByteStringDefaultParent(CBOR_Packet): + CBOR_root = CBORF_BYTE_STRING_PACKET( + "child", + b"abc", + pkt_cls=Raw, ) -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) +pkt = ByteStringDefaultParent() +assert isinstance(pkt.child, Raw) +assert pkt.child.load == b"abc" +assert pkt.child.parent is pkt +assert bytes(pkt) == b"\x43abc" -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) ++ PacketListField-style next_cls_cb semantics -+ 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 += 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 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', ''), - ) - -pkt = RecordPkt(cbor2.dumps([200, 'OK'])) -pkt.code.val == 200 and pkt.msg.val == 'OK' - -= CBORF_ARRAY roundtrip through cbor2 - multiple encode/decode cycles -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING -from scapy.cborpacket import CBOR_Packet - -class 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' - -= CBORF_ARRAY with null elements - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_NULL -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 +class DynamicSequenceChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -= 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 +NextClsCalls = [] +def choose_dynamic_child(pkt, lst, cur, remain): + NextClsCalls.append(len(lst)) + return DynamicSequenceChild -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'), +class DynamicSequenceParent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_OF( + "children", + [], + next_cls_cb=choose_dynamic_child, ) -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' +pkt = DynamicSequenceParent(b"\x01") +assert NextClsCalls == [0] +assert len(pkt.children) == 1 +assert isinstance(pkt.children[0], DynamicSequenceChild) +assert pkt.children[0].parent is pkt -= CBORF_MAP skip unknown keys from cbor2 -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER += CBORF_SEQUENCE_OF rejects combining next_cls_cb with cls/pkt_cls +from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet -class KnownKeysPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('known', 'default'), - CBORF_INTEGER('count', 0), - ) - -pkt = KnownKeysPkt(cbor2.dumps({'known': 'found', 'count': 42, 'extra': 'ignored'})) -pkt.known.val == 'found' and pkt.count.val == 42 - -+ CBORF Fields - Interop: CBOR_Packet complex structures with cbor2 - -= CBOR_Packet CBORF_ARRAY with multiple field types - Scapy encode, cbor2 decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_BOOLEAN -from scapy.cborpacket import CBOR_Packet +class FixedSequenceChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) -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), +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 = 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 ++ TypeError must not be used for callback signature probing -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''), - ) - -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 += 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 ClaimsPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('iss', ''), - CBORF_TEXT_STRING('sub', ''), - CBORF_INTEGER('exp', 0), - CBORF_BOOLEAN('admin', False), - ) - -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 +BrokenNextClsCalls = [] +def broken_next_cls(*args): + BrokenNextClsCalls.append(len(args)) + raise TypeError("intentional next_cls_cb failure") -= 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 -from scapy.cborpacket import CBOR_Packet - -class OffsetPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('name', ''), - CBORF_INTEGER('offset', 0), - CBORF_INTEGER('count', 0), +class BrokenCallbackParent(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_OF( + "children", + [], + next_cls_cb=broken_next_cls, ) -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 - -class NestInner(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('x', 0), - CBORF_INTEGER('y', 0), - ) - -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 +# {"x": 1, "a": 7} +wire = b"\xa2\x61x\x01\x61a\x07" +pkt = ExtensibleMap(wire) +assert pkt.a == 7 +# Exact received bytes are retained while the packet is untouched. +assert bytes(pkt) == wire -= CBORF_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 +# Mutation invalidates Scapy's raw packet cache. Rebuilding must not silently +# discard the unknown extension member. Fixed maps build deterministically, +# therefore "a" sorts before "x". +pkt.a = 8 +assert bytes(pkt) == b"\xa2\x61a\x08\x61x\x01" -class DocMeta(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('creator', ''), - CBORF_INTEGER('version', 0), - ) -class DocPacket(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('title', ''), - CBORF_BYTE_STRING('body', b''), - CBORF_PACKET('metadata', None, DocMeta), - ) - -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), - ) - -class DocPacket2(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('title', ''), - CBORF_BYTE_STRING('body', b''), - CBORF_PACKET('metadata', None, DocMeta2), - ) - -meta = DocMeta2() -meta.creator.val = 'bob' -meta.version.val = 7 -doc = DocPacket2() -doc.title.val = 'Report' -doc.body.val = b'\x01\x02\x03' -doc.metadata = meta -raw = bytes(doc) -doc2 = DocPacket2(raw) -doc2.title.val == 'Report' and doc2.body.val == b'\x01\x02\x03' and doc2.metadata.creator.val == 'bob' and doc2.metadata.version.val == 7 - -+ CBOR_Packet - CBORF_ARRAY_OF with CBOR_Packet elements - -= CBORF_ARRAY_OF with CBOR_Packet class: cbor2 list of lists → Scapy decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF -from scapy.cborpacket import CBOR_Packet - -class StatusItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('code', 0), - CBORF_TEXT_STRING('msg', ''), + CBORF_UNSIGNED_INTEGER("a", 0), ) -class StatusList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('statuses', [], StatusItem) - -raw = cbor2.dumps([[200, 'OK'], [201, 'Created'], [204, 'No Content']]) -pkt = StatusList(raw) -len(pkt.statuses) == 3 and pkt.statuses[0].code.val == 200 and pkt.statuses[1].msg.val == 'Created' and pkt.statuses[2].code.val == 204 +# Indefinite input map: {"x": [1, {"k": 2}], "a": 7} +# The unknown value is itself indefinite/nested, exercising preservation of +# the complete encoded value rather than only simple Python-native values. +wire = ( + b"\xbf" + b"\x61x" + b"\x9f\x01\xbf\x61k\x02\xff\xff" + b"\x61a\x07" + b"\xff" +) +pkt = ExtensibleNestedMap(wire) +assert pkt.a == 7 +pkt.a = 8 -= CBORF_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'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" +) -class ErrList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('errors', [], ErrItem) ++ CBOR_Packet.copy re-parents embedded children -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 += 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 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) +class CopyChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) -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' +class CopyParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, cls=CopyChild) -= 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 +a = CopyParent(child=CopyChild(n=1)) +assert a.child.parent is a +b = a.copy() +assert b.child is not a.child +assert b.child.parent is b, b.child.parent +assert a.child.parent is a +assert b.child.n == 1 -class EmptyItem(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_INTEGER('val', 0), - ) -class EmptyItemList(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF('items', [], EmptyItem) ++ CBORF_MAP deterministic unknown rebuild -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 += 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 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), - ) - -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', '')), + CBORF_UNSIGNED_INTEGER("z", 0), ) -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)) +# 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 = 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 += 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 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): +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 = 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) +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] -= CBORF_SEMANTIC_TAG inside CBORF_MAP: Scapy roundtrip preserves inner value -import cbor2 -from scapy.cbor.cborfields import CBORF_MAP, CBORF_TEXT_STRING, CBORF_INTEGER, CBORF_SEMANTIC_TAG -from scapy.cborpacket import CBOR_Packet -class EventRTPkt(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_TEXT_STRING('event_type', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) ++ CBORF_ARRAY_OF indefinite decoding -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 += 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 TimedEventArr(CBOR_Packet): - CBOR_root = CBORF_ARRAY( - CBORF_TEXT_STRING('evt', ''), - CBORF_SEMANTIC_TAG('tag', None, 1, CBORF_INTEGER('ts', 0)), - ) +class IndefUIntArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], cls=CBORF_UNSIGNED_INTEGER) -pkt = TimedEventArr() -pkt.evt.val = 'start' -pkt.ts.val = 1700000000 -dec = cbor2.loads(bytes(pkt)) -dec[0] == 'start' and isinstance(dec[1], datetime.datetime) +wire = b"\x9f\x01\x02\x03\xff" +pkt = IndefUIntArray(wire) +assert pkt.values == [1, 2, 3] +assert bytes(pkt) == wire -+ CBOR_Packet - realistic models -= 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 packet 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), - ) - -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 - -= 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), - ) +class IndefChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 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 +class IndefPacketArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("children", [], pkt_cls=IndefChild) -= 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 -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 SensorReport(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('station', 0), - CBORF_TEXT_STRING('unit', ''), - CBORF_PACKET('reading', None, SensorData), - ) +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 -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] ++ Medium-severity review follow-ups -= 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 SensorReport2(CBOR_Packet): - CBOR_root = CBORF_MAP( - CBORF_INTEGER('station', 0), - CBORF_TEXT_STRING('unit', ''), - CBORF_PACKET('reading', None, SensorData2), - ) - -raw = cbor2.dumps({'station': 9, 'unit': 'celsius', 'reading': [7, 36.5]}) -pkt = SensorReport2(raw) -pkt2 = SensorReport2(bytes(pkt)) -pkt2.station.val == 9 and pkt2.unit.val == 'celsius' and pkt2.reading.sensor_id.val == 7 - -= Realistic model: StatusList (CBORF_ARRAY_OF of CBOR_Packets) encode and decode -import cbor2 -from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_INTEGER, CBORF_TEXT_STRING, CBORF_ARRAY_OF += 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''), - ) - -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' +class NamedMap(CBOR_Packet): + CBOR_root = CBORF_MAP(CBORF_UNSIGNED_INTEGER("a", 0)) -= 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 From fc7fe612cea9ff71f5f29127aeabc831fc26b36a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Tue, 1 Sep 2026 13:55:18 +0200 Subject: [PATCH 02/48] cbor: avoid Generic cls= kwargs for Python 3.7 Rename CBORF_PACKET/SEQUENCE_OF/ARRAY_OF constructor kwargs to pkt_cls so typing.Generic.__new__ does not collide on 3.7, and drop the BPv7-only BundleEidField unit from the CBOR campaign. AI-Assisted: yes (Cursor) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 51 +++++++++++++++++++------------------- test/scapy/layers/cbor.uts | 28 ++++++--------------- 2 files changed, 33 insertions(+), 46 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 55eee021d5e..ba9667dc5aa 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1401,12 +1401,14 @@ class CBORF_SEQUENCE_OF(CBORF_field[List[Any]]): Preferred constructors (ASN1F_SEQUENCE_OF / PacketListField style):: - CBORF_SEQUENCE_OF("items", [], cls=MyPacket) - CBORF_SEQUENCE_OF("items", [], cls=CBORF_UNSIGNED_INTEGER) + 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`` is accepted as an alias of ``cls`` for PacketListField - familiarity. Pass only one of ``cls`` / ``pkt_cls`` / ``next_cls_cb``. + ``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 @@ -1414,8 +1416,7 @@ class CBORF_SEQUENCE_OF(CBORF_field[List[Any]]): def __init__(self, name, # type: str default, # type: Any - cls=None, # type: _ARRAY_T - pkt_cls=None, # type: Optional[Type[Packet]] + pkt_cls=None, # type: _ARRAY_T next_cls_cb=None, # type: Optional[Callable[..., Optional[Type[Packet]]]] # noqa: E501 ): # type: (...) -> None @@ -1425,16 +1426,14 @@ def __init__(self, self.holds_packets = 0 if next_cls_cb is not None: - if cls is not None or pkt_cls is not None: + if pkt_cls is not None: raise ValueError( - "Pass only next_cls_cb, or only cls/pkt_cls" + "Pass only next_cls_cb, or only pkt_cls" ) self.next_cls_cb = next_cls_cb self.holds_packets = 1 else: - if cls is not None and pkt_cls is not None: - raise ValueError("Pass only one of cls or pkt_cls") - chosen = pkt_cls if pkt_cls is not None else cls + chosen = pkt_cls if isinstance(chosen, type) and issubclass(chosen, CBORF_field) or \ isinstance(chosen, CBORF_field): if isinstance(chosen, type): @@ -1447,7 +1446,7 @@ def __init__(self, self.holds_packets = 1 else: raise ValueError( - "Provide cls, pkt_cls, or next_cls_cb" + "Provide pkt_cls or next_cls_cb" ) super(CBORF_SEQUENCE_OF, self).__init__(name, default) @@ -1579,10 +1578,12 @@ class CBORF_ARRAY_OF(CBORF_field[List[Any]]): Preferred constructors:: - CBORF_ARRAY_OF("items", [], cls=MyPacket) - CBORF_ARRAY_OF("items", [], cls=CBORF_UNSIGNED_INTEGER) + CBORF_ARRAY_OF("items", [], pkt_cls=MyPacket) + CBORF_ARRAY_OF("items", [], pkt_cls=CBORF_UNSIGNED_INTEGER) - ``pkt_cls`` is accepted as an alias of ``cls``. Pass only one of them. + ``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 @@ -1590,15 +1591,12 @@ class CBORF_ARRAY_OF(CBORF_field[List[Any]]): def __init__(self, name, # type: str default, # type: Any - cls=None, # type: _ARRAY_T - pkt_cls=None, # type: Optional[Type[Packet]] + pkt_cls=None, # type: _ARRAY_T ): # type: (...) -> None - if cls is not None and pkt_cls is not None: - raise ValueError("Pass only one of cls or pkt_cls") - chosen = pkt_cls if pkt_cls is not None else cls + chosen = pkt_cls if chosen is None: - raise ValueError("Provide cls or pkt_cls") + raise ValueError("Provide pkt_cls") if isinstance(chosen, type) and issubclass(chosen, CBORF_field) or \ isinstance(chosen, CBORF_field): if isinstance(chosen, type): @@ -1610,7 +1608,7 @@ def __init__(self, self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 else: - raise ValueError("cls must be a CBORF_field or CBOR_Packet") + raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") super(CBORF_ARRAY_OF, self).__init__(name, default) def any2i(self, pkt, x): @@ -2165,17 +2163,20 @@ 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 + self.cls = pkt_cls super(CBORF_PACKET, self).__init__(name, default) def _parse_packet_item(self, pkt, s): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 0e4f60dca12..c902235bbbb 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2167,7 +2167,7 @@ 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, cls=SeqChild) +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" @@ -2192,11 +2192,11 @@ class TwoItemChild(CBOR_Packet): class OneItemChild(CBOR_Packet): CBOR_root = CBORF_UNSIGNED_INTEGER("a", 1) -fld = CBORF_PACKET("x", None, cls=OneItemChild) +fld = CBORF_PACKET("x", None, pkt_cls=OneItemChild) ok = fld.build_value(None, OneItemChild(a=7)) assert ok.items == 1 -fld2 = CBORF_PACKET("x", None, cls=TwoItemChild) +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" @@ -2209,7 +2209,7 @@ from scapy.cbor.cbor import CBOR_Encoding_Error from scapy.cborpacket import CBOR_Packet from scapy.packet import Raw -fld = CBORF_PACKET("x", None, cls=CBOR_Packet) +fld = CBORF_PACKET("x", None, pkt_cls=CBOR_Packet) # Two valid CBOR integers must not be reported as one item try: @@ -2403,20 +2403,6 @@ class BstrPkt(CBOR_Packet): assert BstrPkt().blob == b"ABC" assert bytes(BstrPkt()) == b"\x43" + b"ABC" -= BundleEidField defaults and assignments are always EidStruct -from scapy.contrib.bpv7 import BundleEidField, EidStruct, PrimaryBlock -from scapy.cborpacket import CBOR_Packet - -class EidPkt(CBOR_Packet): - CBOR_root = BundleEidField("eid", "dtn:none") - -# Internal representation (getfieldval) is always EidStruct; attribute access -# returns the human form via i2h(), matching native Scapy Field semantics. -assert isinstance(EidPkt().getfieldval("eid"), EidStruct) -assert EidPkt().eid == "dtn:none" -assert isinstance(PrimaryBlock().getfieldval("source"), EidStruct) -assert PrimaryBlock().source == "dtn:none" - + CBORF_BYTE_STRING_PACKET default normalization = CBORF_BYTE_STRING_PACKET normalizes byte defaults after packet-class state is initialized @@ -2465,7 +2451,7 @@ assert len(pkt.children) == 1 assert isinstance(pkt.children[0], DynamicSequenceChild) assert pkt.children[0].parent is pkt -= CBORF_SEQUENCE_OF rejects combining next_cls_cb with cls/pkt_cls += 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 @@ -2610,7 +2596,7 @@ class CopyChild(CBOR_Packet): CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) class CopyParent(CBOR_Packet): - CBOR_root = CBORF_PACKET("child", None, cls=CopyChild) + CBOR_root = CBORF_PACKET("child", None, pkt_cls=CopyChild) a = CopyParent(child=CopyChild(n=1)) assert a.child.parent is a @@ -2689,7 +2675,7 @@ from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class IndefUIntArray(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF("values", [], cls=CBORF_UNSIGNED_INTEGER) + CBOR_root = CBORF_ARRAY_OF("values", [], pkt_cls=CBORF_UNSIGNED_INTEGER) wire = b"\x9f\x01\x02\x03\xff" pkt = IndefUIntArray(wire) From 3d718eab8252a58f1eedb39602d8671cfa2bb982 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:18:49 +0200 Subject: [PATCH 03/48] cbor: add RFC and lifecycle regression tests for PR #5125 refactor Capture map-key equivalence, deterministic floats, hash/tag field semantics, and multi-map unknown ownership before changing the implementation. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- test/scapy/layers/cbor.uts | 238 +++++++++++++++++++++++++++++++++++-- 1 file changed, 230 insertions(+), 8 deletions(-) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index c902235bbbb..a8dad31dcd0 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2770,17 +2770,17 @@ 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 += CBORMapData lookup treats +0.0 and -0.0 as equivalent map keys from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error -# {+0.0: 1, -0.0: 2} as half floats +# {+0.0: 1, -0.0: 2} as half floats — duplicate under RFC 8949 key equivalence wire = b"\xa2\xf9\x00\x00\x01\xf9\x80\x00\x02" -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] +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "+0.0 / -0.0 duplicate keys were accepted" +except CBOR_Codec_Decoding_Error: + pass = Malformed optional semantic tag does not migrate into trailing CBORF_ANY from scapy.cbor.cbor import CBOR_Decoding_Error @@ -2978,3 +2978,225 @@ assert CBOR_TRUE() != CBOR_FALSE() encoded = bytes.fromhex("fa3fc00000") assert CBOR_FLOAT(1.5, encoded=encoded).enc() == encoded True + + ++ PR5125 refactor regressions (RFC + lifecycle) + += RFC map-key equivalence helper covers scalars, containers, tags, and NaNs +from scapy.cbor.cbor import _cbor_key_equivalent +from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_FLOAT, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_TEXT_STRING, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) +import math + +assert _cbor_key_equivalent(0.0, -0.0) +assert not _cbor_key_equivalent(1, 1.0) +assert _cbor_key_equivalent([1, 2], [1, 2]) +assert not _cbor_key_equivalent([1, 2], [2, 1]) +assert _cbor_key_equivalent( + CBORMapData([(CBOR_TEXT_STRING("a"), 1), (CBOR_TEXT_STRING("b"), 2)]), + CBORMapData([(CBOR_TEXT_STRING("b"), 2), (CBOR_TEXT_STRING("a"), 1)]), +) +assert _cbor_key_equivalent( + CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), + CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), +) +assert not _cbor_key_equivalent( + CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), + CBOR_SEMANTIC_TAG((2, CBOR_UNSIGNED_INTEGER(5))), +) +nan_a = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")) +nan_b = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("fb7ff8000000000001")) +assert math.isnan(nan_a.val) and math.isnan(nan_b.val) +assert _cbor_key_equivalent(nan_a, nan_b) +assert _cbor_key_equivalent(float("nan"), float("nan")) + += Generic maps reject +0.0 and -0.0 as duplicate keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {+0.0: 1, -0.0: 2} half-floats — equivalent keys under RFC 8949 +wire = b"\xa2\xf9\x00\x00\x01\xf9\x80\x00\x02" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "+0.0 and -0.0 were accepted as distinct map keys" +except CBOR_Codec_Decoding_Error: + pass + += Generic maps keep integer 1 and float 1.0 as distinct keys +from scapy.cbor import CBOR_Codecs + +# {1: "i", 1.0: "f"} — half float 1.0 is f93c00 +wire = b"\xa2\x01\x61i\xf9\x3c\x00\x61f" +obj, rem = CBOR_Codecs.CBOR.dec(wire) +assert rem == b"" +md = obj.val +assert md[1].val == "i" +assert md[1.0].val == "f" + += Generic maps reject reordered equivalent maps used as map keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {{"a":1,"b":2}: 0, {"b":2,"a":1}: 1} — equivalent map keys +k1 = b"\xa2\x61a\x01\x61b\x02" +k2 = b"\xa2\x61b\x02\x61a\x01" +wire = b"\xa2" + k1 + b"\x00" + k2 + b"\x01" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "reordered map keys were treated as distinct" +except CBOR_Codec_Decoding_Error: + pass + += Generic maps reject duplicate semantic-tag keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {1(5): 0, 1(5): 1} +wire = b"\xa2\xc1\x05\x00\xc1\x05\x01" +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "duplicate tagged keys were accepted" +except CBOR_Codec_Decoding_Error: + pass + += Generic maps reject distinct NaN encodings as duplicate keys +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {NaN16: 1, NaN64-payload: 2} +wire = bytes.fromhex("a2f97e0001fb7ff800000000000102") +try: + CBOR_Codecs.CBOR.dec(wire) + assert False, "distinct NaN encodings were accepted as distinct keys" +except CBOR_Codec_Decoding_Error: + pass + += CBORMapData treats +0.0 and -0.0 as the same lookup key +from scapy.cbor import CBOR_Codecs + +# single-entry map keyed by +0.0 +wire = b"\xa1\xf9\x00\x00\x01" +obj, remaining = CBOR_Codecs.CBOR.dec(wire) +assert remaining == b"" +md = obj.val +assert md[0.0].val == 1 +assert md[-0.0].val == 1 +assert md[0.0] is md[-0.0] + += Deterministic encoding rebuilds floats from the semantic value +from scapy.cbor.cbor import CBORFloatValue +from scapy.cbor.cborcodec import CBORcodec_Object + +# 1.5 received as binary64 must still encode as preferred binary16 +value = CBORFloatValue(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +assert CBORcodec_Object.encode_cbor_item_deterministic(value) == bytes.fromhex("f93e00") + += Compound CBOR_Object values are unhashable +from scapy.cbor.cbor import CBOR_ARRAY, CBOR_UNSIGNED_INTEGER + +arr = CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1)]) +try: + hash(arr) + assert False, "mutable/compound CBOR_Object remained hashable" +except TypeError: + pass + += Schema-fixed CBORF_SEMANTIC_TAG does not expose an editable tag field +from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_INTEGER +from scapy.cborpacket import CBOR_Packet + +class TaggedTs(CBOR_Packet): + CBOR_root = CBORF_SEMANTIC_TAG( + "tag_number", + None, + 1, + CBORF_INTEGER("ts", 0), + ) + +assert "tag_number" not in [f.name for f in TaggedTs.fields_desc] +assert "ts" in [f.name for f in TaggedTs.fields_desc] +pkt = TaggedTs(b"\xc1\x0a") +assert pkt.ts == 10 +assert bytes(pkt) == b"\xc1\x0a" +# Only the inner value is packet state; mutating ts must change the wire. +pkt.ts = 11 +assert bytes(pkt) == b"\xc1\x0b" + += Two independent CBORF_MAP fields keep separate unknown extensions after mutation +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class TwoMaps(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + ), + CBORF_MAP( + CBORF_UNSIGNED_INTEGER("b", 0), + ), + ) + +# [{"a":1,"x":10},{"b":2,"y":20}] +wire = ( + b"\x82" + b"\xa2\x61a\x01\x61x\x0a" + b"\xa2\x61b\x02\x61y\x14" +) +pkt = TwoMaps(wire) +assert pkt.a == 1 and pkt.b == 2 +assert bytes(pkt) == wire +pkt.a = 3 +# Each map must retain its own unknown: map0 keeps x, map1 keeps y. +# Deterministic rebuild sorts keys inside each map. +assert bytes(pkt) == ( + b"\x82" + b"\xa2\x61a\x03\x61x\x0a" + b"\xa2\x61b\x02\x61y\x14" +) + += Nested CBORF_MAP unknown extensions survive outer mutation +from scapy.cbor.cborfields import ( + CBORF_MAP, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class InnerMapPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("inner", 0), + ) + +class OuterMapPkt(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_PACKET("child", None, InnerMapPkt), + ) + +# {"child":{"inner":2,"u":9},"n":1,"x":5} +wire = ( + b"\xa3" + b"\x65child\xa2\x65inner\x02\x61u\x09" + b"\x61n\x01" + b"\x61x\x05" +) +pkt = OuterMapPkt(wire) +assert pkt.n == 1 +assert pkt.child.inner == 2 +pkt.n = 4 +built = bytes(pkt) +# Outer unknowns (x) and nested unknowns (u) must both survive. +assert b"\x61x\x05" in built +assert b"\x61u\x09" in built +assert pkt.child.inner == 2 From 15ed3b48d147318c46fd264dd11c96108d4e9904 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:23:16 +0200 Subject: [PATCH 04/48] cbor: fix RFC map-key equivalence, deterministic floats, and tag fields Use semantic CBOR key equivalence for duplicate detection and lookup, rebuild deterministic floats from values, drop identity hashing, and treat schema tag numbers as metadata rather than packet fields. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cbor/cbor.py | 98 ++++++++++++++++++++++++++++++++++---- scapy/cbor/cborcodec.py | 27 +++++------ scapy/cbor/cborfields.py | 47 +++++++++++++----- test/scapy/layers/cbor.uts | 7 ++- 4 files changed, 138 insertions(+), 41 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 1b700217ebc..82d1ff1e468 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -364,12 +364,8 @@ def __ne__(self, other): 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))) + # No __hash__: defining __eq__ without __hash__ makes instances unhashable. + # Immutable scalar subclasses may add semantic hashing later if needed. ####################### @@ -543,10 +539,9 @@ def __contains__(self, key): 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: + if _cbor_key_equivalent(map_key, key): matches.append(value) if not matches: raise KeyError(key) @@ -571,12 +566,11 @@ def __eq__(self, other): 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: + if not _cbor_key_equivalent(map_key, other_key): continue if value != other_value: return False @@ -810,6 +804,90 @@ def __deepcopy__(self, memo): return self.__copy__() +def _cbor_key_norm(value): + # type: (Any) -> Any + """Return a hashable RFC 8949 map-key equivalence form for *value*. + + Integers and floats remain distinct groups. Floating ``+0.0`` and + ``-0.0`` collapse. All NaN payloads are equivalent. Arrays compare + order-sensitively; maps compare as unordered pairs of norms. Semantic + tags require the same tag number and an equivalent tagged value. + """ + if isinstance(value, CBORTagValue): + return ("tag", int(value.tag), _cbor_key_norm(value.value)) + if isinstance(value, CBORSimpleValue): + return ("simple", int(value.value)) + if value is CBOR_UNDEFINED_VALUE: + return ("undef", None) + if isinstance(value, CBOR_Object): + if isinstance(value, (CBOR_TRUE, CBOR_FALSE)): + return ("bool", bool(value.val)) + if isinstance(value, CBOR_NULL): + return ("null", None) + if isinstance(value, CBOR_UNDEFINED): + return ("undef", None) + if isinstance(value, (CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER)): + return ("int", int(value.val)) + if isinstance(value, CBOR_BYTE_STRING): + return ("bstr", bytes(value.val)) + if isinstance(value, CBOR_TEXT_STRING): + return ("tstr", str(value.val)) + if isinstance(value, CBOR_FLOAT): + return _cbor_key_norm(float(value.val)) + if isinstance(value, CBOR_ARRAY): + return ("array", tuple(_cbor_key_norm(v) for v in value.val)) + if isinstance(value, CBOR_MAP): + return _cbor_key_norm(value.val) + if isinstance(value, CBOR_SEMANTIC_TAG): + tag_num, inner = value.val + return ("tag", int(tag_num), _cbor_key_norm(inner)) + if isinstance(value, CBOR_SIMPLE_VALUE): + return ("simple", int(value.val)) + return ("obj", type(value).__name__, _cbor_key_norm(value.val)) + if isinstance(value, CBORMapData): + return ( + "map", + frozenset( + (_cbor_key_norm(k), _cbor_key_norm(v)) + for k, v in value.cbor_pairs() + ), + ) + if isinstance(value, dict): + return ( + "map", + frozenset( + (_cbor_key_norm(k), _cbor_key_norm(v)) + for k, v in value.items() + ), + ) + if isinstance(value, bool): + return ("bool", value) + if isinstance(value, int): + return ("int", value) + if isinstance(value, float): + if math.isnan(value): + return ("float", "nan") + if value == 0.0: + return ("float", 0.0) + return ("float", float(value)) + if isinstance(value, bytes): + return ("bstr", value) + if isinstance(value, str): + return ("tstr", value) + if isinstance(value, list): + return ("array", tuple(_cbor_key_norm(v) for v in value)) + if isinstance(value, tuple) and len(value) == 2 and isinstance(value[0], int): + # Bare semantic-tag tuple (tag_num, inner), as stored on CBOR_SEMANTIC_TAG. + return ("tag", int(value[0]), _cbor_key_norm(value[1])) + return ("other", type(value).__name__, repr(value)) + + +def _cbor_key_equivalent(a, b): + # type: (Any, Any) -> bool + """Return True when *a* and *b* are equivalent CBOR map keys (RFC 8949).""" + return _cbor_key_norm(a) == _cbor_key_norm(b) + + class _CBOR_ERROR(CBOR_Object[Union[bytes, CBOR_Object[Any]]]): """CBOR decoding error wrapper""" tag = None # type: ignore # Error objects don't have a CBOR tag diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 5ad5dbe5a79..5157cfc89f2 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -917,19 +917,17 @@ def do_dec(cls, remaining=s) pairs = [] # type: List[Tuple[Any, Any]] - seen_keys = set() # type: set[bytes] + seen_keys = [] # type: List[Any] 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( - "Duplicate CBOR map key: %r" % (key,), - remaining=s) - seen_keys.add(key_wire) + from scapy.cbor.cbor import _cbor_key_equivalent + for prev in seen_keys: + if _cbor_key_equivalent(prev, key): + raise CBOR_Codec_Decoding_Error( + "Duplicate CBOR map key: %r" % (key,), + remaining=s) + seen_keys.append(key) pairs.append((key, value)) if length is CBOR_INDEFINITE: @@ -1317,12 +1315,9 @@ def _encode_cbor_item_deterministic(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) + # Deterministic encoding always rebuilds from the semantic float + # value (shortest exact representation). Never reuse source wire. + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item)) if item is None: return CBORcodec_SIMPLE_AND_FLOAT.enc(None) raise CBOR_Codec_Encoding_Error( diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index ba9667dc5aa..d5f6f2c8e03 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -441,6 +441,16 @@ def set_val(self, pkt, val): return pkt.setfieldval(self.name, val) + def mark_absent(self, pkt): + # type: (CBOR_Packet) -> None + """Record that this field was not present on the wire.""" + self.set_val(pkt, CBOR_ABSENT) + + def is_absent(self, pkt): + # type: (CBOR_Packet) -> bool + """Return True when this field is marked :data:`CBOR_ABSENT`.""" + return pkt.getfieldval(self.name) is CBOR_ABSENT + def is_empty(self, pkt): # type: (CBOR_Packet) -> bool val = pkt.getfieldval(self.name) @@ -1126,7 +1136,7 @@ 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) + field._field.mark_absent(pkt) elif isinstance(field, CBORF_CONDITIONAL): # Condition false or skipped: leave value untouched. pass @@ -1915,7 +1925,7 @@ def _dissect_value_bytes(fld, val_bytes): 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) + fld._field.mark_absent(pkt) def build(self, pkt): # type: (CBOR_Packet) -> bytes @@ -1938,10 +1948,9 @@ 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 tag number, while the inner field stores its - value under its own name on the packet. + Wraps an ``inner_field`` with the given numeric ``tag_num``. The tag + number is schema metadata only: it is not stored as editable packet + field state. The inner field stores its value under its own name. Example:: @@ -2010,7 +2019,6 @@ def dissect_result(self, pkt, s): 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): @@ -2044,11 +2052,28 @@ def build_value(self, pkt, value): def get_fields_list(self): # type: () -> List[CBORF_field[Any]] - return [self] + self.inner_field.get_fields_list() + # Tag number is schema metadata; only the tagged value is packet state. + return self.inner_field.get_fields_list() + + def mark_absent(self, pkt): + # type: (CBOR_Packet) -> None + self.inner_field.mark_absent(pkt) + + def is_absent(self, pkt): + # type: (CBOR_Packet) -> bool + return self.inner_field.is_absent(pkt) def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return pkt.getfieldval(self.name) is CBOR_ABSENT + return self.is_absent(pkt) + + def set_val(self, pkt, val): + # type: (CBOR_Packet, Any) -> None + # Presence bookkeeping for optional wrappers targets the inner value. + if val is CBOR_ABSENT: + self.mark_absent(pkt) + return + self.inner_field.set_val(pkt, val) ############################## @@ -2074,7 +2099,7 @@ def __getattr__(self, attr): def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult - if pkt.getfieldval(self._field.name) is CBOR_ABSENT: + if self._field.is_absent(pkt): return CBORBuildResult(b"", 0) if self._field.is_empty(pkt): return CBORBuildResult(b"", 0) @@ -2083,7 +2108,7 @@ def build_result(self, 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) + self._field.mark_absent(pkt) return CBORParseResult(remaining=s, items=0) return self._field.dissect_result(pkt, s) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index a8dad31dcd0..a6c9230c7b4 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -358,8 +358,7 @@ class OptionalTaggedThenFallback(CBOR_Packet): 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.tagged_value is CBOR_ABSENT assert pkt.fallback == 7 = A matching optional semantic tag with the wrong inner type is malformed @@ -453,7 +452,7 @@ class OptionalTaggedBeforeAny(CBOR_Packet): # 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("tagged_value") is CBOR_ABSENT assert pkt.getfieldval("fallback") is not None assert pkt.getfieldval("fallback") is not CBOR_ABSENT @@ -2816,7 +2815,7 @@ else: # 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.getfieldval("tagged_value") is CBOR_ABSENT assert ok.fallback is not None = CBORF_MAP rejects non-text map keys From 80e0319139e82f023a9caa0de575e42a068a7fac Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:29:37 +0200 Subject: [PATCH 05/48] cbor: store CBORF_ANY as CBOR_Object and drop islist workaround Arbitrary CBOR values use the lossless object tree; typed floats stay plain Python floats with wire fidelity via the packet raw cache. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cbor/cbor.py | 28 ++++++++ scapy/cbor/cborfields.py | 122 ++++++++++++++++++++++++--------- test/scapy/layers/cbor.uts | 137 ++++++++++++++++++++----------------- 3 files changed, 195 insertions(+), 92 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 82d1ff1e468..b59fb96addc 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -435,6 +435,34 @@ def cbor_pairs(self): # type: () -> List[Tuple[Any, Any]] return list(self._pairs) + @property + def pairs(self): + # type: () -> List[Tuple[Any, Any]] + """Ordered ``(key, value)`` pairs (primary map representation).""" + return self.cbor_pairs() + + def as_dict(self): + # type: () -> Dict[Any, Any] + """Convert to a Python dict, raising if CBOR key distinctions would be lost.""" + out = {} # type: Dict[Any, Any] + used_norms = [] # type: List[Any] + for key, value in self._pairs: + norm = _cbor_key_norm(key) + if norm in used_norms: + raise ValueError( + "CBOR map keys are equivalent under RFC 8949; " + "cannot convert to dict without losing distinctions" + ) + # Also reject Python-dict collisions (True vs 1, etc.). + py_key = key.val if isinstance(key, CBOR_Object) else key + if py_key in out: + raise ValueError( + "Converting CBOR map to dict would collapse distinct keys" + ) + used_norms.append(norm) + out[py_key] = value + return out + def copy(self): # type: () -> CBORMapData return copy.deepcopy(self) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index d5f6f2c8e03..b57a945fa7e 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -195,7 +195,11 @@ def _cbor_packet_from_bytes(cls, data, parent): def cbor_object_to_python(obj): # type: (Any) -> Any - """Convert a :class:`CBOR_Object` tree to native Python values.""" + """Convert a :class:`CBOR_Object` tree to native Python values. + + Prefer keeping :class:`CBOR_Object` for arbitrary CBOR (``CBORF_ANY``). + This helper remains for typed-field coercion and legacy call sites. + """ if not isinstance(obj, CBOR_Object): return obj if isinstance(obj, CBOR_UNDEFINED): @@ -203,8 +207,6 @@ def cbor_object_to_python(obj): 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() @@ -222,11 +224,74 @@ def cbor_object_to_python(obj): 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 float(obj.val) return obj.val +def python_to_cbor_object(value): + # type: (Any) -> Any + """Convert native Python / legacy wrappers into a :class:`CBOR_Object` tree.""" + from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_BYTE_STRING, + CBOR_FALSE, + CBOR_FLOAT, + CBOR_MAP, + CBOR_NEGATIVE_INTEGER, + CBOR_NULL, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_TEXT_STRING, + CBOR_TRUE, + CBOR_UNDEFINED, + CBOR_UNSIGNED_INTEGER, + CBORMapData, + CBORFloatValue, + CBORSimpleValue, + CBORTagValue, + CBOR_UNDEFINED_VALUE, + ) + if isinstance(value, CBOR_Object): + return value + if value is CBOR_UNDEFINED_VALUE: + return CBOR_UNDEFINED() + if isinstance(value, CBORTagValue): + return CBOR_SEMANTIC_TAG( + (value.tag, python_to_cbor_object(value.value)) + ) + if isinstance(value, CBORSimpleValue): + return CBOR_SIMPLE_VALUE(value.value) + if isinstance(value, CBORFloatValue): + return CBOR_FLOAT(float(value), encoded=value.cbor_encoded) + if isinstance(value, CBORMapData): + return CBOR_MAP(CBORMapData([ + (python_to_cbor_object(k), python_to_cbor_object(v)) + for k, v in value.cbor_pairs() + ])) + if isinstance(value, bool): + return CBOR_TRUE() if value else CBOR_FALSE() + if value is None: + return CBOR_NULL() + if isinstance(value, int): + if value >= 0: + return CBOR_UNSIGNED_INTEGER(value) + return CBOR_NEGATIVE_INTEGER(value) + if isinstance(value, float): + return CBOR_FLOAT(value) + if isinstance(value, bytes): + return CBOR_BYTE_STRING(value) + if isinstance(value, str): + return CBOR_TEXT_STRING(value) + if isinstance(value, list): + return CBOR_ARRAY([python_to_cbor_object(item) for item in value]) + if isinstance(value, dict): + return CBOR_MAP(CBORMapData([ + (python_to_cbor_object(k), python_to_cbor_object(v)) + for k, v in value.items() + ])) + raise TypeError("Cannot convert %r to CBOR_Object" % (type(value),)) + + class CBORF_element(object): """Base class for CBOR packet field elements.""" @@ -488,15 +553,12 @@ def copy(self): class CBORF_ANY(CBORF_field[Any]): - """Represent any well-formed CBOR value, including recursion.""" + """Represent any well-formed CBOR value as a lossless ``CBOR_Object``.""" 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". + # Python None / CBOR null is a real value; only CBOR_ABSENT means absent. return pkt.getfieldval(self.name) is CBOR_ABSENT def matches_next_item(self, pkt, s): @@ -513,9 +575,14 @@ 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 any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> Any + if x is CBOR_ABSENT or x is CBOR_NO_ITEM or x is CBOR_UNDEFINED_VALUE: + return x + return python_to_cbor_object(x) + def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult val = pkt.getfieldval(self.name) @@ -526,14 +593,14 @@ def build_result(self, pkt): 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 + if isinstance(obj, CBOR_UNDEFINED): + return CBOR_UNDEFINED_VALUE, remain + return 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) @@ -981,10 +1048,9 @@ def max_items(self, pkt): class CBORF_FLOAT(CBORF_field[float]): """CBOR float field (major type 7). - 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. + Stores a plain Python ``float``. Exact received encodings are preserved + only while the packet ``raw_packet_cache`` remains valid; after semantic + rebuild, preferred (shortest exact) encoding is used. """ CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT @@ -997,35 +1063,28 @@ def matches_next_item(self, pkt, s): 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) + return float(x.val) 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[float, bytes] - from scapy.cbor.cbor import CBORFloatValue obj, remain = CBORcodec_SIMPLE_AND_FLOAT.dec(s) if not isinstance(obj, CBOR_FLOAT): raise CBOR_Type_Mismatch( "Expected float, got %r" % obj) - return CBORFloatValue(obj.val, encoded=obj._encoded), remain + return float(obj.val), remain def encode_value(self, x): # type: (Any) -> bytes - from scapy.cbor.cbor import CBORFloatValue if isinstance(x, CBOR_FLOAT): - return x.enc() - if isinstance(x, CBORFloatValue) and x.cbor_encoded is not None: - return x.cbor_encoded + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(x.val)) return CBORcodec_SIMPLE_AND_FLOAT.enc(float(x)) def i2h(self, pkt, x): @@ -1861,9 +1920,7 @@ def _collect_pair(): raise CBOR_Decoding_Error( "CBOR map value did not decode to a single item" ) - unknown_pairs.append( - (key, cbor_object_to_python(val_obj)) - ) + unknown_pairs.append((key, val_obj)) if count is CBOR_INDEFINITE: while True: @@ -1976,6 +2033,9 @@ def __init__(self, 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 CBOR_ABSENT: + # Tag number is schema metadata; absence applies to the value field. + self.inner_field.default = CBOR_ABSENT if default is None: default = tag_num super(CBORF_SEMANTIC_TAG, self).__init__(name, default) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index a6c9230c7b4..a1b663c74f0 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -359,7 +359,7 @@ class OptionalTaggedThenFallback(CBOR_Packet): pkt = OptionalTaggedThenFallback(b"\x81\x07") from scapy.cbor.cborfields import CBOR_ABSENT assert pkt.tagged_value is CBOR_ABSENT -assert pkt.fallback == 7 +assert pkt.fallback.val == 7 = A matching optional semantic tag with the wrong inner type is malformed from scapy.cbor.cbor import CBOR_Decoding_Error @@ -474,7 +474,8 @@ class OptionalAnyWithTail(CBOR_Packet): ) pkt = OptionalAnyWithTail(b"\x82\xf6\x01") -assert pkt.value is None +from scapy.cbor.cbor import CBOR_NULL +assert isinstance(pkt.value, CBOR_NULL) assert pkt.tail == 1 # Mutating another field invalidates Scapy's raw-packet cache. The rebuilt @@ -657,7 +658,7 @@ except CBOR_Codec_Decoding_Error: = 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.cbor.cbor import CBOR_MAP, CBORMapData from scapy.cborpacket import CBOR_Packet class AnyMapPkt(CBOR_Packet): @@ -667,14 +668,15 @@ class AnyMapPkt(CBOR_Packet): ) pkt = AnyMapPkt(b"\x82\xa0\x00") -assert isinstance(pkt.a, CBORMapData) -assert len(pkt.a) == 0 +assert isinstance(pkt.a, CBOR_MAP) +assert isinstance(pkt.a.val, CBORMapData) +assert len(pkt.a.val) == 0 pkt.b = 1 assert bytes(pkt) == b"\x82\xa0\x01" = 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.cbor.cbor import CBOR_MAP, CBORMapData from scapy.cborpacket import CBOR_Packet class AnyMapPkt2(CBOR_Packet): @@ -684,13 +686,15 @@ class AnyMapPkt2(CBOR_Packet): ) pkt = AnyMapPkt2(b"\x82\xa1\x01\x02\x00") -assert isinstance(pkt.a, CBORMapData) -assert pkt.a[1] == 2 +assert isinstance(pkt.a, CBOR_MAP) +assert isinstance(pkt.a.val, CBORMapData) +assert pkt.a.val[1].val == 2 pkt.b = 1 assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" = In-place mutation of CBORF_ANY list invalidates raw cache from scapy.cbor.cborfields import CBORF_ANY +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class AnyRoot(CBOR_Packet): @@ -698,7 +702,7 @@ class AnyRoot(CBOR_Packet): pkt = AnyRoot(b"\x82\x01\x02") assert pkt.raw_packet_cache == b"\x82\x01\x02" -pkt.value.append(3) +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) assert bytes(pkt) == b"\x83\x01\x02\x03" = Typed map lookup distinguishes integer 1 from boolean True @@ -875,7 +879,7 @@ 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.ts is CBOR_ABSENT assert pkt.endpoint is CBOR_ABSENT pkt.n = 1 assert bytes(pkt) == b"\xa1\x61n\x01" @@ -1140,10 +1144,11 @@ class RROptionalBooleanThenAny(CBOR_Packet): CBORF_ANY("value", None), ) +from scapy.cbor.cbor import CBOR_SIMPLE_VALUE for wire, expected in ((b"\xf0", 16), (b"\xf8\x20", 32)): pkt = RROptionalBooleanThenAny(wire) - assert isinstance(pkt.value, CBORSimpleValue) - assert pkt.value.value == expected + assert isinstance(pkt.value, CBOR_SIMPLE_VALUE) + assert pkt.value.val == expected = Optional null and undefined do not consume each other's wire values class RROptionalNullThenUndefined(CBOR_Packet): @@ -1370,11 +1375,15 @@ assert pkt.tail == 3 + Finding 5: recursive CBORF_ANY mutations must invalidate the raw cache = Appending to a decoded root CBORF_ANY array changes serialized bytes +from scapy.cbor.cbor import ( + CBOR_ARRAY, CBOR_MAP, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, + CBOR_UNSIGNED_INTEGER, CBORMapData, +) class RRMutableAnyRoot(CBOR_Packet): CBOR_root = CBORF_ANY("value", None) pkt = RRMutableAnyRoot(b"\x82\x01\x02") -pkt.value.append(3) +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) assert bytes(pkt) == b"\x83\x01\x02\x03" = Mutating a nested CBORF_ANY array changes serialized bytes @@ -1385,31 +1394,32 @@ class RRMutableAnyNested(CBOR_Packet): ) pkt = RRMutableAnyNested(b"\x82\x82\x01\x02\x00") -pkt.value.append(3) +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) assert bytes(pkt) == b"\x82\x83\x01\x02\x03\x00" = Mutating the list inside a decoded semantic tag changes serialized bytes pkt = RRMutableAnyRoot(b"\xd8\x2a\x82\x01\x02") -assert isinstance(pkt.value, CBORTagValue) -pkt.value.value.append(3) +assert isinstance(pkt.value, CBOR_SEMANTIC_TAG) +pkt.value.val[1].val.append(CBOR_UNSIGNED_INTEGER(3)) assert bytes(pkt) == b"\xd8\x2a\x83\x01\x02\x03" = Mutating a decoded semantic tag number changes serialized bytes pkt = RRMutableAnyRoot(b"\xd8\x2a\x01") -assert isinstance(pkt.value, CBORTagValue) -pkt.value.tag = 43 +assert isinstance(pkt.value, CBOR_SEMANTIC_TAG) +pkt.value.val = (43, pkt.value.val[1]) assert bytes(pkt) == b"\xd8\x2b\x01" = Mutating a decoded extended simple value changes serialized bytes pkt = RRMutableAnyRoot(b"\xf8\x20") -assert isinstance(pkt.value, CBORSimpleValue) -pkt.value.value = 33 +assert isinstance(pkt.value, CBOR_SIMPLE_VALUE) +pkt.value.val = 33 assert bytes(pkt) == b"\xf8\x21" = Mutating an array value inside a decoded map changes serialized bytes pkt = RRMutableAnyRoot(b"\xa1\x61a\x81\x01") -assert isinstance(pkt.value, CBORMapData) -pkt.value["a"].append(2) +assert isinstance(pkt.value, CBOR_MAP) +assert isinstance(pkt.value.val, CBORMapData) +pkt.value.val["a"].val.append(CBOR_UNSIGNED_INTEGER(2)) assert bytes(pkt) == b"\xa1\x61a\x82\x01\x02" @@ -1536,22 +1546,24 @@ except ValueError: pass = Nested mutable CBORF_ANY defaults are isolated between packet instances +from scapy.cbor.cbor import CBOR_ARRAY, CBOR_UNSIGNED_INTEGER, CBOR_SEMANTIC_TAG class RRNestedMutableDefault(CBOR_Packet): CBOR_root = CBORF_ANY("value", [[0]]) a = RRNestedMutableDefault() b = RRNestedMutableDefault() -a.value[0].append(1) -assert b.value == [[0]] +a.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(1)) +assert len(b.value.val[0].val) == 1 +assert b.value.val[0].val[0].val == 0 = Mutable semantic-tag defaults are isolated between packet instances class RRMutableTagDefault(CBOR_Packet): - CBOR_root = CBORF_ANY("value", CBORTagValue(1, [])) + CBOR_root = CBORF_ANY("value", CBOR_SEMANTIC_TAG((1, CBOR_ARRAY([])))) a = RRMutableTagDefault() b = RRMutableTagDefault() -a.value.value.append(1) -assert b.value == CBORTagValue(1, []) +a.value.val[1].val.append(CBOR_UNSIGNED_INTEGER(1)) +assert len(b.value.val[1].val) == 0 = Semantically duplicate map keys are rejected despite different encodings try: @@ -1610,6 +1622,7 @@ pkt.tail = 8 assert bytes(pkt) == b"\xa2\x64flag\x00\x64tail\x08" = Mutable CBORMapData defaults are isolated between packet instances +from scapy.cbor.cbor import CBOR_MAP, CBOR_UNSIGNED_INTEGER, CBORMapData, CBOR_TEXT_STRING class RRMutableMapDefault(CBOR_Packet): CBOR_root = CBORF_ANY( "value", @@ -1618,8 +1631,9 @@ class RRMutableMapDefault(CBOR_Packet): a = RRMutableMapDefault() b = RRMutableMapDefault() -a.value["a"].append(1) -assert b.value["a"] == [] +assert isinstance(a.value, CBOR_MAP) +a.value.val["a"].val.append(CBOR_UNSIGNED_INTEGER(1)) +assert len(b.value.val["a"].val) == 0 = Direct and extended simple values round-trip through CBORF_ANY for wire in (b"\xf0", b"\xf8\x20", b"\xf8\xff"): @@ -1688,9 +1702,10 @@ class AnyUndefinedMap(CBOR_Packet): wire = b"\xa1\x61u\xf7" pkt = AnyUndefinedMap(wire) -assert pkt.value["u"] is CBOR_UNDEFINED_VALUE +from scapy.cbor.cbor import CBOR_UNDEFINED +assert isinstance(pkt.value.val["u"], CBOR_UNDEFINED) clone = pkt.copy() -assert clone.value["u"] is CBOR_UNDEFINED_VALUE +assert isinstance(clone.value.val["u"], CBOR_UNDEFINED) assert bytes(clone) == wire + Finding 2 - Positional reservation must protect trailing required fields @@ -1722,12 +1737,12 @@ class OptionalBoolThenAnySequence(CBOR_Packet): # 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 arr.required.val is True assert bytes(arr) == b"\x81\xf5" seq = OptionalBoolThenAnySequence(b"\xf5") assert seq.getfieldval("maybe") is CBOR_ABSENT -assert seq.required is True +assert seq.required.val is True assert bytes(seq) == b"\xf5" = Indefinite arrays reserve the final item for a required ANY field @@ -1748,7 +1763,7 @@ class OptionalBoolThenAnyIndefinite(CBOR_Packet): pkt = OptionalBoolThenAnyIndefinite(b"\x9f\xf5\xff") assert pkt.getfieldval("maybe") is CBOR_ABSENT -assert pkt.required is True +assert pkt.required.val is True assert bytes(pkt) == b"\x9f\xf5\xff" = Optional ANY does not consume an item required by a trailing typed field @@ -1804,7 +1819,7 @@ class OptionalPacketThenAny(CBOR_Packet): pkt = OptionalPacketThenAny(b"\x81\xf5") assert pkt.getfieldval("child") is CBOR_ABSENT -assert pkt.required is True +assert pkt.required.val is True = Nonterminal SEQUENCE_OF stops at a typed delimiter in an unframed sequence from scapy.cbor.cborfields import ( @@ -1961,7 +1976,7 @@ class OptionalSemanticTagDefault(CBOR_Packet): ) pkt = OptionalSemanticTagDefault() -assert pkt.getfieldval("tag") is CBOR_ABSENT +assert pkt.getfieldval("value") is CBOR_ABSENT assert bytes(pkt) == b"\x80" = Fixed-schema maps reject duplicate known keys instead of silently taking the last value @@ -2072,7 +2087,7 @@ class AnyMapWithSibling(CBOR_Packet): wire = b"\x82\xa1\x01\x02\x00" pkt = AnyMapWithSibling(wire) -assert pkt.value[1] == 2 +assert pkt.value.val[1].val == 2 pkt.sibling = 1 assert bytes(pkt) == b"\x82\xa1\x01\x02\x01" @@ -2086,8 +2101,9 @@ class AnyArrayWithSibling(CBOR_Packet): CBORF_UNSIGNED_INTEGER("sibling", 0), ) +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER pkt = AnyArrayWithSibling(b"\x82\x82\x01\x02\x00") -pkt.value.append(3) +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) assert bytes(pkt) == b"\x82\x83\x01\x02\x03\x00" = Generic map lookup keeps integer 1 and Boolean true as distinct CBOR keys @@ -2098,9 +2114,9 @@ class TypedKeyMap(CBOR_Packet): CBOR_root = CBORF_ANY("value", None) 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 pkt.value.val[1].val == "i" +assert pkt.value.val[True].val == "b" +assert len(pkt.value.val.cbor_pairs()) == 2 assert bytes(pkt) == b"\xa2\x01\x61i\xf5\x61b" + Deterministic CBOR and float edge cases @@ -2658,13 +2674,14 @@ wire = ( b"\x61a\x07" b"\xff" ) +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER 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] +clone._cbor_unknown_map_pairs[0][1].val.append(CBOR_UNSIGNED_INTEGER(3)) +assert len(orig._cbor_unknown_map_pairs[0][1].val) == 2 +assert len(clone._cbor_unknown_map_pairs[0][1].val) == 3 + CBORF_ARRAY_OF indefinite decoding @@ -2701,8 +2718,7 @@ assert pkt.children[1].parent is pkt + Medium-severity review follow-ups -= CBORF_FLOAT preserves received half-float wire after cache clear -from scapy.cbor.cbor import CBORFloatValue += CBORF_FLOAT rebuilds preferred half-float after cache clear from scapy.cbor.cborfields import CBORF_FLOAT from scapy.cborpacket import CBOR_Packet @@ -2711,14 +2727,13 @@ class FloatPkt(CBOR_Packet): 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 +assert bytes(pkt) == wire # untouched raw cache pkt.raw_packet_cache = None pkt.raw_packet_cache_fields = None -assert bytes(pkt) == wire +assert bytes(pkt) == wire # preferred encoding for 1.5 is half pkt.value = 1.5 -assert bytes(pkt) == wire # preferred half for 1.5 +assert bytes(pkt) == wire = Unframed CBORF_SEQUENCE leaves trailing CBOR items from scapy.cbor.cborfields import CBORF_SEQUENCE, CBORF_UNSIGNED_INTEGER @@ -2850,23 +2865,22 @@ else: 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 += CBORF_FLOAT preserves non-preferred NaN payload only while raw cache is valid from scapy.cbor.cborfields import CBORF_FLOAT from scapy.cborpacket import CBOR_Packet class FloatPkt(CBOR_Packet): CBOR_root = CBORF_FLOAT("value", 0.0) -# binary64 NaN with a low payload bit (preferred width stays binary64) +# binary64 NaN with a low payload bit wire = bytes.fromhex("fb7ff8000000000001") pkt = FloatPkt(wire) -assert isinstance(pkt.value, CBORFloatValue) assert pkt.value != pkt.value # NaN -assert pkt.value.cbor_encoded == wire +assert bytes(pkt) == wire pkt.raw_packet_cache = None pkt.raw_packet_cache_fields = None -assert bytes(pkt) == wire +# After cache clear, rebuild uses preferred quiet NaN (binary16). +assert bytes(pkt) == bytes.fromhex("f97e00") = CBORF_TEXT_STRING rejects bytes values instead of str(bytes) corruption from scapy.cbor.cborfields import CBORF_TEXT_STRING @@ -2883,8 +2897,8 @@ except TypeError: 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 += CBORF_ANY preserves non-preferred float wire only while raw cache is valid +from scapy.cbor.cbor import CBOR_FLOAT from scapy.cbor.cborfields import CBORF_ANY from scapy.cborpacket import CBOR_Packet @@ -2894,11 +2908,12 @@ class AnyFloatPkt(CBOR_Packet): # 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 +assert isinstance(pkt.value, CBOR_FLOAT) +assert pkt.value.val != pkt.value.val # NaN +assert bytes(pkt) == wire pkt.raw_packet_cache = None pkt.raw_packet_cache_fields = None +# CBOR_FLOAT.enc() still preserves _encoded when present on the object. assert bytes(pkt) == wire = RandCBORObject generates encodable objects including nested containers From 1e0eb344d5e4432719fd7aab40de4b1bf0c9f263 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:31:21 +0200 Subject: [PATCH 06/48] cbor: store unknown map extensions in per-map packet fields Replace packet-global _cbor_unknown_map_pairs with a dedicated mutable field on each CBORF_MAP so nested and sibling maps keep independent extensions across copy and rebuild. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cbor/cborfields.py | 68 +++++++++++++++++++++++++++++++++----- scapy/cborpacket.py | 11 +----- test/scapy/layers/cbor.uts | 13 ++++---- 3 files changed, 68 insertions(+), 24 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index b57a945fa7e..f75837e02e9 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1775,6 +1775,42 @@ def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.name) +class CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): + """Per-map storage for unknown text-key extension pairs. + + Not a CBOR wire field by itself: owning :class:`CBORF_MAP` instances read + and write this packet field around known members. + """ + ismutable = True + islist = 1 + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> List[Tuple[str, Any]] + if x is None or x is CBOR_ABSENT: + return [] + return list(x) + + def do_copy(self, x): # type: ignore[override] + # type: (Any) -> Any + return copy.deepcopy(x) + + def is_empty(self, pkt): + # type: (CBOR_Packet) -> bool + return not pkt.getfieldval(self.name) + + def encode_value(self, x): + # type: (Any) -> bytes + raise CBOR_Encoding_Error( + "CBORF_MAP_UNKNOWN is not encoded as a standalone CBOR item" + ) + + def m2i(self, pkt, s): + # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] + raise CBOR_Decoding_Error( + "CBORF_MAP_UNKNOWN is not decoded as a standalone CBOR item" + ) + + class CBORF_MAP(CBORF_element): """ CBOR map with a fixed set of named, typed fields (major type 5). @@ -1792,11 +1828,11 @@ class CBORF_MAP(CBORF_element): 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. + Unknown received key/value pairs are retained in a dedicated packet field + (``unknown_field``, defaulting to a unique ``_cbor_unknown_`` name) as + ordered ``(key, value)`` pairs. While the packet raw cache is valid the + exact received bytes are preserved; after any mutation unknown members are + re-encoded using core-deterministic CBOR together with known fields. Example:: @@ -1809,9 +1845,16 @@ class MyCBOR(CBOR_Packet): CBOR_tag = CBOR_MajorTypes.MAP holds_packets = 1 islist = 1 + _unknown_id = 0 def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None + unknown_field = kwargs.pop("unknown_field", None) + if kwargs: + raise TypeError( + "CBORF_MAP() got unexpected keyword arguments: %s" + % ", ".join(sorted(kwargs)) + ) self.seq = seq field_by_name = {} # type: Dict[str, Any] encoded_keys = {} # type: Dict[str, bytes] @@ -1825,6 +1868,15 @@ def __init__(self, *seq, **kwargs): encoded_keys[name] = CBORcodec_TEXT_STRING.enc(name) self._field_by_name = field_by_name self._encoded_keys = encoded_keys + if unknown_field is None: + CBORF_MAP._unknown_id += 1 + unknown_field = "_cbor_unknown_%d" % CBORF_MAP._unknown_id + if unknown_field in field_by_name: + raise ValueError( + "CBORF_MAP unknown_field %r collides with a known member" + % (unknown_field,) + ) + self._unknown_field = CBORF_MAP_UNKNOWN(unknown_field, []) def __repr__(self): # type: () -> str @@ -1840,7 +1892,7 @@ def get_fields_list(self): child for field in self.seq for child in field.get_fields_list() - ] + ] + [self._unknown_field] def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult @@ -1856,7 +1908,7 @@ def build_result(self, pkt): % fld.name ) pairs.append((self._encoded_keys[fld.name], value_result.data)) - unknown = getattr(pkt, "_cbor_unknown_map_pairs", None) or [] + unknown = pkt.getfieldval(self._unknown_field.name) or [] for key, value in unknown: key_bytes = CBORcodec_TEXT_STRING.enc(key) value_bytes = CBORcodec_Object.encode_cbor_item_deterministic(value) @@ -1976,7 +2028,7 @@ def _dissect_value_bytes(fld, val_bytes): raise CBOR_Decoding_Error( "Required map field %r is missing" % fld.name ) - pkt._cbor_unknown_map_pairs = unknown_pairs # type: ignore[attr-defined] + self._unknown_field.set_val(pkt, unknown_pairs) return CBORParseResult(remaining=remaining, items=1) def _mark_map_field_absent(self, pkt, fld): diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 8c066f6366c..0ac3eb1c7da 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -193,19 +193,10 @@ def copy(self): 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) + setattr(clone, attr, getattr(self, attr)) 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: diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index a1b663c74f0..26af54a1536 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2660,11 +2660,13 @@ assert bytes(pkt) == b"\xa2\x61x\xa2\x61a\x02\x61b\x01\x61z\x02" = CBORF_MAP copy isolates nested unknown extension values from the original packet from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER from scapy.cborpacket import CBOR_Packet class MapCopyIsolation(CBOR_Packet): CBOR_root = CBORF_MAP( CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", ) wire = ( @@ -2674,14 +2676,13 @@ wire = ( b"\x61a\x07" b"\xff" ) -from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER 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].val.append(CBOR_UNSIGNED_INTEGER(3)) -assert len(orig._cbor_unknown_map_pairs[0][1].val) == 2 -assert len(clone._cbor_unknown_map_pairs[0][1].val) == 3 +assert clone.unknown_pairs is not orig.unknown_pairs +assert clone.unknown_pairs[0][1] is not orig.unknown_pairs[0][1] +clone.unknown_pairs[0][1].val.append(CBOR_UNSIGNED_INTEGER(3)) +assert len(orig.unknown_pairs[0][1].val) == 2 +assert len(clone.unknown_pairs[0][1].val) == 3 + CBORF_ARRAY_OF indefinite decoding From e674bf432d0af60bc2f03babff2eb4d073954d53 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:32:08 +0200 Subject: [PATCH 07/48] cbor: restore standard Packet.do_build and slim copy() Unknown map state now lives in normal fields, so CBOR packets can use Scapy's default build path. Keep only parent reattachment in copy(). Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cborpacket.py | 22 ++------------- test/scapy/layers/cbor.uts | 56 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 20 deletions(-) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 0ac3eb1c7da..2c8812a4077 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -13,8 +13,6 @@ from scapy.base_classes import Packet_metaclass from scapy.packet import Packet -import copy - from typing import ( Any, Dict, @@ -164,18 +162,6 @@ def self_build(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 result = self.CBOR_root.dissect_result(self, x) @@ -191,12 +177,8 @@ def copy(self): ``parent`` for ownership, so reattach after the clone is built. """ clone = super(CBOR_Packet, self).copy() - for attr in ( - "_cbor_raw_cache_items", - "_crc_content_span", - ): - if hasattr(self, attr): - setattr(clone, attr, getattr(self, attr)) + if hasattr(self, "_cbor_raw_cache_items"): + clone._cbor_raw_cache_items = self._cbor_raw_cache_items # type: ignore[attr-defined] 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: diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 26af54a1536..eda0fe33ad0 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3215,3 +3215,59 @@ built = bytes(pkt) assert b"\x61x\x05" in built assert b"\x61u\x09" in built assert pkt.child.inner == 2 + + ++ CBOR_Packet standard Scapy lifecycle + += Unknown map extensions survive Packet.__iter__ and standard do_build +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LifeMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", + ) + +wire = b"\xa2\x61a\x01\x61x\x0a" +pkt = LifeMap(wire) +pkt.a = 2 # clears explicit / raw cache +assert bytes(pkt) == b"\xa2\x61a\x02\x61x\x0a" +iterated = next(iter(pkt)) +assert bytes(iterated) == b"\xa2\x61a\x02\x61x\x0a" +assert iterated.unknown_pairs[0][0] == "x" + += fuzz() and show() accept CBOR packets with unknown map members +from scapy.packet import fuzz +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LifeMap2(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", + ) + +pkt = LifeMap2(b"\xa2\x61a\x01\x61x\x0a") +fuzzed = fuzz(pkt) +assert isinstance(fuzzed, LifeMap2) +pkt.show(dump=True) +pkt.show2(dump=True) + += copy and deepcopy re-parent nested CBOR packets +import copy +from scapy.cbor.cborfields import CBORF_PACKET, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LifeChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) + +class LifeParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, LifeChild) + +pkt = LifeParent(child=LifeChild(n=1)) +cloned = pkt.copy() +deep = copy.deepcopy(pkt) +assert cloned.child.parent is cloned +assert deep.child.parent is deep +assert cloned.child.n == 1 and deep.child.n == 1 From 5b2ce8120dbbbcbf5ad344870bea009b7e51591f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:35:40 +0200 Subject: [PATCH 08/48] cbor: require explicit SEQUENCE_OF framing and Scapy list limits Reject nonterminal unbounded SEQUENCE_OF, support count_from/max_count, and replace the 1<<30 sentinel with conf.max_list_count. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cbor/cborfields.py | 81 +++++++++++++++++++++------------ test/scapy/layers/cbor.uts | 93 ++++++++++++++++++++++---------------- 2 files changed, 106 insertions(+), 68 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index f75837e02e9..7479d860922 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1239,7 +1239,7 @@ def _dissect_children_budgeted(self, pkt, s, count): 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* + # 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 ( @@ -1247,12 +1247,8 @@ def _dissect_children_budgeted(self, pkt, s, count): 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. + # Validate without constructing a throwaway packet. + field._field.parse_value(pkt, remaining) self._mark_absent(pkt, field) continue try: @@ -1376,7 +1372,7 @@ def __init__(self, *seq, **kwargs): def _reject_ambiguous_unbounded_sequences(self): # type: () -> None - def _unbounded(field): + def _is_unbounded_sequence_of(field): # type: (Any) -> bool if isinstance(field, CBORF_optional): return False @@ -1384,27 +1380,17 @@ def _unbounded(field): 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] - ) + and getattr(field, "is_unbounded", False) ) - 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)): + for index, field in enumerate(self.seq): + if not _is_unbounded_sequence_of(field): + continue + # Unbounded SEQUENCE_OF must be the final schema field. + if index != len(self.seq) - 1: raise ValueError( - "Ambiguous unbounded CBOR sequences in array schema" + "Unbounded CBORF_SEQUENCE_OF must be the last field " + "in the sequence (or provide count_from=)" ) def build_result(self, pkt): @@ -1478,6 +1464,10 @@ class CBORF_SEQUENCE_OF(CBORF_field[List[Any]]): :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``. + + An unbounded ``SEQUENCE_OF`` (no ``count_from``) must be the last field in + its surrounding sequence/array. Use ``count_from`` for an explicit length + and ``max_count`` to cap decoding (defaults to ``conf.max_list_count``). """ CBOR_tag = None islist = 1 @@ -1487,12 +1477,16 @@ def __init__(self, default, # type: Any pkt_cls=None, # type: _ARRAY_T next_cls_cb=None, # type: Optional[Callable[..., Optional[Type[Packet]]]] # noqa: E501 + count_from=None, # type: Optional[Any] + max_count=None, # type: Optional[int] ): # type: (...) -> None self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] self.cls = None self.item_field = None self.holds_packets = 0 + self.count_from = count_from + self.max_count = max_count if next_cls_cb is not None: if pkt_cls is not None: @@ -1510,7 +1504,9 @@ def __init__(self, else: self.item_field = chosen self.holds_packets = 0 - elif hasattr(chosen, "CBOR_root") or callable(chosen): + elif hasattr(chosen, "CBOR_root") or ( + isinstance(chosen, type) and issubclass(chosen, Packet) + ): self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 else: @@ -1519,6 +1515,15 @@ def __init__(self, ) super(CBORF_SEQUENCE_OF, self).__init__(name, default) + @property + def is_unbounded(self): + # type: () -> bool + return self.count_from is None + + def _list_limit(self): + # type: () -> int + return self.max_count or config.conf.max_list_count + def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> List[Any] if x is None: @@ -1536,9 +1541,19 @@ def _decode_items(self, pkt, data, max_items=None): values = [] # type: List[Any] remaining = data consumed = 0 + limit = self._list_limit() + if self.count_from is not None: + if callable(self.count_from): + max_items = int(self.count_from(pkt)) + else: + max_items = int(pkt.getfieldval(self.count_from)) while remaining and not cbor_is_break(remaining): if max_items is not None and consumed >= max_items: break + if consumed >= limit: + raise CBOR_Decoding_Error( + "CBOR SEQUENCE_OF exceeded max_count=%d" % limit + ) before_len = len(remaining) if self.holds_packets: pkt_cls = self.cls @@ -1619,11 +1634,17 @@ def build_result(self, pkt): def min_items(self, pkt): # type: (CBOR_Packet) -> int + if self.count_from is not None and pkt is not None: + if callable(self.count_from): + return int(self.count_from(pkt)) + return int(pkt.getfieldval(self.count_from)) return 0 def max_items(self, pkt): # type: (CBOR_Packet) -> int - return 1 << 30 + if self.count_from is not None and pkt is not None: + return self.min_items(pkt) + return self._list_limit() def i2repr(self, pkt, x): # type: (CBOR_Packet, Any) -> str @@ -1673,7 +1694,9 @@ def __init__(self, else: self.item_field = chosen self.holds_packets = 0 - elif hasattr(chosen, "CBOR_root") or callable(chosen): + elif hasattr(chosen, "CBOR_root") or ( + isinstance(chosen, type) and issubclass(chosen, Packet) + ): self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 else: diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index eda0fe33ad0..f3c2d05ef0a 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -886,23 +886,44 @@ assert bytes(pkt) == b"\xa1\x61n\x01" + array item reservation -= Nonterminal SEQUENCE_OF reserves items for a later required field += Nonterminal unbounded SEQUENCE_OF is rejected at schema construction 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( +try: + CBORF_ARRAY( CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), CBORF_UNSIGNED_INTEGER("tail", 0), ) + assert False, "nonterminal unbounded SEQUENCE_OF was accepted" +except ValueError: + pass -pkt = SeqThenReq(b"\x83\x01\x02\x03") -assert pkt.vals == [1, 2] -assert pkt.tail == 3 += SEQUENCE_OF with count_from consumes an explicit number of items +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +class CountedSeq(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_SEQUENCE_OF( + "vals", + [], + CBORF_UNSIGNED_INTEGER, + count_from=lambda pkt: pkt.n, + ), + ) + +pkt = CountedSeq(b"\x83\x02\x0a\x0b") +assert pkt.n == 2 +assert pkt.vals == [10, 11] = Optional same-type scalar reserves the sole item for a required tail from scapy.cbor.cborfields import ( @@ -923,23 +944,21 @@ 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 += Indefinite array rejects nonterminal unbounded SEQUENCE_OF 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( +try: + 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 + assert False, "nonterminal unbounded SEQUENCE_OF was accepted" +except ValueError: + pass + Shared helpers @@ -1296,9 +1315,9 @@ assert bytes(pkt) == b"\x82\xf6\x01" + Finding 4: positional arrays must reserve items for later required fields -= Definite arrays reserve the final item after a nonterminal SEQUENCE_OF -class RRSequenceThenTail(CBOR_Packet): - CBOR_root = CBORF_ARRAY( += Definite arrays reject nonterminal unbounded SEQUENCE_OF +try: + CBORF_ARRAY( CBORF_SEQUENCE_OF( "values", [], @@ -1306,14 +1325,13 @@ class RRSequenceThenTail(CBOR_Packet): ), CBORF_UNSIGNED_INTEGER("tail", None), ) + assert False, "nonterminal unbounded SEQUENCE_OF accepted" +except ValueError: + pass -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( += Indefinite arrays reject nonterminal unbounded SEQUENCE_OF +try: + CBORF_ARRAY_INDEFINITE( CBORF_SEQUENCE_OF( "values", [], @@ -1321,10 +1339,9 @@ class RRIndefiniteSequenceThenTail(CBOR_Packet): ), CBORF_UNSIGNED_INTEGER("tail", None), ) - -pkt = RRIndefiniteSequenceThenTail(b"\x9f\x01\x02\x03\xff") -assert pkt.values == [1, 2] -assert pkt.tail == 3 + assert False, "nonterminal unbounded SEQUENCE_OF accepted" +except ValueError: + pass = An optional scalar yields a sole item to a required scalar of the same type class RROptionalThenRequiredUnsigned(CBOR_Packet): @@ -1351,7 +1368,7 @@ class RROptionalThenRequiredPacket(CBOR_Packet): pkt = RROptionalThenRequiredPacket(b"\x81\x07") assert pkt.required_child.value == 7 -= A SEQUENCE_OF reserves an item for a later required conditional field += A SEQUENCE_OF before a required conditional field must use count_from class RRSequenceThenConditionalTail(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("flag", 0), @@ -1359,6 +1376,7 @@ class RRSequenceThenConditionalTail(CBOR_Packet): "values", [], CBORF_UNSIGNED_INTEGER("item", None), + count_from=lambda pkt: 1, ), CBORF_CONDITIONAL( CBORF_UNSIGNED_INTEGER("tail", None), @@ -1821,25 +1839,22 @@ pkt = OptionalPacketThenAny(b"\x81\xf5") assert pkt.getfieldval("child") is CBOR_ABSENT assert pkt.required.val is True -= Nonterminal SEQUENCE_OF stops at a typed delimiter in an unframed sequence += Nonterminal unbounded SEQUENCE_OF is rejected in unframed sequences from scapy.cbor.cborfields import ( CBORF_SEQUENCE, CBORF_SEQUENCE_OF, CBORF_TEXT_STRING, CBORF_UNSIGNED_INTEGER, ) -from scapy.cborpacket import CBOR_Packet -class IntSequenceThenText(CBOR_Packet): - CBOR_root = CBORF_SEQUENCE( +try: + CBORF_SEQUENCE( CBORF_SEQUENCE_OF("items", [], CBORF_UNSIGNED_INTEGER), CBORF_TEXT_STRING("tail", ""), ) - -pkt = IntSequenceThenText(b"\x01\x02\x61x") -assert pkt.items == [1, 2] -assert pkt.tail == "x" -assert bytes(pkt) == b"\x01\x02\x61x" + assert False, "nonterminal unbounded SEQUENCE_OF accepted" +except ValueError: + pass = Ambiguous unbounded array schema is rejected even with an optional field between sequences from scapy.cbor.cborfields import ( From 508a897b359b6c476f12b9095ce68adde40f5801 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 06:37:52 +0200 Subject: [PATCH 09/48] cbor: trim exports, tighten deterministic defaults, simplify byte-string packets Default deterministic scanning rejects indefinite containers, drop internal result types from the public surface, and simplify BYTE_STRING_PACKET decode. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cbor/__init__.py | 13 ------------- scapy/cbor/cborcodec.py | 9 ++++----- scapy/cbor/cborfields.py | 31 ++++++++++--------------------- scapy/cborpacket.py | 4 +++- test/scapy/layers/cbor.uts | 12 +++++++++--- 5 files changed, 26 insertions(+), 43 deletions(-) diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index 1d9574d7c1a..cb1dea8bdc7 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -29,13 +29,9 @@ CBOR_NULL, CBOR_UNDEFINED, CBOR_UNDEFINED_VALUE, - CBOR_NO_ITEM, CBOR_FLOAT, - CBORFloatValue, CBOR_DECODING_ERROR, RandCBORObject, - CBORTagValue, - CBORSimpleValue, ) from scapy.cbor.cborcodec import ( @@ -51,8 +47,6 @@ ) from scapy.cbor.cborfields import ( - CBORBuildResult, - CBORParseResult, CBORF_element, CBORF_field, CBORF_ANY, @@ -106,12 +100,8 @@ "CBOR_NULL", "CBOR_UNDEFINED", "CBOR_UNDEFINED_VALUE", - "CBOR_NO_ITEM", "CBOR_FLOAT", - "CBORFloatValue", "CBOR_DECODING_ERROR", - "CBORTagValue", - "CBORSimpleValue", # Random/Fuzzing "RandCBORObject", # Codec classes @@ -124,9 +114,6 @@ "CBORcodec_MAP", "CBORcodec_SEMANTIC_TAG", "CBORcodec_SIMPLE_AND_FLOAT", - # Result types - "CBORBuildResult", - "CBORParseResult", # Field base classes "CBORF_element", "CBORF_field", diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 5157cfc89f2..9091489b818 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -358,14 +358,13 @@ def _cbor_preferred_float_ai_from_encoded(ai, bits): return _cbor_preferred_float_ai(float_val) -def cbor_find_non_deterministic(s, allow_indefinite=True, base_offset=0): +def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): # type: (bytes, bool, int) -> List[Tuple[int, str]] - """Scan *s* for non-shortest CBOR argument encodings. + """Scan *s* for encodings that are not RFC 8949 core-deterministic. 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``. + items are rejected by default. Protocols that permit indefinite containers + (for example some BPv7 outer arrays) may pass ``allow_indefinite=True``. """ issues = [] # type: List[Tuple[int, str]] index = [0] diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 7479d860922..36982abb40e 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -56,7 +56,6 @@ CBORcodec_TEXT_STRING, CBORcodec_SIMPLE_AND_FLOAT, ) -from scapy.error import log_runtime from scapy.packet import Packet from scapy.volatile import ( RandChoice, @@ -767,11 +766,7 @@ def m2i(self, pkt, s): def encode_value(self, x): # type: (Any) -> bytes - data = bytes(x) - if self.definite_only: - # Always emit definite form (codec already does). - pass - return CBORcodec_BYTE_STRING.enc(data) + return CBORcodec_BYTE_STRING.enc(bytes(x)) def randval(self): # type: () -> RandString @@ -803,30 +798,24 @@ def __init__(self, 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] + # type: (CBOR_Packet, bytes) -> Optional[Type[Packet]] if self.pkt_cls is not None: - return self.pkt_cls, True + return self.pkt_cls 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 + return self.cls_cb(pkt, data) + return None def _decode_packet_value(self, pkt, data): # type: (CBOR_Packet, bytes) -> Packet - pkt_cls, registered = self._resolve_packet_class(pkt, data) + pkt_cls = self._resolve_packet_class(pkt, data) if pkt_cls is None: - return _cbor_packet_from_bytes(packet.Raw, data, pkt) + return packet.Raw(data) 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) + raise CBOR_Decoding_Error( + "Failed to decode byte-string packet content: %s" % exc + ) from exc def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> Packet diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 2c8812a4077..99717c35051 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -178,7 +178,9 @@ def copy(self): """ clone = super(CBOR_Packet, self).copy() if hasattr(self, "_cbor_raw_cache_items"): - clone._cbor_raw_cache_items = self._cbor_raw_cache_items # type: ignore[attr-defined] + clone._cbor_raw_cache_items = ( # type: ignore[attr-defined] + self._cbor_raw_cache_items + ) 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: diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index f3c2d05ef0a..3cea14ca0be 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1688,7 +1688,7 @@ assert bytes(undefined.copy()) == b"\xf7" = CBOR structural sentinels preserve singleton identity under copy operations import copy -from scapy.cbor import CBOR_NO_ITEM, CBOR_UNDEFINED_VALUE +from scapy.cbor.cbor import CBOR_NO_ITEM, CBOR_UNDEFINED_VALUE from scapy.cbor.cborfields import CBOR_ABSENT for sentinel in (CBOR_ABSENT, CBOR_UNDEFINED_VALUE, CBOR_NO_ITEM): @@ -2152,8 +2152,14 @@ assert cbor_find_non_deterministic(wire) == [] = Indefinite maps require bytewise lexicographic key order from scapy.cbor.cborcodec import cbor_find_non_deterministic -assert not cbor_find_non_deterministic(bytes.fromhex("bf616101616202ff")) -assert cbor_find_non_deterministic(bytes.fromhex("bf616201616102ff")) +assert not cbor_find_non_deterministic( + bytes.fromhex("bf616101616202ff"), + allow_indefinite=True, +) +assert cbor_find_non_deterministic( + bytes.fromhex("bf616201616102ff"), + allow_indefinite=True, +) = NaN preferred width uses the original payload bit pattern from scapy.cbor.cborcodec import cbor_find_non_deterministic From 12a522d5b826b8d7c0ca25550500d6f46f1ac549 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 07:01:04 +0200 Subject: [PATCH 10/48] cbor: remove legacy Float/Tag/Simple/UNDEFINED_VALUE wrappers Use CBOR_FLOAT, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, and a CBOR_UNDEFINED singleton directly in the object model and update tests accordingly. Co-authored-by: Cursor AI-Assisted: yes (Composer) --- scapy/cbor/__init__.py | 2 - scapy/cbor/cbor.py | 122 +++-------------------- scapy/cbor/cborcodec.py | 31 +----- scapy/cbor/cborfields.py | 54 ++++------ test/scapy/layers/cbor.uts | 43 ++++---- test/scapy/layers/cbor_cbor2_interop.uts | 68 ++++++------- 6 files changed, 85 insertions(+), 235 deletions(-) diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index cb1dea8bdc7..8d2316eb439 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -28,7 +28,6 @@ CBOR_TRUE, CBOR_NULL, CBOR_UNDEFINED, - CBOR_UNDEFINED_VALUE, CBOR_FLOAT, CBOR_DECODING_ERROR, RandCBORObject, @@ -99,7 +98,6 @@ "CBOR_TRUE", "CBOR_NULL", "CBOR_UNDEFINED", - "CBOR_UNDEFINED_VALUE", "CBOR_FLOAT", "CBOR_DECODING_ERROR", # Random/Fuzzing diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index b59fb96addc..b37f3b80617 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -500,14 +500,13 @@ 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. + # Normalize CBOR_Object keys 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) + return ("undef", None) if isinstance(key, CBOR_UNSIGNED_INTEGER): return (int, int(key.val)) if isinstance(key, CBOR_NEGATIVE_INTEGER): @@ -529,12 +528,11 @@ def _key_identity(key): 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. + # bool is a subclass of int. if isinstance(key, bool): return (bool, key) if isinstance(key, float): - encoded = getattr(key, "cbor_encoded", None) - return CBORMapData._float_key_identity(key, encoded) + return CBORMapData._float_key_identity(key) if isinstance(key, int): return (int, key) return (type(key), key) @@ -686,84 +684,34 @@ def __init__(self): class CBOR_UNDEFINED(CBOR_Object[None]): - """CBOR undefined value""" + """CBOR undefined value (singleton).""" tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT + _instance = None # type: Optional["CBOR_UNDEFINED"] + + def __new__(cls): + # type: () -> CBOR_UNDEFINED + if cls._instance is None: + cls._instance = CBOR_Object.__new__(cls) + return cls._instance def __init__(self): # type: () -> None - super(CBOR_UNDEFINED, self).__init__(None) - - -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" + if not hasattr(self, "val"): + super(CBOR_UNDEFINED, self).__init__(None) def __bool__(self): # type: () -> bool return False def __copy__(self): - # type: () -> _CBORUndefined + # type: () -> CBOR_UNDEFINED return self def __deepcopy__(self, memo): - # type: (dict) -> _CBORUndefined + # type: (dict) -> CBOR_UNDEFINED return self -CBOR_UNDEFINED_VALUE = _CBORUndefined() - - class _CBORNoItem(object): """Structural sentinel: sequence ended without consuming input.""" @@ -800,38 +748,6 @@ def enc(self, codec=None): 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__() - - def _cbor_key_norm(value): # type: (Any) -> Any """Return a hashable RFC 8949 map-key equivalence form for *value*. @@ -841,12 +757,6 @@ def _cbor_key_norm(value): order-sensitively; maps compare as unordered pairs of norms. Semantic tags require the same tag number and an equivalent tagged value. """ - if isinstance(value, CBORTagValue): - return ("tag", int(value.tag), _cbor_key_norm(value.value)) - if isinstance(value, CBORSimpleValue): - return ("simple", int(value.value)) - if value is CBOR_UNDEFINED_VALUE: - return ("undef", None) if isinstance(value, CBOR_Object): if isinstance(value, (CBOR_TRUE, CBOR_FALSE)): return ("bool", bool(value.val)) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 9091489b818..ec54f01f76e 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -1176,25 +1176,11 @@ def _encode_cbor_item(item): """Encode a Python value to CBOR bytes""" 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): @@ -1214,9 +1200,6 @@ 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) @@ -1249,7 +1232,7 @@ def _encode_cbor_item_deterministic(item): 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 + :class:`~scapy.cbor.cbor.CBOR_Object` instances are accepted and reduced to native values (preferred float encoding, deterministic nested maps). """ from scapy.cbor.cbor import ( @@ -1259,10 +1242,7 @@ def _encode_cbor_item_deterministic(item): CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, CBOR_UNDEFINED, - CBOR_UNDEFINED_VALUE, CBORMapData, - CBORTagValue, - CBORSimpleValue, ) if isinstance(item, CBOR_Object): @@ -1285,15 +1265,6 @@ def _encode_cbor_item_deterministic(item): 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): diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 36982abb40e..938bc0310c7 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -32,13 +32,10 @@ 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, @@ -197,12 +194,12 @@ def cbor_object_to_python(obj): """Convert a :class:`CBOR_Object` tree to native Python values. Prefer keeping :class:`CBOR_Object` for arbitrary CBOR (``CBORF_ANY``). - This helper remains for typed-field coercion and legacy call sites. + Tags, simples, and undefined stay as ``CBOR_Object`` instances. """ if not isinstance(obj, CBOR_Object): return obj - if isinstance(obj, CBOR_UNDEFINED): - return CBOR_UNDEFINED_VALUE + if isinstance(obj, (CBOR_UNDEFINED, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE)): + return obj if isinstance(obj, CBOR_ARRAY): return [cbor_object_to_python(item) for item in obj.val] if isinstance(obj, CBOR_MAP): @@ -217,11 +214,6 @@ def cbor_object_to_python(obj): (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): return float(obj.val) return obj.val @@ -229,7 +221,7 @@ def cbor_object_to_python(obj): def python_to_cbor_object(value): # type: (Any) -> Any - """Convert native Python / legacy wrappers into a :class:`CBOR_Object` tree.""" + """Convert native Python values into a :class:`CBOR_Object` tree.""" from scapy.cbor.cbor import ( CBOR_ARRAY, CBOR_BYTE_STRING, @@ -238,30 +230,13 @@ def python_to_cbor_object(value): CBOR_MAP, CBOR_NEGATIVE_INTEGER, CBOR_NULL, - CBOR_SEMANTIC_TAG, - CBOR_SIMPLE_VALUE, CBOR_TEXT_STRING, CBOR_TRUE, - CBOR_UNDEFINED, CBOR_UNSIGNED_INTEGER, CBORMapData, - CBORFloatValue, - CBORSimpleValue, - CBORTagValue, - CBOR_UNDEFINED_VALUE, ) if isinstance(value, CBOR_Object): return value - if value is CBOR_UNDEFINED_VALUE: - return CBOR_UNDEFINED() - if isinstance(value, CBORTagValue): - return CBOR_SEMANTIC_TAG( - (value.tag, python_to_cbor_object(value.value)) - ) - if isinstance(value, CBORSimpleValue): - return CBOR_SIMPLE_VALUE(value.value) - if isinstance(value, CBORFloatValue): - return CBOR_FLOAT(float(value), encoded=value.cbor_encoded) if isinstance(value, CBORMapData): return CBOR_MAP(CBORMapData([ (python_to_cbor_object(k), python_to_cbor_object(v)) @@ -405,7 +380,9 @@ def i2m(self, pkt, x): def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> _I - if x is CBOR_ABSENT or x is CBOR_UNDEFINED_VALUE or x is CBOR_NO_ITEM: + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return cast(_I, x) + if isinstance(x, CBOR_UNDEFINED): return cast(_I, x) if isinstance(x, CBOR_Object): x = cbor_object_to_python(x) @@ -482,7 +459,9 @@ def max_items(self, pkt): def do_copy(self, x): # type: (Any) -> Any - if x is CBOR_ABSENT or x is CBOR_UNDEFINED_VALUE or x is CBOR_NO_ITEM: + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return x + if isinstance(x, CBOR_UNDEFINED): return x if isinstance(x, list): return copy.deepcopy(x) @@ -572,13 +551,17 @@ def matches_next_item(self, pkt, s): 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: + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return x + if isinstance(x, CBOR_UNDEFINED): return x return copy.deepcopy(x) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> Any - if x is CBOR_ABSENT or x is CBOR_NO_ITEM or x is CBOR_UNDEFINED_VALUE: + if x is CBOR_ABSENT or x is CBOR_NO_ITEM: + return x + if isinstance(x, CBOR_UNDEFINED): return x return python_to_cbor_object(x) @@ -591,10 +574,7 @@ def build_result(self, pkt): def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] - obj, remain = CBORcodec_Object.decode_cbor_item(s) - if isinstance(obj, CBOR_UNDEFINED): - return CBOR_UNDEFINED_VALUE, remain - return obj, remain + return CBORcodec_Object.decode_cbor_item(s) def encode_value(self, x): # type: (Any) -> bytes diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 3cea14ca0be..23b4d93808f 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -973,8 +973,6 @@ from scapy.cbor.cbor import ( CBOR_FALSE, CBOR_TRUE, CBOR_FLOAT, - CBORTagValue, - CBORSimpleValue, ) from scapy.cbor.cborcodec import ( CBOR_Codec_Decoding_Error, @@ -1660,9 +1658,9 @@ for wire in (b"\xf0", b"\xf8\x20", b"\xf8\xff"): + Finding 1 - CBOR sentinel identity survives Scapy copying -= CBOR_ABSENT and CBOR_UNDEFINED_VALUE survive Packet.copy and deepcopy += CBOR_ABSENT and CBOR_UNDEFINED survive Packet.copy and deepcopy import copy -from scapy.cbor import CBOR_UNDEFINED_VALUE +from scapy.cbor import CBOR_UNDEFINED from scapy.cbor.cborfields import CBORF_ANY, CBORF_ARRAY, CBORF_optional, CBOR_ABSENT from scapy.cborpacket import CBOR_Packet @@ -1672,7 +1670,7 @@ class OptionalAnyCopy(CBOR_Packet): ) class UndefinedAnyCopy(CBOR_Packet): - CBOR_root = CBORF_ANY("value", CBOR_UNDEFINED_VALUE) + CBOR_root = CBORF_ANY("value", CBOR_UNDEFINED()) absent = OptionalAnyCopy(b"\x80") assert absent.getfieldval("value") is CBOR_ABSENT @@ -1681,17 +1679,17 @@ assert copy.deepcopy(absent).getfieldval("value") is CBOR_ABSENT assert bytes(absent.copy()) == b"\x80" 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 undefined.getfieldval("value") is CBOR_UNDEFINED() +assert undefined.copy().getfieldval("value") is CBOR_UNDEFINED() +assert copy.deepcopy(undefined).getfieldval("value") is CBOR_UNDEFINED() assert bytes(undefined.copy()) == b"\xf7" = CBOR structural sentinels preserve singleton identity under copy operations import copy -from scapy.cbor.cbor import CBOR_NO_ITEM, CBOR_UNDEFINED_VALUE +from scapy.cbor.cbor import CBOR_NO_ITEM, CBOR_UNDEFINED from scapy.cbor.cborfields import CBOR_ABSENT -for sentinel in (CBOR_ABSENT, CBOR_UNDEFINED_VALUE, CBOR_NO_ITEM): +for sentinel in (CBOR_ABSENT, CBOR_UNDEFINED(), CBOR_NO_ITEM): assert copy.copy(sentinel) is sentinel assert copy.deepcopy(sentinel) is sentinel @@ -1711,7 +1709,7 @@ assert fresh.copy().getfieldval("value") is CBOR_ABSENT assert bytes(fresh.copy()) == b"\x80" = Undefined values nested in a generic map survive packet copies -from scapy.cbor import CBOR_UNDEFINED_VALUE +from scapy.cbor import CBOR_UNDEFINED from scapy.cbor.cborfields import CBORF_ANY from scapy.cborpacket import CBOR_Packet @@ -2980,9 +2978,10 @@ from scapy.cbor.cbor import ( CBORMapData, CBOR_Object, CBOR_TRUE, - CBORSimpleValue, - CBORTagValue, - CBOR_UNDEFINED_VALUE, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_TEXT_STRING, + CBOR_UNDEFINED, CBOR_UNSIGNED_INTEGER, ) @@ -2991,12 +2990,12 @@ 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 +assert "CBOR_SEMANTIC_TAG" in repr(CBOR_SEMANTIC_TAG((1, CBOR_TEXT_STRING("x")))) +assert "CBOR_SIMPLE_VALUE" in repr(CBOR_SIMPLE_VALUE(41)) +assert isinstance(CBOR_UNDEFINED(), CBOR_UNDEFINED) +assert not CBOR_UNDEFINED() +assert copy.copy(CBOR_UNDEFINED()) is CBOR_UNDEFINED() +assert copy.deepcopy(CBOR_UNDEFINED()) is CBOR_UNDEFINED() bad = bytes.fromhex("ff") err = CBOR_DECODING_ERROR(bad, exc=ValueError("boom")) assert "boom" in repr(err) @@ -3127,11 +3126,11 @@ assert md[-0.0].val == 1 assert md[0.0] is md[-0.0] = Deterministic encoding rebuilds floats from the semantic value -from scapy.cbor.cbor import CBORFloatValue +from scapy.cbor.cbor import CBOR_FLOAT from scapy.cbor.cborcodec import CBORcodec_Object # 1.5 received as binary64 must still encode as preferred binary16 -value = CBORFloatValue(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +value = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) assert CBORcodec_Object.encode_cbor_item_deterministic(value) == bytes.fromhex("f93e00") = Compound CBOR_Object values are unhashable diff --git a/test/scapy/layers/cbor_cbor2_interop.uts b/test/scapy/layers/cbor_cbor2_interop.uts index 226d73a5082..5be5741febe 100644 --- a/test/scapy/layers/cbor_cbor2_interop.uts +++ b/test/scapy/layers/cbor_cbor2_interop.uts @@ -44,10 +44,7 @@ from scapy.cbor.cbor import ( CBOR_TEXT_STRING, CBOR_TRUE, CBOR_UNDEFINED, - CBOR_UNDEFINED_VALUE, CBOR_UNSIGNED_INTEGER, - CBORSimpleValue, - CBORTagValue, ) from scapy.cbor.cborcodec import ( CBOR_Codec_Decoding_Error, @@ -157,12 +154,12 @@ def rr_norm_cbor2(value): def rr_norm_native(value): - if value is CBOR_UNDEFINED_VALUE: + if isinstance(value, CBOR_UNDEFINED): 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, CBOR_SIMPLE_VALUE): + return ("simple", value.val) + if isinstance(value, CBOR_SEMANTIC_TAG): + return ("tag", value.val[0], rr_norm_native(value.val[1])) if isinstance(value, CBORMapData): return _rr_map(value.cbor_pairs(), rr_norm_native) if isinstance(value, bool): @@ -231,11 +228,13 @@ def rr_assert_extension_wire(value, **dump_kwargs): def rr_to_scapy_native(value): if value is cbor2.undefined: - return CBOR_UNDEFINED_VALUE + return CBOR_UNDEFINED() if isinstance(value, cbor2.CBORSimpleValue): - return CBORSimpleValue(value.value) + return CBOR_SIMPLE_VALUE(value.value) if isinstance(value, cbor2.CBORTag): - return CBORTagValue(value.tag, rr_to_scapy_native(value.value)) + return CBOR_SEMANTIC_TAG( + (value.tag, rr_to_scapy_native(value.value)) + ) if isinstance(value, Mapping): return { rr_to_scapy_native(key): rr_to_scapy_native(item) @@ -898,31 +897,30 @@ 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 += Positive and negative floating zero are rejected as equivalent map keys ~ external_cbor2 wire = ( b"\xa2" + cbor2.dumps(0.0) + cbor2.dumps("positive") + cbor2.dumps(-0.0) + cbor2.dumps("negative") ) +# cbor2 may accept the wire into a collapsed Python mapping. cbor2.loads(wire, immutable=True) -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 +try: + rr_scapy_decode(wire) + assert False, "+0.0 and -0.0 were accepted as distinct map keys" +except CBOR_Codec_Decoding_Error: + pass -= Distinct double-precision NaN payloads survive as separate map keys ~ external_cbor2 += Distinct double-precision NaN payloads are rejected as equivalent 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 may still expose both entries in an immutable mapping. 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 +try: + rr_scapy_decode(wire) + assert False, "distinct NaN payloads were accepted as distinct map keys" +except CBOR_Codec_Decoding_Error: + pass = CBOR integer 1 and Boolean true remain distinct map keys in Scapy ~ external_cbor2 wire = ( @@ -995,7 +993,7 @@ 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)] + CBOR_UNDEFINED(), CBOR_SEMANTIC_TAG((60000, CBOR_UNSIGNED_INTEGER(4)))] wire = b"".join(CBORcodec_Object.encode_cbor_item(value) for value in values) expected = [rr_norm_native(value) for value in values] @@ -1128,7 +1126,6 @@ assert bytes(pkt) == wire 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 @@ -1216,13 +1213,13 @@ for value in (1 << 100, -(1 << 100)): = 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) +pkt.value.val.append(CBOR_UNSIGNED_INTEGER(3)) assert cbor2.loads(bytes(pkt)) == [[1, 2, 3], 0] = In-place mutation of a nested CBORF_ANY list invalidates cached bytes ~ external_cbor2 wire = cbor2.dumps([[[1]], 0], canonical=True) pkt = RRCbor2AnyEnvelope(wire) -pkt.value[0].append(2) +pkt.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(2)) assert cbor2.loads(bytes(pkt)) == [[[1, 2]], 0] = CBORF_ANY map-value mutation invalidates cached bytes ~ external_cbor2 @@ -1230,14 +1227,9 @@ 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 isinstance(map_value, CBOR_MAP) +assert isinstance(map_value.val, CBORMapData) +map_value.val["x"].val.append(CBOR_UNSIGNED_INTEGER(2)) assert cbor2.loads(bytes(pkt)) == [{"x": [1, 2]}, 0] + Seeded randomized differential corpora From 26cfe50936509796f65eb52447e88f6728b271ce Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 08:26:27 +0200 Subject: [PATCH 11/48] cbor: compare NaN map keys by sign and significand RFC 8949 map-key equivalence for floats must not collapse every NaN. Identity now uses sign plus a width-normalized significand, preferring CBOR_FLOAT wire bytes when available, while +0.0 and -0.0 still match. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 95 +++++++++++++++++++++--- test/scapy/layers/cbor.uts | 56 ++++++++++++-- test/scapy/layers/cbor_cbor2_interop.uts | 13 ++-- 3 files changed, 140 insertions(+), 24 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index b37f3b80617..32144dc8136 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -748,14 +748,93 @@ def enc(self, codec=None): return super(CBOR_FLOAT, self).enc(codec) +def _cbor_float_key_identity_from_encoded(encoded): + # type: (bytes) -> Tuple[Any, ...] + """Map-key identity for a CBOR float encoding (AI 25/26/27).""" + wire = bytes(encoded) + if not wire: + raise ValueError("empty CBOR float encoding") + ai = wire[0] & 0x1f + if ai == 25: + if len(wire) < 3: + raise ValueError("truncated half float") + bits = struct.unpack(">H", wire[1:3])[0] + sign = (bits >> 15) & 0x1 + exponent = (bits >> 10) & 0x1f + fraction = bits & 0x3ff + if exponent == 31 and fraction: + # Zero-extend the 10-bit significand to binary64 width. + return ("nan", sign, fraction << 42) + if exponent == 0: + if fraction == 0: + float_val = -0.0 if sign else 0.0 + else: + float_val = ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) + elif exponent == 31: + float_val = float("-inf") if sign else float("inf") + else: + float_val = ( + ((-1) ** sign) * + (1.0 + fraction / 1024.0) * + (2 ** (exponent - 15)) + ) + return _cbor_float_key_identity(float_val) + if ai == 26: + if len(wire) < 5: + raise ValueError("truncated single float") + bits = struct.unpack(">I", wire[1:5])[0] + sign = (bits >> 31) & 0x1 + exponent = (bits >> 23) & 0xff + fraction = bits & 0x7fffff + if exponent == 0xff and fraction: + return ("nan", sign, fraction << 29) + float_val = struct.unpack(">f", struct.pack(">I", bits))[0] + return _cbor_float_key_identity(float_val) + if ai == 27: + if len(wire) < 9: + raise ValueError("truncated double float") + bits = struct.unpack(">Q", wire[1:9])[0] + sign = (bits >> 63) & 0x1 + exponent = (bits >> 52) & 0x7ff + fraction = bits & ((1 << 52) - 1) + if exponent == 0x7ff and fraction: + return ("nan", sign, fraction) + float_val = struct.unpack(">d", struct.pack(">Q", bits))[0] + return _cbor_float_key_identity(float_val) + raise ValueError("not a CBOR float encoding: ai=%d" % ai) + + +def _cbor_float_key_identity(value, encoded=None): + # type: (float, Optional[bytes]) -> Tuple[Any, ...] + """Return RFC 8949 floating-point map-key identity for *value*. + + Finite ``+0.0`` / ``-0.0`` collapse. NaNs compare by sign and + significand after zero-extension to a 52-bit binary64 significand. + When *encoded* is a CBOR float item, prefer that bit pattern so payload + and sign survive Python's NaN canonicalization. + """ + if encoded is not None: + return _cbor_float_key_identity_from_encoded(encoded) + fval = float(value) + if math.isnan(fval): + bits = struct.unpack(">Q", struct.pack(">d", fval))[0] + sign = (bits >> 63) & 0x1 + significand = bits & ((1 << 52) - 1) + return ("nan", sign, significand) + if fval == 0.0: + return ("finite", 0.0) + return ("finite", fval) + + def _cbor_key_norm(value): # type: (Any) -> Any """Return a hashable RFC 8949 map-key equivalence form for *value*. Integers and floats remain distinct groups. Floating ``+0.0`` and - ``-0.0`` collapse. All NaN payloads are equivalent. Arrays compare - order-sensitively; maps compare as unordered pairs of norms. Semantic - tags require the same tag number and an equivalent tagged value. + ``-0.0`` collapse. NaNs are equivalent only when sign and normalized + significand match across widths. Arrays compare order-sensitively; + maps compare as unordered pairs of norms. Semantic tags require the + same tag number and an equivalent tagged value. """ if isinstance(value, CBOR_Object): if isinstance(value, (CBOR_TRUE, CBOR_FALSE)): @@ -771,7 +850,9 @@ def _cbor_key_norm(value): if isinstance(value, CBOR_TEXT_STRING): return ("tstr", str(value.val)) if isinstance(value, CBOR_FLOAT): - return _cbor_key_norm(float(value.val)) + return _cbor_float_key_identity( + value.val, getattr(value, "_encoded", None) + ) if isinstance(value, CBOR_ARRAY): return ("array", tuple(_cbor_key_norm(v) for v in value.val)) if isinstance(value, CBOR_MAP): @@ -803,11 +884,7 @@ def _cbor_key_norm(value): if isinstance(value, int): return ("int", value) if isinstance(value, float): - if math.isnan(value): - return ("float", "nan") - if value == 0.0: - return ("float", 0.0) - return ("float", float(value)) + return _cbor_float_key_identity(value) if isinstance(value, bytes): return ("bstr", value) if isinstance(value, str): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 23b4d93808f..ca6891bb690 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3046,11 +3046,28 @@ assert not _cbor_key_equivalent( CBOR_SEMANTIC_TAG((1, CBOR_UNSIGNED_INTEGER(5))), CBOR_SEMANTIC_TAG((2, CBOR_UNSIGNED_INTEGER(5))), ) -nan_a = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")) -nan_b = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("fb7ff8000000000001")) -assert math.isnan(nan_a.val) and math.isnan(nan_b.val) -assert _cbor_key_equivalent(nan_a, nan_b) +# Case A: same quiet-NaN significand across half and binary64 widths +nan_half = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")) +nan_double_same = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000000000000") +) +assert math.isnan(nan_half.val) and math.isnan(nan_double_same.val) +assert _cbor_key_equivalent(nan_half, nan_double_same) +# Case B: different significand payloads are distinct keys +nan_payload = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000000000001") +) +assert not _cbor_key_equivalent(nan_half, nan_payload) +# Case C: opposite sign with the same payload +nan_neg = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f9fe00")) +assert not _cbor_key_equivalent(nan_half, nan_neg) +# Plain Python NaNs (best-effort binary64 identity) remain self-equivalent assert _cbor_key_equivalent(float("nan"), float("nan")) +# Finite float width differences with the same numeric value are equivalent +assert _cbor_key_equivalent( + CBOR_FLOAT(1.5, encoded=bytes.fromhex("f93e00")), + CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")), +) = Generic maps reject +0.0 and -0.0 as duplicate keys from scapy.cbor import CBOR_Codecs @@ -3101,18 +3118,41 @@ try: except CBOR_Codec_Decoding_Error: pass -= Generic maps reject distinct NaN encodings as duplicate keys += Generic maps keep distinct NaN payloads as separate keys from scapy.cbor import CBOR_Codecs -from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error +import math -# {NaN16: 1, NaN64-payload: 2} +# {NaN16: 1, NaN64-low-payload: 2} — different significands wire = bytes.fromhex("a2f97e0001fb7ff800000000000102") +obj, rem = CBOR_Codecs.CBOR.dec(wire) +assert rem == b"" +pairs = obj.val.cbor_pairs() +assert len(pairs) == 2 +assert all(math.isnan(key.val) for key, _ in pairs) +assert obj.enc() == wire + += Generic maps reject identical NaN keys as duplicates +from scapy.cbor import CBOR_Codecs +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error + +# {NaN16: 1, same NaN as binary64 extension: 2} +wire = bytes.fromhex("a2f97e0001fb7ff800000000000002") try: CBOR_Codecs.CBOR.dec(wire) - assert False, "distinct NaN encodings were accepted as distinct keys" + assert False, "equivalent NaN keys were accepted" except CBOR_Codec_Decoding_Error: pass += Generic maps keep opposite-sign NaNs with the same payload as distinct keys +from scapy.cbor import CBOR_Codecs + +# {+NaN16: 1, -NaN16: 2} — opposite signs are distinct keys +wire = bytes.fromhex("a2f97e0001f9fe0002") +obj, rem = CBOR_Codecs.CBOR.dec(wire) +assert rem == b"" +assert len(obj.val.cbor_pairs()) == 2 +assert obj.enc() == wire + = CBORMapData treats +0.0 and -0.0 as the same lookup key from scapy.cbor import CBOR_Codecs diff --git a/test/scapy/layers/cbor_cbor2_interop.uts b/test/scapy/layers/cbor_cbor2_interop.uts index 5be5741febe..a650c1359fe 100644 --- a/test/scapy/layers/cbor_cbor2_interop.uts +++ b/test/scapy/layers/cbor_cbor2_interop.uts @@ -910,17 +910,16 @@ try: except CBOR_Codec_Decoding_Error: pass -= Distinct double-precision NaN payloads are rejected as equivalent map keys ~ external_cbor2 += Distinct double-precision NaN payloads remain distinct map keys ~ external_cbor2 first_nan = b"\xfb\x7f\xf8\x00\x00\x00\x00\x00\x01" second_nan = b"\xfb\x7f\xf8\x00\x00\x00\x00\x00\x02" wire = b"\xa2" + first_nan + cbor2.dumps(1) + second_nan + cbor2.dumps(2) -# cbor2 may still expose both entries in an immutable mapping. cbor2.loads(wire, immutable=True) -try: - rr_scapy_decode(wire) - assert False, "distinct NaN payloads were accepted as distinct map keys" -except CBOR_Codec_Decoding_Error: - pass +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 = ( From a79cb3871b22bc32d814ae127403c0b8476d2573 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 08:28:44 +0200 Subject: [PATCH 12/48] cbor: preserve NaN sign and significand in deterministic encoding Share NaN component and preferred-width helpers between the deterministic validator and encoder so CBOR_FLOAT NaNs shorten without losing payload identity, including unknown-map rebuilds. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 104 +++++++++++++++++++++++++++++++++++-- test/scapy/layers/cbor.uts | 70 +++++++++++++++++++++++++ 2 files changed, 170 insertions(+), 4 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index ec54f01f76e..0edbe650213 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -329,6 +329,94 @@ def _cbor_nan_preferred_ai(ai, bits): return ai +def _cbor_nan_components(ai, bits): + # type: (int, int) -> Optional[Tuple[int, int]] + """Return ``(sign, significand52)`` for a NaN pattern, else ``None``. + + The significand is zero-extended to a binary64-width 52-bit field so + half / single / double representations of the same NaN share identity. + """ + if ai == 25: + sign = (int(bits) >> 15) & 0x1 + exponent = (int(bits) >> 10) & 0x1f + fraction = int(bits) & 0x3ff + if exponent != 31 or not fraction: + return None + return sign, fraction << 42 + if ai == 26: + sign = (int(bits) >> 31) & 0x1 + exponent = (int(bits) >> 23) & 0xff + fraction = int(bits) & 0x7fffff + if exponent != 0xff or not fraction: + return None + return sign, fraction << 29 + if ai == 27: + sign = (int(bits) >> 63) & 0x1 + exponent = (int(bits) >> 52) & 0x7ff + fraction = int(bits) & ((1 << 52) - 1) + if exponent != 0x7ff or not fraction: + return None + return sign, fraction + return None + + +def _cbor_encode_nan(sign, significand52, ai): + # type: (int, int, int) -> bytes + """Encode a NaN at float AI *ai* preserving *sign* and *significand52*.""" + if ai == 25: + fraction = (significand52 >> 42) & 0x3ff + bits = (sign << 15) | (0x1f << 10) | fraction + return chb(0xf9) + struct.pack(">H", bits) + if ai == 26: + fraction = (significand52 >> 29) & 0x7fffff + bits = (sign << 31) | (0xff << 23) | fraction + return chb(0xfa) + struct.pack(">I", bits) + if ai == 27: + bits = ( + (sign << 63) | + (0x7ff << 52) | + (significand52 & ((1 << 52) - 1)) + ) + return chb(0xfb) + struct.pack(">Q", bits) + raise CBOR_Codec_Encoding_Error("Invalid NaN float AI: %d" % ai) + + +def _cbor_float_bits_from_encoded(encoded): + # type: (bytes) -> Tuple[int, int] + """Return ``(ai, bits)`` for a definite CBOR float item.""" + wire = bytes(encoded) + if not wire: + raise CBOR_Codec_Encoding_Error("empty CBOR float encoding") + ai = wire[0] & 0x1f + if ai == 25: + if len(wire) < 3: + raise CBOR_Codec_Encoding_Error("truncated half float") + return ai, struct.unpack(">H", wire[1:3])[0] + if ai == 26: + if len(wire) < 5: + raise CBOR_Codec_Encoding_Error("truncated single float") + return ai, struct.unpack(">I", wire[1:5])[0] + if ai == 27: + if len(wire) < 9: + raise CBOR_Codec_Encoding_Error("truncated double float") + return ai, struct.unpack(">Q", wire[1:9])[0] + raise CBOR_Codec_Encoding_Error("not a CBOR float encoding: ai=%d" % ai) + + +def _cbor_preferred_nan_encoding(encoded): + # type: (bytes) -> bytes + """Shortest CBOR float encoding preserving NaN sign and significand.""" + ai, bits = _cbor_float_bits_from_encoded(encoded) + comps = _cbor_nan_components(ai, bits) + if comps is None: + raise CBOR_Codec_Encoding_Error( + "encoded float is not a NaN: %r" % (bytes(encoded),) + ) + sign, significand52 = comps + preferred = _cbor_nan_preferred_ai(ai, bits) + return _cbor_encode_nan(sign, significand52, preferred) + + def _cbor_preferred_float_ai(value): # type: (float) -> int """Return the preferred float AI (25/26/27) for a numeric *value*.""" @@ -351,11 +439,10 @@ def _cbor_preferred_float_ai(value): 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): + comps = _cbor_nan_components(ai, bits) + if comps is not None: return _cbor_nan_preferred_ai(ai, bits) - return _cbor_preferred_float_ai(float_val) + return _cbor_preferred_float_ai(_cbor_float_from_bits(ai, bits)) def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): @@ -1235,9 +1322,11 @@ def _encode_cbor_item_deterministic(item): :class:`~scapy.cbor.cbor.CBOR_Object` instances are accepted and reduced to native values (preferred float encoding, deterministic nested maps). """ + import math from scapy.cbor.cbor import ( CBOR_Object, CBOR_ARRAY, + CBOR_FLOAT, CBOR_MAP, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE, @@ -1248,6 +1337,12 @@ def _encode_cbor_item_deterministic(item): if isinstance(item, CBOR_Object): if isinstance(item, CBOR_UNDEFINED): return CBOR_UNDEFINED().enc() + if isinstance(item, CBOR_FLOAT): + encoded = getattr(item, "_encoded", None) + if encoded is not None and math.isnan(float(item.val)): + return _cbor_preferred_nan_encoding(encoded) + # Finite floats ignore original width; rebuild preferred form. + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item.val)) if isinstance(item, CBOR_ARRAY): return _encode_cbor_item_deterministic(list(item.val)) if isinstance(item, CBOR_MAP): @@ -1287,6 +1382,7 @@ def _encode_cbor_item_deterministic(item): if isinstance(item, float): # Deterministic encoding always rebuilds from the semantic float # value (shortest exact representation). Never reuse source wire. + # Plain NaNs without retained CBOR bytes use quiet binary16. return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item)) if item is None: return CBORcodec_SIMPLE_AND_FLOAT.enc(None) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index ca6891bb690..967acb77986 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3173,6 +3173,76 @@ from scapy.cbor.cborcodec import CBORcodec_Object value = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) assert CBORcodec_Object.encode_cbor_item_deterministic(value) == bytes.fromhex("f93e00") += Deterministic NaN encoding preserves sign and preferred width +from scapy.cbor.cbor import CBOR_FLOAT, _cbor_key_equivalent +from scapy.cbor.cborcodec import ( + CBORcodec_Object, + cbor_find_non_deterministic, +) + +enc = CBORcodec_Object.encode_cbor_item_deterministic + +# Already-shortest quiet half NaN stays half +half = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")) +assert enc(half) == bytes.fromhex("f97e00") +assert cbor_find_non_deterministic(enc(half)) == [] + +# binary64 quiet NaN with only top significand bits shortens to half +wide_half = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("fb7ff8000000000000")) +assert enc(wide_half) == bytes.fromhex("f97e00") +assert _cbor_key_equivalent(half, wide_half) +assert cbor_find_non_deterministic(enc(wide_half)) == [] + +# binary64 NaN using top 23 significand bits shortens to float32, not half +# significand bits 51..29 set as 0x40000001 << 29? Use mant with bit 29 set: +# 0x8000000000000 | 0x20000000 = top quiet bit + lowest single-preserved bit +to_single = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000020000000") +) +assert enc(to_single) == bytes.fromhex("fa7fc00001") +assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000020000000")) +assert cbor_find_non_deterministic(enc(to_single)) == [] + +# binary64 NaN with a low significand bit cannot shorten +irreducible = CBOR_FLOAT( + float("nan"), encoded=bytes.fromhex("fb7ff8000000000001") +) +assert enc(irreducible) == bytes.fromhex("fb7ff8000000000001") +assert cbor_find_non_deterministic(enc(irreducible)) == [] + +# Negative quiet half NaN keeps its sign under deterministic encoding +neg = CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("fbfff8000000000000")) +assert enc(neg) == bytes.fromhex("f9fe00") +assert cbor_find_non_deterministic(enc(neg)) == [] +assert not _cbor_key_equivalent(half, neg) + += Unknown map NaN values keep payload through deterministic rebuild +from scapy.cbor.cbor import CBOR_FLOAT, _cbor_key_equivalent +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class MapWithUnknown(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_pairs", + ) + +# {"a": 1, "n": } +wire = bytes.fromhex("a2616101616efb7ff8000000000000") +pkt = MapWithUnknown(wire) +assert pkt.a == 1 +assert len(pkt.unknown_pairs) == 1 +assert isinstance(pkt.unknown_pairs[0][1], CBOR_FLOAT) +pkt.a = 2 # invalidate raw cache; unknown members rebuild deterministically +rebuilt = bytes(pkt) +assert b"\x61a\x02" in rebuilt +# preferred half NaN with the same identity +assert bytes.fromhex("f97e00") in rebuilt +assert _cbor_key_equivalent( + pkt.unknown_pairs[0][1], + CBOR_FLOAT(float("nan"), encoded=bytes.fromhex("f97e00")), +) + = Compound CBOR_Object values are unhashable from scapy.cbor.cbor import CBOR_ARRAY, CBOR_UNSIGNED_INTEGER From f9e610436fad5abcf9a41aedec8afd99fdcf6f0c Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 08:29:54 +0200 Subject: [PATCH 13/48] cbor: drop dead map key-identity helpers and stale PR noise Remove unused CBORMapData identity helpers, document equivalence-based lookup, delete the ConditionalField getsource test, and restore the unrelated README packaging badge. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- README.md | 3 +- scapy/cbor/cbor.py | 70 ++++++---------------------------------------- test/fields.uts | 12 -------- 3 files changed, 11 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 83c347e08d9..559b8ee5596 100644 --- a/README.md +++ b/README.md @@ -92,7 +92,8 @@ follow the instructions to install them. ## Packaging status -[![Packaging status](https://repology.org/badge/vertical-allrepos/scapy.svg?columns=4&exclude_unsupported=1)](https://repology.org/project/scapy/versions) +[![Packaging status](https://repology.org/badge/vertical-allrepos/scapy.svg?columns=4&exclude_unsupported=1&header= +)](https://repology.org/project/scapy/versions) ## License diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 32144dc8136..ceec13ece99 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -416,13 +416,15 @@ def strshow(self, lvl=0): 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. + Storage preserves ordered ``(key, value)`` pairs so ``enc()`` can emit a + faithful CBOR map. Lookup (``__getitem__`` / ``__contains__``) uses + RFC 8949 map-key equivalence via :func:`_cbor_key_equivalent`, so values + that compare equal under Python ``==`` but differ as CBOR items (``1`` vs + ``True``, distinct NaN payloads, etc.) stay distinct. + + Arbitrary CBOR maps cannot always be represented as Python ``dict`` + objects; :meth:`as_dict` raises when equivalence or Python key collision + would lose distinctions. """ __slots__ = ("_pairs",) @@ -483,60 +485,6 @@ 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_Object keys 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): - return ("undef", None) - 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. - if isinstance(key, bool): - return (bool, key) - if isinstance(key, float): - return CBORMapData._float_key_identity(key) - if isinstance(key, int): - return (int, key) - return (type(key), key) - def keys(self): # type: () -> List[Any] out = [] # type: List[Any] diff --git a/test/fields.uts b/test/fields.uts index dbef36fb59d..e2d1132d414 100644 --- a/test/fields.uts +++ b/test/fields.uts @@ -2357,15 +2357,3 @@ 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 From 26b452986e7a6441a92372480095a57049673943 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:06:57 +0200 Subject: [PATCH 14/48] cbor: invalidate CBOR_FLOAT wire cache when val is assigned Decoded floats keep _encoded until mutated. Clearing it on val assignment prevents rebuilds from emitting stale float bytes. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 8 ++++++ test/scapy/layers/cbor.uts | 51 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index ceec13ece99..f3c7b1ad80d 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -689,6 +689,14 @@ def __init__(self, val, encoded=None): # Exact received float encoding when known; preferred width when None. self._encoded = encoded + def __setattr__(self, name, value): + # type: (str, Any) -> None + # After construction, assigning val invalidates the wire cache even + # when the new semantic value compares equal to the old one. + if name == "val" and hasattr(self, "_encoded"): + object.__setattr__(self, "_encoded", None) + super(CBOR_FLOAT, self).__setattr__(name, value) + def enc(self, codec=None): # type: (Any) -> bytes if self._encoded is not None: diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 967acb77986..f05e2a3a5e1 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2936,6 +2936,57 @@ pkt.raw_packet_cache_fields = None # CBOR_FLOAT.enc() still preserves _encoded when present on the object. assert bytes(pkt) == wire += Assigning CBOR_FLOAT.val invalidates the retained wire encoding +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborcodec import CBORcodec_Object + +obj = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +assert obj.enc() == bytes.fromhex("fb3ff8000000000000") +obj.val = 2.0 +decoded, rem = CBORcodec_Object.dec(obj.enc()) +assert rem == b"" +assert decoded.val == 2.0 +# Explicit assignment clears the cache even when the value is unchanged. +obj2 = CBOR_FLOAT(1.5, encoded=bytes.fromhex("fb3ff8000000000000")) +obj2.val = 1.5 +assert obj2._encoded is None +assert obj2.enc() == bytes.fromhex("f93e00") + += CBORF_ANY float mutation rebuilds from the new semantic value +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet + +class AnyFloatMutPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +wire = bytes.fromhex("fb3ff8000000000000") # 1.5 as binary64 +pkt = AnyFloatMutPkt(wire) +assert bytes(pkt) == wire +assert isinstance(pkt.value, CBOR_FLOAT) +pkt.value.val = 2.0 +rebuilt = bytes(pkt) +parsed = AnyFloatMutPkt(rebuilt) +assert parsed.value.val == 2.0 + += CBORF_ANY NaN mutation does not reuse the original encoded payload +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet +import math + +class AnyNanMutPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +wire = bytes.fromhex("fb7ff8000000000001") +pkt = AnyNanMutPkt(wire) +assert isinstance(pkt.value, CBOR_FLOAT) +assert bytes(pkt) == wire +pkt.value.val = float("nan") +rebuilt = bytes(pkt) +assert rebuilt != wire +assert math.isnan(AnyNanMutPkt(rebuilt).value.val) + = RandCBORObject generates encodable objects including nested containers import random from scapy.cbor.cbor import ( From ab1e0f62132c4054667b4952195baf9cf4ebebad Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:08:14 +0200 Subject: [PATCH 15/48] cbor: reject nonterminal SEQUENCE_OF hidden by optional/conditional Unwrap transparent wrappers before the terminal-sequence schema check so optional/conditional nesting cannot hide an ambiguous unbounded SEQUENCE_OF. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 29 +++++----- test/scapy/layers/cbor.uts | 108 +++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+), 12 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 938bc0310c7..c7c99fc947f 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1312,6 +1312,18 @@ def max_items(self, pkt): return sum(f.max_items(pkt) for f in self.seq) +def _unwrap_transparent_cbor_wrapper(field): + # type: (Any) -> Any + """Unwrap optional/conditional wrappers that add no CBOR framing.""" + while True: + if isinstance(field, CBORF_optional): + field = field._field + elif isinstance(field, CBORF_CONDITIONAL): + field = field.fld + else: + return field + + class CBORF_ARRAY(_CBORF_compound): """ CBOR array with a fixed sequence of named, typed fields (major type 4). @@ -1341,19 +1353,12 @@ def __init__(self, *seq, **kwargs): def _reject_ambiguous_unbounded_sequences(self): # type: () -> None - def _is_unbounded_sequence_of(field): - # type: (Any) -> bool - if isinstance(field, CBORF_optional): - return False - if isinstance(field, CBORF_CONDITIONAL): - return False - return ( - isinstance(field, CBORF_SEQUENCE_OF) - and getattr(field, "is_unbounded", False) - ) - for index, field in enumerate(self.seq): - if not _is_unbounded_sequence_of(field): + inner = _unwrap_transparent_cbor_wrapper(field) + if not ( + isinstance(inner, CBORF_SEQUENCE_OF) + and getattr(inner, "is_unbounded", False) + ): continue # Unbounded SEQUENCE_OF must be the final schema field. if index != len(self.seq) - 1: diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index f05e2a3a5e1..8e02b12724a 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -902,6 +902,114 @@ try: except ValueError: pass += Optional wrapper cannot hide a nonterminal unbounded SEQUENCE_OF +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) + +try: + CBORF_ARRAY( + CBORF_optional( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + assert False, "optional unbounded SEQUENCE_OF before tail was accepted" +except ValueError: + pass + += Conditional wrapper cannot hide a nonterminal unbounded SEQUENCE_OF +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_CONDITIONAL, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) + +try: + CBORF_ARRAY( + CBORF_CONDITIONAL( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER), + lambda pkt: True, + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + assert False, "conditional unbounded SEQUENCE_OF before tail was accepted" +except ValueError: + pass + += Nested transparent wrappers cannot hide a nonterminal unbounded SEQUENCE_OF +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_CONDITIONAL, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) + +try: + CBORF_ARRAY( + CBORF_optional( + CBORF_CONDITIONAL( + CBORF_optional( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) + ), + lambda pkt: True, + ) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), + ) + assert False, "nested wrapped unbounded SEQUENCE_OF before tail was accepted" +except ValueError: + pass + += Terminal optional unbounded SEQUENCE_OF remains a valid schema +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) +from scapy.cborpacket import CBOR_Packet + +class TerminalOptSeq(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_optional( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) + ), + ) + +pkt = TerminalOptSeq(b"\x83\x01\x0a\x0b") +assert pkt.n == 1 +assert pkt.vals == [10, 11] + += Wrapped SEQUENCE_OF with count_from may precede a required field +from scapy.cbor.cborfields import ( + CBORF_ARRAY, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, + CBORF_optional, +) + +# Schema construction must succeed; runtime presence of the optional +# sequence is not part of this framing check. +CBORF_ARRAY( + CBORF_UNSIGNED_INTEGER("n", 0), + CBORF_optional( + CBORF_SEQUENCE_OF( + "vals", + [], + CBORF_UNSIGNED_INTEGER, + count_from=lambda pkt: pkt.n, + ) + ), + CBORF_UNSIGNED_INTEGER("tail", 0), +) + = SEQUENCE_OF with count_from consumes an explicit number of items from scapy.cbor.cborfields import ( CBORF_ARRAY, From d06fe04dcb0da0e7d2f67dfd8b22c9a9f68b8805 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:10:33 +0200 Subject: [PATCH 16/48] cbor: speed map-key checks and reject collection pkt instances Use set membership on normalized map keys for duplicate detection and as_dict collision tracking, and require packet classes (not instances) for SEQUENCE_OF / ARRAY_OF pkt_cls. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 5 +-- scapy/cbor/cborcodec.py | 17 +++++----- scapy/cbor/cborfields.py | 11 ++++--- test/scapy/layers/cbor.uts | 67 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 85 insertions(+), 15 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index f3c7b1ad80d..2b08b147912 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -17,6 +17,7 @@ Generic, List, Optional, + Set, Tuple, Type, TypeVar, @@ -447,7 +448,7 @@ def as_dict(self): # type: () -> Dict[Any, Any] """Convert to a Python dict, raising if CBOR key distinctions would be lost.""" out = {} # type: Dict[Any, Any] - used_norms = [] # type: List[Any] + used_norms = set() # type: Set[Any] for key, value in self._pairs: norm = _cbor_key_norm(key) if norm in used_norms: @@ -461,7 +462,7 @@ def as_dict(self): raise ValueError( "Converting CBOR map to dict would collapse distinct keys" ) - used_norms.append(norm) + used_norms.add(norm) out[py_key] = value return out diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 0edbe650213..7f1377b8ffd 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -14,6 +14,7 @@ Generic, List, Optional, + Set, Tuple, Type, TypeVar, @@ -1003,17 +1004,17 @@ def do_dec(cls, remaining=s) pairs = [] # type: List[Tuple[Any, Any]] - seen_keys = [] # type: List[Any] + seen_norms = set() # type: Set[Any] def _add_pair(key, value): # type: (Any, Any) -> None - from scapy.cbor.cbor import _cbor_key_equivalent - for prev in seen_keys: - if _cbor_key_equivalent(prev, key): - raise CBOR_Codec_Decoding_Error( - "Duplicate CBOR map key: %r" % (key,), - remaining=s) - seen_keys.append(key) + from scapy.cbor.cbor import _cbor_key_norm + norm = _cbor_key_norm(key) + if norm in seen_norms: + raise CBOR_Codec_Decoding_Error( + "Duplicate CBOR map key: %r" % (key,), + remaining=s) + seen_norms.add(norm) pairs.append((key, value)) if length is CBOR_INDEFINITE: diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index c7c99fc947f..87182ef7db0 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1417,7 +1417,8 @@ class CBORF_ARRAY_INDEFINITE(CBORF_ARRAY): _ARRAY_T = Union[ - 'CBOR_Packet', + Type['CBOR_Packet'], + Type[Packet], Type['CBORF_field[Any]'], 'CBORF_PACKET', 'CBORF_field[Any]', @@ -1478,8 +1479,8 @@ def __init__(self, else: self.item_field = chosen self.holds_packets = 0 - elif hasattr(chosen, "CBOR_root") or ( - isinstance(chosen, type) and issubclass(chosen, Packet) + elif isinstance(chosen, type) and ( + hasattr(chosen, "CBOR_root") or issubclass(chosen, Packet) ): self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 @@ -1668,8 +1669,8 @@ def __init__(self, else: self.item_field = chosen self.holds_packets = 0 - elif hasattr(chosen, "CBOR_root") or ( - isinstance(chosen, type) and issubclass(chosen, Packet) + elif isinstance(chosen, type) and ( + hasattr(chosen, "CBOR_root") or issubclass(chosen, Packet) ): self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 8e02b12724a..ed4e7ec3275 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2612,6 +2612,36 @@ except ValueError: else: raise AssertionError("conflicting SEQUENCE_OF selectors accepted") += CBORF_SEQUENCE_OF rejects a packet instance as pkt_cls +from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class SeqOfChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + +CBORF_SEQUENCE_OF("ok", [], pkt_cls=SeqOfChild) +try: + CBORF_SEQUENCE_OF("bad", [], pkt_cls=SeqOfChild()) +except ValueError: + pass +else: + raise AssertionError("SEQUENCE_OF accepted a packet instance as pkt_cls") + += CBORF_ARRAY_OF rejects a packet instance as pkt_cls +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class ArrayOfChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("value", 0) + +CBORF_ARRAY_OF("ok", [], pkt_cls=ArrayOfChild) +try: + CBORF_ARRAY_OF("bad", [], pkt_cls=ArrayOfChild()) +except ValueError: + pass +else: + raise AssertionError("ARRAY_OF accepted a packet instance as pkt_cls") + + TypeError must not be used for callback signature probing @@ -3324,6 +3354,43 @@ assert md[0.0].val == 1 assert md[-0.0].val == 1 assert md[0.0] is md[-0.0] += CBORMapData.as_dict converts distinct keys and rejects collisions +from scapy.cbor.cbor import ( + CBOR_FLOAT, + CBOR_TEXT_STRING, + CBOR_TRUE, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) + +md = CBORMapData([ + (CBOR_TEXT_STRING("a"), CBOR_UNSIGNED_INTEGER(10)), + (CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(20)), +]) +as_dict = md.as_dict() +assert as_dict["a"].val == 10 +assert as_dict[1].val == 20 + +# RFC-equivalent keys (+0.0 / -0.0) cannot become a Python dict. +try: + CBORMapData([ + (CBOR_FLOAT(0.0), 1), + (CBOR_FLOAT(-0.0), 2), + ]).as_dict() + assert False, "equivalent float keys were collapsed into a dict" +except ValueError: + pass + +# Python dict key collision (True vs 1) is rejected even when CBOR norms differ. +try: + CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "i"), + (CBOR_TRUE(), "b"), + ]).as_dict() + assert False, "True/1 Python key collision was accepted by as_dict" +except ValueError: + pass + = Deterministic encoding rebuilds floats from the semantic value from scapy.cbor.cbor import CBOR_FLOAT from scapy.cbor.cborcodec import CBORcodec_Object From 0e0f49067de2524f644cdd277de8ff48232e4730 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:29:27 +0200 Subject: [PATCH 17/48] cbor: track CBOR_ANY float wire-cache mutation Fingerprint CBORF_ANY values including CBOR_FLOAT._encoded so same-value .val assignment invalidates the packet raw cache without changing semantic CBOR_Object equality. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 85 ++++++++++++++++++++++++++++++++++++++ scapy/cborpacket.py | 11 +++++ test/scapy/layers/cbor.uts | 18 ++++++++ 3 files changed, 114 insertions(+) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 87182ef7db0..7017662daf1 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -14,6 +14,7 @@ """ import copy +import math from dataclasses import dataclass @@ -557,6 +558,16 @@ def do_copy(self, x): # type: ignore[override] return x return copy.deepcopy(x) + def cache_fingerprint(self, x): + # type: (Any) -> Any + """Snapshot for Scapy mutable raw-cache comparison. + + Includes ``CBOR_FLOAT._encoded`` so explicit ``.val`` assignment that + clears the wire cache is visible even when the semantic float is + unchanged. + """ + return _cbor_any_cache_fingerprint(x) + def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> Any if x is CBOR_ABSENT or x is CBOR_NO_ITEM: @@ -583,6 +594,80 @@ def encode_value(self, x): return CBORcodec_Object.encode_cbor_item(x) +def _cbor_any_cache_fingerprint(obj): + # type: (Any) -> Any + """Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` values.""" + from scapy.cbor.cbor import CBORMapData + if obj is CBOR_ABSENT or obj is CBOR_NO_ITEM: + return ("sentinel", obj) + if isinstance(obj, CBOR_UNDEFINED): + return ("undefined",) + if isinstance(obj, CBOR_FLOAT): + fval = float(obj.val) + if math.isnan(fval): + token = ("nan",) # type: Any + elif math.isinf(fval): + token = ("inf", math.copysign(1.0, fval)) + elif fval == 0.0: + token = ("zero", math.copysign(1.0, fval)) + else: + token = ("num", fval) + encoded = getattr(obj, "_encoded", None) + return ("float", token, encoded) + if isinstance(obj, CBOR_ARRAY): + return ( + "array", + tuple(_cbor_any_cache_fingerprint(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, dict): + pairs = list(obj.val.items()) + else: + pairs = list(obj.val) + return ( + "map", + tuple( + ( + _cbor_any_cache_fingerprint(key), + _cbor_any_cache_fingerprint(value), + ) + for key, value in pairs + ), + ) + if isinstance(obj, CBORMapData): + return ( + "mapdata", + tuple( + ( + _cbor_any_cache_fingerprint(key), + _cbor_any_cache_fingerprint(value), + ) + for key, value in obj.cbor_pairs() + ), + ) + if isinstance(obj, CBOR_SEMANTIC_TAG): + tag_num, inner = obj.val + return ("tag", int(tag_num), _cbor_any_cache_fingerprint(inner)) + if isinstance(obj, CBOR_Object): + return (type(obj).__name__, obj.val) + if isinstance(obj, list): + return ("list", tuple(_cbor_any_cache_fingerprint(item) for item in obj)) + if isinstance(obj, dict): + return ( + "dict", + tuple( + ( + _cbor_any_cache_fingerprint(key), + _cbor_any_cache_fingerprint(value), + ) + for key, value in obj.items() + ), + ) + return ("py", type(obj).__name__, obj) + + ############################# # Simple CBOR Fields # ############################# diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 99717c35051..439e33d7e95 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -156,6 +156,17 @@ def getfieldval(self, attr): return self.fields[attr] return super(CBOR_Packet, self).getfieldval(attr) + def _raw_packet_cache_field_value(self, fld, val, copy=False): + # type: (Any, Any, bool) -> Optional[Any] + # Field-local fingerprints (e.g. CBORF_ANY) include wire-cache state + # that semantic CBOR_Object equality ignores. + cache_fingerprint = getattr(fld, "cache_fingerprint", None) + if cache_fingerprint is not None: + return cache_fingerprint(val) + return super(CBOR_Packet, self)._raw_packet_cache_field_value( + fld, val, copy + ) + def self_build(self): # type: () -> bytes if _cbor_raw_cache_is_valid(self): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index ed4e7ec3275..ec49095a366 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3107,6 +3107,24 @@ rebuilt = bytes(pkt) parsed = AnyFloatMutPkt(rebuilt) assert parsed.value.val == 2.0 += CBORF_ANY same-value float mutation invalidates the packet raw cache +from scapy.cbor.cbor import CBOR_FLOAT +from scapy.cbor.cborfields import CBORF_ANY +from scapy.cborpacket import CBOR_Packet + +class AnyFloatSameMutPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +wire = bytes.fromhex("fb3ff8000000000000") # 1.5 as binary64 +pkt = AnyFloatSameMutPkt(wire) +assert bytes(pkt) == wire +assert isinstance(pkt.value, CBOR_FLOAT) +pkt.value.val = 1.5 +rebuilt = bytes(pkt) +assert rebuilt != wire +assert rebuilt == bytes.fromhex("f93e00") +assert AnyFloatSameMutPkt(rebuilt).value.val == 1.5 + = CBORF_ANY NaN mutation does not reuse the original encoded payload from scapy.cbor.cbor import CBOR_FLOAT from scapy.cbor.cborfields import CBORF_ANY From d6c569405d21b6a05fb400005b22d3953f1c059b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:30:08 +0200 Subject: [PATCH 18/48] cbor: require Packet subclasses with CBOR_root for collections Reject non-Packet classes that only define CBOR_root and plain Packet types without CBOR_root in SEQUENCE_OF / ARRAY_OF pkt_cls validation. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 13 ++++++----- test/scapy/layers/cbor.uts | 46 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 54 insertions(+), 5 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 7017662daf1..e8d8b9f0f85 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1502,7 +1502,6 @@ class CBORF_ARRAY_INDEFINITE(CBORF_ARRAY): _ARRAY_T = Union[ - Type['CBOR_Packet'], Type[Packet], Type['CBORF_field[Any]'], 'CBORF_PACKET', @@ -1564,8 +1563,10 @@ def __init__(self, else: self.item_field = chosen self.holds_packets = 0 - elif isinstance(chosen, type) and ( - hasattr(chosen, "CBOR_root") or issubclass(chosen, Packet) + elif ( + isinstance(chosen, type) + and issubclass(chosen, Packet) + and hasattr(chosen, "CBOR_root") ): self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 @@ -1754,8 +1755,10 @@ def __init__(self, else: self.item_field = chosen self.holds_packets = 0 - elif isinstance(chosen, type) and ( - hasattr(chosen, "CBOR_root") or issubclass(chosen, Packet) + elif ( + isinstance(chosen, type) + and issubclass(chosen, Packet) + and hasattr(chosen, "CBOR_root") ): self.cls = cast("Type[CBOR_Packet]", chosen) self.holds_packets = 1 diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index ec49095a366..cb60f6992fe 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2642,6 +2642,52 @@ except ValueError: else: raise AssertionError("ARRAY_OF accepted a packet instance as pkt_cls") += CBORF_SEQUENCE_OF and ARRAY_OF reject fake CBOR_root classes +from scapy.cbor.cborfields import ( + CBORF_ARRAY_OF, + CBORF_SEQUENCE_OF, + CBORF_UNSIGNED_INTEGER, +) + +class FakeRoot(object): + CBOR_root = CBORF_UNSIGNED_INTEGER("x", 0) + +try: + CBORF_SEQUENCE_OF("items", [], pkt_cls=FakeRoot) +except ValueError: + pass +else: + raise AssertionError("SEQUENCE_OF accepted a non-Packet CBOR_root class") + +try: + CBORF_ARRAY_OF("items", [], pkt_cls=FakeRoot) +except ValueError: + pass +else: + raise AssertionError("ARRAY_OF accepted a non-Packet CBOR_root class") + += CBORF_SEQUENCE_OF and ARRAY_OF reject plain Packet without CBOR_root +from scapy.packet import Packet +from scapy.fields import ByteField +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_SEQUENCE_OF + +class PlainPacket(Packet): + fields_desc = [ByteField("x", 0)] + +try: + CBORF_SEQUENCE_OF("items", [], pkt_cls=PlainPacket) +except ValueError: + pass +else: + raise AssertionError("SEQUENCE_OF accepted Packet without CBOR_root") + +try: + CBORF_ARRAY_OF("items", [], pkt_cls=PlainPacket) +except ValueError: + pass +else: + raise AssertionError("ARRAY_OF accepted Packet without CBOR_root") + + TypeError must not be used for callback signature probing From 7cbcbbbb7b7d3f80dc9baf4180cadfad2346c88e Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:31:20 +0200 Subject: [PATCH 19/48] cbor: raise ValueError for unhashable as_dict keys Detect unhashable Python key conversions from valid CBOR map keys (arrays/maps) instead of leaking a raw TypeError from dict insertion. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 8 +++++++- test/scapy/layers/cbor.uts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 2b08b147912..1029198ce86 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -456,8 +456,14 @@ def as_dict(self): "CBOR map keys are equivalent under RFC 8949; " "cannot convert to dict without losing distinctions" ) - # Also reject Python-dict collisions (True vs 1, etc.). py_key = key.val if isinstance(key, CBOR_Object) else key + try: + hash(py_key) + except TypeError: + raise ValueError( + "CBOR map key cannot be represented as a Python dict key" + ) + # Also reject Python-dict collisions (True vs 1, etc.). if py_key in out: raise ValueError( "Converting CBOR map to dict would collapse distinct keys" diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index cb60f6992fe..6da26cf7b16 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3420,6 +3420,8 @@ assert md[0.0] is md[-0.0] = CBORMapData.as_dict converts distinct keys and rejects collisions from scapy.cbor.cbor import ( + CBOR_BYTE_STRING, + CBOR_FALSE, CBOR_FLOAT, CBOR_TEXT_STRING, CBOR_TRUE, @@ -3430,10 +3432,14 @@ from scapy.cbor.cbor import ( md = CBORMapData([ (CBOR_TEXT_STRING("a"), CBOR_UNSIGNED_INTEGER(10)), (CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(20)), + (CBOR_BYTE_STRING(b"b"), CBOR_UNSIGNED_INTEGER(30)), + (CBOR_FALSE(), CBOR_UNSIGNED_INTEGER(40)), ]) as_dict = md.as_dict() assert as_dict["a"].val == 10 assert as_dict[1].val == 20 +assert as_dict[b"b"].val == 30 +assert as_dict[False].val == 40 # RFC-equivalent keys (+0.0 / -0.0) cannot become a Python dict. try: @@ -3455,6 +3461,33 @@ try: except ValueError: pass += CBORMapData.as_dict rejects unhashable array and map keys +from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_MAP, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) + +try: + CBORMapData([ + (CBOR_ARRAY([CBOR_UNSIGNED_INTEGER(1)]), CBOR_UNSIGNED_INTEGER(0)), + ]).as_dict() + assert False, "array map key did not raise ValueError" +except ValueError as exc: + assert "cannot be represented as a Python dict key" in str(exc) + +try: + CBORMapData([ + ( + CBOR_MAP(CBORMapData([(CBOR_UNSIGNED_INTEGER(1), CBOR_UNSIGNED_INTEGER(2))])), + CBOR_UNSIGNED_INTEGER(0), + ), + ]).as_dict() + assert False, "map map key did not raise ValueError" +except ValueError as exc: + assert "cannot be represented as a Python dict key" in str(exc) + = Deterministic encoding rebuilds floats from the semantic value from scapy.cbor.cbor import CBOR_FLOAT from scapy.cbor.cborcodec import CBORcodec_Object From 8fa6fca0a8d3bf8dff12d44dcc7d4f03dd566376 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:52:49 +0200 Subject: [PATCH 20/48] cbor: compare CBORMapData equality independent of pair order RFC 8949 maps are unordered mappings; reuse CBOR key equivalence for same-type equality instead of comparing stored pair order. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 20 +++++++++++++++++++- test/scapy/layers/cbor.uts | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 1029198ce86..5a162cefd3e 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -562,7 +562,25 @@ def __eq__(self, other): return False return True if isinstance(other, CBORMapData): - return self._pairs == other._pairs + # RFC 8949 maps are unordered; pair order is not identity. + if len(self._pairs) != len(other._pairs): + return False + used = [False] * len(other._pairs) + for map_key, value in self._pairs: + matched = False + for idx, (other_key, other_value) in enumerate(other._pairs): + if used[idx]: + continue + if not _cbor_key_equivalent(map_key, other_key): + continue + if value != other_value: + return False + used[idx] = True + matched = True + break + if not matched: + return False + return True return NotImplemented def __repr__(self): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 6da26cf7b16..dfc8018fb64 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3461,6 +3461,42 @@ try: except ValueError: pass += CBORMapData equality is independent of pair order +from scapy.cbor.cbor import ( + CBOR_TRUE, + CBOR_UNSIGNED_INTEGER, + CBORMapData, +) + +a = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "a"), + (CBOR_UNSIGNED_INTEGER(2), "b"), +]) +b = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(2), "b"), + (CBOR_UNSIGNED_INTEGER(1), "a"), +]) +assert a == b +assert b == a +assert a != CBORMapData([(CBOR_UNSIGNED_INTEGER(1), "a")]) +assert a != CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "a"), + (CBOR_UNSIGNED_INTEGER(2), "c"), +]) +# Integer 1 and Boolean true remain distinct CBOR keys. +typed = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "i"), + (CBOR_TRUE(), "b"), +]) +assert typed == CBORMapData([ + (CBOR_TRUE(), "b"), + (CBOR_UNSIGNED_INTEGER(1), "i"), +]) +assert typed != CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), "i"), + (CBOR_UNSIGNED_INTEGER(1), "b"), +]) + = CBORMapData.as_dict rejects unhashable array and map keys from scapy.cbor.cbor import ( CBOR_ARRAY, From ab43021a9ad408b891244de40c757a58e085b52a Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:55:57 +0200 Subject: [PATCH 21/48] cbor: share homogeneous SEQUENCE_OF/ARRAY_OF via _CBORF_HOMOGENEOUS Factor pkt_cls selection, element codec helpers, and max_count limits into a private base so ARRAY_OF gains the same decode caps as SEQUENCE_OF. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 382 +++++++++++++++++-------------------- test/scapy/layers/cbor.uts | 79 ++++++++ 2 files changed, 254 insertions(+), 207 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index e8d8b9f0f85..9921ea59e28 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1509,7 +1509,133 @@ class CBORF_ARRAY_INDEFINITE(CBORF_ARRAY): ] -class CBORF_SEQUENCE_OF(CBORF_field[List[Any]]): +class _CBORF_HOMOGENEOUS(CBORF_field[List[Any]]): + """Shared machinery for homogeneous CBOR collections.""" + islist = 1 + + def __init__(self, + name, # type: str + default, # type: Any + pkt_cls=None, # type: _ARRAY_T + max_count=None, # type: Optional[int] + ): + # type: (...) -> None + self.cls = None + self.item_field = None + self.holds_packets = 0 + self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] + self.max_count = max_count + self._init_element_type(pkt_cls) + super(_CBORF_HOMOGENEOUS, self).__init__(name, default) + + def _init_element_type(self, pkt_cls): + # type: (_ARRAY_T) -> None + 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.item_field = chosen + self.holds_packets = 0 + elif ( + isinstance(chosen, type) + and issubclass(chosen, Packet) + and hasattr(chosen, "CBOR_root") + ): + self.cls = cast("Type[CBOR_Packet]", chosen) + self.holds_packets = 1 + else: + raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") + + def _list_limit(self): + # type: () -> int + if self.max_count is not None: + return self.max_count + return config.conf.max_list_count + + def _check_list_limit(self, consumed): + # type: (int) -> None + limit = self._list_limit() + if consumed >= limit: + raise CBOR_Decoding_Error( + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, limit) + ) + + def any2i(self, pkt, x): + # type: (CBOR_Packet, Any) -> List[Any] + if x is None: + return None # type: ignore + if self.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_element(self, pkt, s, values=None): + # type: (CBOR_Packet, bytes, Optional[List[Any]]) -> Tuple[Any, bytes] + if self.holds_packets: + pkt_cls = self.cls + if self.next_cls_cb is not None: + values = values if values is not None else [] + pkt_cls = self.next_cls_cb( + pkt, + values, + values[-1] if values else None, + s, + ) + if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: + return CBOR_NO_ITEM, s + item_bytes, remaining = cbor_item_span(s) + try: + child = pkt_cls(item_bytes, _parent=pkt) # type: ignore + except CBOR_Decoding_Error: + raise + except Exception as exc: + raise CBOR_Decoding_Error(str(exc)) + return child, remaining + result = self.item_field.parse_value(pkt, s) + if result.items != 1: + raise CBOR_Decoding_Error( + "%s element must consume exactly one item" + % self.__class__.__name__ + ) + return result.value, result.remaining + + def _encode_element(self, pkt, item): + # type: (CBOR_Packet, Any) -> bytes + if self.holds_packets: + return _encode_exactly_one_cbor_item( + item, context="%s element" % self.__class__.__name__ + ) + result = self.item_field.build_value(pkt, item) + if result.items != 1: + raise CBOR_Encoding_Error( + "%s element must emit exactly one item" + % self.__class__.__name__ + ) + return result.data + + def i2repr(self, pkt, x): + # type: (CBOR_Packet, Any) -> str + if self.holds_packets: + return repr(x) + if x is None: + return self._empty_repr + return self._open_repr + ", ".join( + self.item_field.i2repr(pkt, item) for item in x + ) + self._close_repr + + def __repr__(self): + # type: () -> str + return "<%s %s>" % (self.__class__.__name__, self.name) + + +class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): """ Unframed sequence of homogeneous elements (no CBOR array head). @@ -1529,7 +1655,9 @@ class CBORF_SEQUENCE_OF(CBORF_field[List[Any]]): and ``max_count`` to cap decoding (defaults to ``conf.max_list_count``). """ CBOR_tag = None - islist = 1 + _empty_repr = "()" + _open_repr = "(" + _close_repr = ")" def __init__(self, name, # type: str @@ -1541,68 +1669,34 @@ def __init__(self, ): # type: (...) -> None self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] - self.cls = None - self.item_field = None - self.holds_packets = 0 self.count_from = count_from - self.max_count = max_count - if next_cls_cb is not None: if pkt_cls is not None: raise ValueError( "Pass only next_cls_cb, or only pkt_cls" ) self.next_cls_cb = next_cls_cb + self.cls = None + self.item_field = None 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 ( - isinstance(chosen, type) - and issubclass(chosen, Packet) - and hasattr(chosen, "CBOR_root") - ): - 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) + self.max_count = max_count + CBORF_field.__init__(self, name, default) + return + super(CBORF_SEQUENCE_OF, self).__init__( + name, default, pkt_cls=pkt_cls, max_count=max_count + ) @property def is_unbounded(self): # type: () -> bool return self.count_from is None - def _list_limit(self): - # type: () -> int - return self.max_count or config.conf.max_list_count - - 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 - limit = self._list_limit() if self.count_from is not None: if callable(self.count_from): max_items = int(self.count_from(pkt)) @@ -1611,47 +1705,19 @@ def _decode_items(self, pkt, data, max_items=None): while remaining and not cbor_is_break(remaining): if max_items is not None and consumed >= max_items: break - if consumed >= limit: - raise CBOR_Decoding_Error( - "CBOR SEQUENCE_OF exceeded max_count=%d" % limit - ) + self._check_list_limit(consumed) 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 + item, next_remaining = self._decode_element( + pkt, remaining, values=values + ) + if item is CBOR_NO_ITEM: + break + if len(next_remaining) >= before_len: + raise CBOR_Decoding_Error( + "Sequence decoder did not consume input") + values.append(item) + consumed += 1 + remaining = next_remaining return values, remaining, consumed def m2i(self, pkt, s): @@ -1673,25 +1739,8 @@ def build_result(self, pkt): 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) + parts = [self._encode_element(pkt, item) for item in val] + return CBORBuildResult(b"".join(parts), len(val)) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1707,23 +1756,8 @@ def max_items(self, pkt): return self.min_items(pkt) return self._list_limit() - 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]]): +class CBORF_ARRAY_OF(_CBORF_HOMOGENEOUS): """ CBOR array of homogeneous elements (major type 4). @@ -1735,47 +1769,23 @@ class CBORF_ARRAY_OF(CBORF_field[List[Any]]): ``pkt_cls`` may be a :class:`CBOR_Packet` subclass or a :class:`CBORF_field` class/instance. Do not use a ``cls=`` keyword: :class:`~typing.Generic` reserves that name on Python 3.7. + Use ``max_count`` to cap decoding (defaults to ``conf.max_list_count``). """ CBOR_tag = CBOR_MajorTypes.ARRAY - islist = 1 + _empty_repr = "[]" + _open_repr = "[" + _close_repr = "]" def __init__(self, name, # type: str default, # type: Any pkt_cls=None, # type: _ARRAY_T + max_count=None, # type: Optional[int] ): # type: (...) -> None - 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.item_field = chosen - self.holds_packets = 0 - elif ( - isinstance(chosen, type) - and issubclass(chosen, Packet) - and hasattr(chosen, "CBOR_root") - ): - self.cls = cast("Type[CBOR_Packet]", chosen) - self.holds_packets = 1 - else: - raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") - super(CBORF_ARRAY_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] + super(CBORF_ARRAY_OF, self).__init__( + name, default, pkt_cls=pkt_cls, max_count=max_count + ) def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[List[Any], bytes] @@ -1783,41 +1793,27 @@ def m2i(self, pkt, s): major_type, count, s = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) - if major_type != 4: + if major_type != int(CBOR_MajorTypes.ARRAY): raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) 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() + self._check_list_limit(len(lst)) + item, s = self._decode_element(pkt, s) + lst.append(item) else: + if count > self._list_limit(): + raise CBOR_Decoding_Error( + "CBOR %s exceeded max_count=%d" + % (self.__class__.__name__, self._list_limit()) + ) for _ in range(count): - _decode_element() + item, s = self._decode_element(pkt, s) + lst.append(item) return lst, s def build_result(self, pkt): @@ -1826,39 +1822,11 @@ def build_result(self, pkt): if val is None: 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 + parts = [self._encode_element(pkt, item) for item in val] + data = CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), len(val)) + data += b"".join(parts) return CBORBuildResult(data, 1) - def i2repr(self, pkt, x): - # type: (CBOR_Packet, Any) -> str - if self.holds_packets: - return repr(x) - elif x is None: - return "[]" - else: - return "[%s]" % ", ".join( - self.item_field.i2repr(pkt, item) for item in x - ) - - def __repr__(self): - # type: () -> str - return "<%s %s>" % (self.__class__.__name__, self.name) class CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index dfc8018fb64..1c5d201e13a 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2920,6 +2920,85 @@ assert pkt.children[0].n == 1 assert pkt.children[0].parent is pkt assert pkt.children[1].parent is pkt + ++ Homogeneous collection max_count limits + += CBORF_ARRAY_OF respects explicit max_count on definite arrays +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LimitedArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "values", [], pkt_cls=CBORF_UNSIGNED_INTEGER, max_count=2 + ) + +assert LimitedArray(b"\x82\x01\x02").values == [1, 2] +try: + LimitedArray(b"\x83\x01\x02\x03") + assert False, "definite ARRAY_OF ignored max_count" +except CBOR_Decoding_Error: + pass + + += CBORF_ARRAY_OF respects explicit max_count on indefinite arrays +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LimitedIndefArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF( + "values", [], pkt_cls=CBORF_UNSIGNED_INTEGER, max_count=2 + ) + +assert LimitedIndefArray(b"\x9f\x01\x02\xff").values == [1, 2] +try: + LimitedIndefArray(b"\x9f\x01\x02\x03\xff") + assert False, "indefinite ARRAY_OF ignored max_count" +except CBOR_Decoding_Error: + pass + + += CBORF_ARRAY_OF falls back to conf.max_list_count +from scapy.config import conf +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_ARRAY_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class ConfLimitedArray(CBOR_Packet): + CBOR_root = CBORF_ARRAY_OF("values", [], pkt_cls=CBORF_UNSIGNED_INTEGER) + +old = conf.max_list_count +conf.max_list_count = 2 +try: + assert ConfLimitedArray(b"\x82\x01\x02").values == [1, 2] + try: + ConfLimitedArray(b"\x83\x01\x02\x03") + assert False, "ARRAY_OF ignored conf.max_list_count" + except CBOR_Decoding_Error: + pass +finally: + conf.max_list_count = old + + += CBORF_SEQUENCE_OF respects max_count +from scapy.cbor.cbor import CBOR_Decoding_Error +from scapy.cbor.cborfields import CBORF_SEQUENCE_OF, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class LimitedSeq(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE_OF( + "values", [], pkt_cls=CBORF_UNSIGNED_INTEGER, max_count=2 + ) + +assert LimitedSeq(b"\x01\x02").values == [1, 2] +try: + LimitedSeq(b"\x01\x02\x03") + assert False, "SEQUENCE_OF ignored max_count" +except CBOR_Decoding_Error: + pass + + + Medium-severity review follow-ups = CBORF_FLOAT rebuilds preferred half-float after cache clear From 91b7b67a0b7a6d74b0c3c2b4d502a640e17fe84f Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:56:26 +0200 Subject: [PATCH 22/48] cbor: own SEQUENCE/ARRAY schema checks on _CBORF_compound Move unwrap and unbounded SEQUENCE_OF validation onto the compound base so SEQUENCE and ARRAY share one implementation without sibling calls. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 59 ++++++++++++++++++---------------------- 1 file changed, 27 insertions(+), 32 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 9921ea59e28..216c971e290 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1254,6 +1254,33 @@ def _mark_absent(self, pkt, field): # Condition false or skipped: leave value untouched. pass + def _unwrap_transparent_cbor_wrapper(self, field): + # type: (Any) -> Any + """Unwrap optional/conditional wrappers that add no CBOR framing.""" + while True: + if isinstance(field, CBORF_optional): + field = field._field + elif isinstance(field, CBORF_CONDITIONAL): + field = field.fld + else: + return field + + def _reject_ambiguous_unbounded_sequences(self): + # type: () -> None + for index, field in enumerate(self.seq): + inner = self._unwrap_transparent_cbor_wrapper(field) + if not ( + isinstance(inner, CBORF_SEQUENCE_OF) + and getattr(inner, "is_unbounded", False) + ): + continue + # Unbounded SEQUENCE_OF must be the final schema field. + if index != len(self.seq) - 1: + raise ValueError( + "Unbounded CBORF_SEQUENCE_OF must be the last field " + "in the sequence (or provide count_from=)" + ) + def _dissect_children(self, pkt, s, count): # type: (CBOR_Packet, bytes, Union[int, CBOR_INDEFINITE]) -> bytes remaining = s @@ -1357,10 +1384,6 @@ def __init__(self, *seq, **kwargs): 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) @@ -1397,18 +1420,6 @@ def max_items(self, pkt): return sum(f.max_items(pkt) for f in self.seq) -def _unwrap_transparent_cbor_wrapper(field): - # type: (Any) -> Any - """Unwrap optional/conditional wrappers that add no CBOR framing.""" - while True: - if isinstance(field, CBORF_optional): - field = field._field - elif isinstance(field, CBORF_CONDITIONAL): - field = field.fld - else: - return field - - class CBORF_ARRAY(_CBORF_compound): """ CBOR array with a fixed sequence of named, typed fields (major type 4). @@ -1436,22 +1447,6 @@ def __init__(self, *seq, **kwargs): super(CBORF_ARRAY, self).__init__(*seq, **kwargs) self._reject_ambiguous_unbounded_sequences() - def _reject_ambiguous_unbounded_sequences(self): - # type: () -> None - for index, field in enumerate(self.seq): - inner = _unwrap_transparent_cbor_wrapper(field) - if not ( - isinstance(inner, CBORF_SEQUENCE_OF) - and getattr(inner, "is_unbounded", False) - ): - continue - # Unbounded SEQUENCE_OF must be the final schema field. - if index != len(self.seq) - 1: - raise ValueError( - "Unbounded CBORF_SEQUENCE_OF must be the last field " - "in the sequence (or provide count_from=)" - ) - def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult items_data, total_items = self._build_children(pkt) From 47f0426bccceedea67e726ef7637a43cd9f34ae1 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 09:58:00 +0200 Subject: [PATCH 23/48] cbor: make item encode/decode real CBORcodec_Object methods Move encode/decode helpers onto the codec class as staticmethods and drop end-of-file monkey-patches plus the unused private decode safe= flag. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 413 +++++++++++++++++++++------------------- 1 file changed, 213 insertions(+), 200 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 7f1377b8ffd..c19fe6db643 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -662,7 +662,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=False, depth=_depth) + return CBORcodec_Object.decode_cbor_item(s, depth=_depth) @classmethod def dec(cls, @@ -696,6 +696,211 @@ def enc(cls, s): # type: (_K) -> bytes raise NotImplementedError("Subclasses must implement enc") + @staticmethod + def encode_cbor_item(item): + # type: (Any) -> bytes + """Encode a Python value to CBOR bytes""" + from scapy.cbor.cbor import ( + CBOR_Object, + CBORMapData, + ) + + if isinstance(item, CBOR_Object): + return item.enc() + elif isinstance(item, CBORMapData): + return CBORcodec_MAP.enc(item) + elif isinstance(item, bool): + # Must check bool before int (bool is subclass of int) + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif isinstance(item, int): + if item >= 0: + return CBORcodec_UNSIGNED_INTEGER.enc(item) + else: + return CBORcodec_NEGATIVE_INTEGER.enc(item) + elif isinstance(item, bytes): + return CBORcodec_BYTE_STRING.enc(item) + elif isinstance(item, str): + return CBORcodec_TEXT_STRING.enc(item) + elif isinstance(item, list): + return CBORcodec_ARRAY.enc(item) + elif isinstance(item, dict): + return CBORcodec_MAP.enc(item) + elif isinstance(item, float): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + elif item is None: + return CBORcodec_SIMPLE_AND_FLOAT.enc(None) + else: + raise CBOR_Codec_Encoding_Error( + "Cannot encode type: %s" % type(item)) + + @staticmethod + def _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 = CBORcodec_Object.encode_cbor_item_deterministic(key) + value_bytes = CBORcodec_Object.encode_cbor_item_deterministic( + value + ) + encoded_pairs.append((key_bytes, value_bytes)) + encoded_pairs.sort(key=lambda item: item[0]) + parts = [CBOR_encode_head(5, len(encoded_pairs))] + for key_bytes, value_bytes in encoded_pairs: + parts.append(key_bytes) + parts.append(value_bytes) + return b"".join(parts) + + @staticmethod + def encode_cbor_item_deterministic(item): + # type: (Any) -> bytes + """Encode a Python value using RFC 8949 core-deterministic rules. + + Unlike :meth:`encode_cbor_item`, map keys at every nesting level are + sorted by their deterministic encoded bytes. Intended for schema-driven + rebuild paths such as preserved unknown ``CBORF_MAP`` members. + + :class:`~scapy.cbor.cbor.CBOR_Object` instances are accepted and reduced + to native values (preferred float encoding, deterministic nested maps). + """ + import math + from scapy.cbor.cbor import ( + CBOR_Object, + CBOR_ARRAY, + CBOR_FLOAT, + CBOR_MAP, + CBOR_SEMANTIC_TAG, + CBOR_SIMPLE_VALUE, + CBOR_UNDEFINED, + CBORMapData, + ) + + if isinstance(item, CBOR_Object): + if isinstance(item, CBOR_UNDEFINED): + return CBOR_UNDEFINED().enc() + if isinstance(item, CBOR_FLOAT): + encoded = getattr(item, "_encoded", None) + if encoded is not None and math.isnan(float(item.val)): + return _cbor_preferred_nan_encoding(encoded) + # Finite floats ignore original width; rebuild preferred form. + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item.val)) + if isinstance(item, CBOR_ARRAY): + return CBORcodec_Object.encode_cbor_item_deterministic( + list(item.val) + ) + if isinstance(item, CBOR_MAP): + if isinstance(item.val, CBORMapData): + return CBORcodec_Object._encode_cbor_map_deterministic( + item.val.cbor_pairs() + ) + if isinstance(item.val, list): + return CBORcodec_Object._encode_cbor_map_deterministic( + item.val + ) + return CBORcodec_Object._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) + + CBORcodec_Object.encode_cbor_item_deterministic(inner) + ) + if isinstance(item, CBOR_SIMPLE_VALUE): + return CBORcodec_SIMPLE_AND_FLOAT.enc(item) + return CBORcodec_Object.encode_cbor_item_deterministic(item.val) + if isinstance(item, CBORMapData): + return CBORcodec_Object._encode_cbor_map_deterministic( + item.cbor_pairs() + ) + if isinstance(item, dict): + return CBORcodec_Object._encode_cbor_map_deterministic( + list(item.items()) + ) + if isinstance(item, list): + encoded_items = [ + CBORcodec_Object.encode_cbor_item_deterministic(element) + for element in item + ] + return ( + CBOR_encode_head(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): + # Deterministic encoding always rebuilds from the semantic float + # value (shortest exact representation). Never reuse source wire. + # Plain NaNs without retained CBOR bytes use quiet binary16. + return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item)) + if item is None: + return CBORcodec_SIMPLE_AND_FLOAT.enc(None) + raise CBOR_Codec_Encoding_Error( + "Cannot deterministically encode type: %s" % type(item) + ) + + @staticmethod + def decode_cbor_item(s, depth=0): + # type: (Any, 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 = CBORcodec_Object.decode_cbor_item( + memoryview(s), depth=depth + ) + return ( + obj, + _cbor_buf_bytes(rem) if isinstance(rem, memoryview) else rem, + ) + if not s: + raise CBOR_Codec_Decoding_Error( + "Empty CBOR data", remaining=_cbor_buf_bytes(s)) + + if cbor_is_break(s): + raise CBOR_Codec_Decoding_Error( + "Standalone break byte (0xff)", + remaining=_cbor_buf_bytes(s)) + + initial_byte = s[0] + major_type = initial_byte >> 5 + + # Dispatch to appropriate codec based on major type + if major_type == 0: + return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=False, _depth=depth) + elif major_type == 1: + return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=False, _depth=depth) + elif major_type == 2: + return CBORcodec_BYTE_STRING.dec(s, safe=False, _depth=depth) + elif major_type == 3: + return CBORcodec_TEXT_STRING.dec(s, safe=False, _depth=depth) + elif major_type == 4: + return CBORcodec_ARRAY.dec(s, safe=False, _depth=depth) + elif major_type == 5: + return CBORcodec_MAP.dec(s, safe=False, _depth=depth) + elif major_type == 6: + return CBORcodec_SEMANTIC_TAG.dec(s, safe=False, _depth=depth) + elif major_type == 7: + return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=False, _depth=depth) + else: + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(s)) + CBOR_Codecs.CBOR.register_stem(CBORcodec_Object) @@ -947,7 +1152,7 @@ def do_dec(cls, 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) + remainder, depth=_depth + 1) items.append(item) else: for _ in range(length): @@ -955,7 +1160,7 @@ def do_dec(cls, 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) + remainder, depth=_depth + 1) items.append(item) return cls.cbor_object(items), remainder @@ -1026,12 +1231,12 @@ def _add_pair(key, value): 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) + remainder, depth=_depth + 1) if not remainder: raise CBOR_Codec_Decoding_Error( "Map key without value", remaining=s) value, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=False, depth=_depth + 1) + remainder, depth=_depth + 1) _add_pair(key, value) else: for _ in range(length): @@ -1039,12 +1244,12 @@ def _add_pair(key, value): 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) + remainder, depth=_depth + 1) if not remainder: raise CBOR_Codec_Decoding_Error( "Map key without value", remaining=s) value, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=False, depth=_depth + 1) + remainder, depth=_depth + 1) _add_pair(key, value) return cls.cbor_object(CBORMapData(pairs)), remainder @@ -1088,7 +1293,7 @@ def do_dec(cls, "Tag without following item", remaining=s) item, remainder = CBORcodec_Object.decode_cbor_item( - remainder, safe=False, depth=_depth + 1) + remainder, depth=_depth + 1) return cls.cbor_object((tag_num, item)), remainder @@ -1254,195 +1459,3 @@ def do_dec(cls, raise CBOR_Codec_Decoding_Error( "Invalid additional info for major type 7: %d" % additional_info, remaining=s) - - -# Helper methods for encoding/decoding arbitrary CBOR items - - -def _encode_cbor_item(item): - # type: (Any) -> bytes - """Encode a Python value to CBOR bytes""" - from scapy.cbor.cbor import ( - CBOR_Object, - CBORMapData, - ) - - if isinstance(item, CBOR_Object): - return item.enc() - elif isinstance(item, CBORMapData): - return CBORcodec_MAP.enc(item) - elif isinstance(item, bool): - # Must check bool before int (bool is subclass of int) - return CBORcodec_SIMPLE_AND_FLOAT.enc(item) - elif isinstance(item, int): - if item >= 0: - return CBORcodec_UNSIGNED_INTEGER.enc(item) - else: - return CBORcodec_NEGATIVE_INTEGER.enc(item) - elif isinstance(item, bytes): - return CBORcodec_BYTE_STRING.enc(item) - elif isinstance(item, str): - return CBORcodec_TEXT_STRING.enc(item) - elif isinstance(item, list): - return CBORcodec_ARRAY.enc(item) - elif isinstance(item, dict): - return CBORcodec_MAP.enc(item) - elif isinstance(item, float): - return CBORcodec_SIMPLE_AND_FLOAT.enc(item) - elif item is None: - return CBORcodec_SIMPLE_AND_FLOAT.enc(None) - else: - raise CBOR_Codec_Encoding_Error( - "Cannot encode type: %s" % type(item)) - - -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` instances are accepted and reduced to - native values (preferred float encoding, deterministic nested maps). - """ - import math - from scapy.cbor.cbor import ( - CBOR_Object, - CBOR_ARRAY, - CBOR_FLOAT, - CBOR_MAP, - CBOR_SEMANTIC_TAG, - CBOR_SIMPLE_VALUE, - CBOR_UNDEFINED, - CBORMapData, - ) - - if isinstance(item, CBOR_Object): - if isinstance(item, CBOR_UNDEFINED): - return CBOR_UNDEFINED().enc() - if isinstance(item, CBOR_FLOAT): - encoded = getattr(item, "_encoded", None) - if encoded is not None and math.isnan(float(item.val)): - return _cbor_preferred_nan_encoding(encoded) - # Finite floats ignore original width; rebuild preferred form. - return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item.val)) - 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 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): - # Deterministic encoding always rebuilds from the semantic float - # value (shortest exact representation). Never reuse source wire. - # Plain NaNs without retained CBOR bytes use quiet binary16. - return CBORcodec_SIMPLE_AND_FLOAT.enc(float(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=_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=False, _depth=depth) - elif major_type == 1: - return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=False, _depth=depth) - elif major_type == 2: - return CBORcodec_BYTE_STRING.dec(s, safe=False, _depth=depth) - elif major_type == 3: - return CBORcodec_TEXT_STRING.dec(s, safe=False, _depth=depth) - elif major_type == 4: - return CBORcodec_ARRAY.dec(s, safe=False, _depth=depth) - elif major_type == 5: - return CBORcodec_MAP.dec(s, safe=False, _depth=depth) - elif major_type == 6: - return CBORcodec_SEMANTIC_TAG.dec(s, safe=False, _depth=depth) - elif major_type == 7: - return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=False, _depth=depth) - else: - raise CBOR_Codec_Decoding_Error( - "Invalid major type: %d" % major_type, - remaining=_cbor_buf_bytes(s)) - - -# 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) From 662f7f0839a4d59eca2a90ddf6775f31f4e3b0bf Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:00:17 +0200 Subject: [PATCH 24/48] cbor: inline cache helpers and tighten packet field ownership Move raw-cache and ANY conversion helpers onto their owning classes, delete unused extract_packet/_cbor_packet_from_bytes wrappers, and share mutable default materialization between getfield accessors. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 292 +++++++++++++++++---------------------- scapy/cborpacket.py | 135 +++++++++--------- 2 files changed, 189 insertions(+), 238 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 216c971e290..4d7e5ed38cb 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -184,12 +184,6 @@ def _cbor_attach_parent(parent, child): 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. @@ -220,53 +214,6 @@ def cbor_object_to_python(obj): return obj.val -def python_to_cbor_object(value): - # type: (Any) -> Any - """Convert native Python values into a :class:`CBOR_Object` tree.""" - from scapy.cbor.cbor import ( - CBOR_ARRAY, - CBOR_BYTE_STRING, - CBOR_FALSE, - CBOR_FLOAT, - CBOR_MAP, - CBOR_NEGATIVE_INTEGER, - CBOR_NULL, - CBOR_TEXT_STRING, - CBOR_TRUE, - CBOR_UNSIGNED_INTEGER, - CBORMapData, - ) - if isinstance(value, CBOR_Object): - return value - if isinstance(value, CBORMapData): - return CBOR_MAP(CBORMapData([ - (python_to_cbor_object(k), python_to_cbor_object(v)) - for k, v in value.cbor_pairs() - ])) - if isinstance(value, bool): - return CBOR_TRUE() if value else CBOR_FALSE() - if value is None: - return CBOR_NULL() - if isinstance(value, int): - if value >= 0: - return CBOR_UNSIGNED_INTEGER(value) - return CBOR_NEGATIVE_INTEGER(value) - if isinstance(value, float): - return CBOR_FLOAT(value) - if isinstance(value, bytes): - return CBOR_BYTE_STRING(value) - if isinstance(value, str): - return CBOR_TEXT_STRING(value) - if isinstance(value, list): - return CBOR_ARRAY([python_to_cbor_object(item) for item in value]) - if isinstance(value, dict): - return CBOR_MAP(CBORMapData([ - (python_to_cbor_object(k), python_to_cbor_object(v)) - for k, v in value.items() - ])) - raise TypeError("Cannot convert %r to CBOR_Object" % (type(value),)) - - class CBORF_element(object): """Base class for CBOR packet field elements.""" @@ -389,29 +336,6 @@ def any2i(self, pkt, x): x = cbor_object_to_python(x) return self.h2i(pkt, x) - def extract_packet(self, - cls, # type: Type[CBOR_Packet] - s, # type: bytes - _parent=None, # type: Optional[CBOR_Packet] - ): - # type: (...) -> Tuple[CBOR_Packet, bytes] - try: - c = cls(s, _parent=_parent) - except CBORF_badsequence: - 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) @@ -558,6 +482,120 @@ def do_copy(self, x): # type: ignore[override] return x return copy.deepcopy(x) + @staticmethod + def python_to_cbor_object(value): + # type: (Any) -> Any + """Convert native Python values into a :class:`CBOR_Object` tree.""" + from scapy.cbor.cbor import ( + CBOR_ARRAY, + CBOR_BYTE_STRING, + CBOR_FALSE, + CBOR_FLOAT, + CBOR_MAP, + CBOR_NEGATIVE_INTEGER, + CBOR_NULL, + CBOR_TEXT_STRING, + CBOR_TRUE, + CBOR_UNSIGNED_INTEGER, + CBORMapData, + ) + convert = CBORF_ANY.python_to_cbor_object + if isinstance(value, CBOR_Object): + return value + if isinstance(value, CBORMapData): + return CBOR_MAP(CBORMapData([ + (convert(k), convert(v)) + for k, v in value.cbor_pairs() + ])) + if isinstance(value, bool): + return CBOR_TRUE() if value else CBOR_FALSE() + if value is None: + return CBOR_NULL() + if isinstance(value, int): + if value >= 0: + return CBOR_UNSIGNED_INTEGER(value) + return CBOR_NEGATIVE_INTEGER(value) + if isinstance(value, float): + return CBOR_FLOAT(value) + if isinstance(value, bytes): + return CBOR_BYTE_STRING(value) + if isinstance(value, str): + return CBOR_TEXT_STRING(value) + if isinstance(value, list): + return CBOR_ARRAY([convert(item) for item in value]) + if isinstance(value, dict): + return CBOR_MAP(CBORMapData([ + (convert(k), convert(v)) + for k, v in value.items() + ])) + raise TypeError("Cannot convert %r to CBOR_Object" % (type(value),)) + + @staticmethod + def _cache_fingerprint(obj): + # type: (Any) -> Any + """Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` values.""" + from scapy.cbor.cbor import CBORMapData + fingerprint = CBORF_ANY._cache_fingerprint + if obj is CBOR_ABSENT or obj is CBOR_NO_ITEM: + return ("sentinel", obj) + if isinstance(obj, CBOR_UNDEFINED): + return ("undefined",) + if isinstance(obj, CBOR_FLOAT): + fval = float(obj.val) + if math.isnan(fval): + token = ("nan",) # type: Any + elif math.isinf(fval): + token = ("inf", math.copysign(1.0, fval)) + elif fval == 0.0: + token = ("zero", math.copysign(1.0, fval)) + else: + token = ("num", fval) + encoded = getattr(obj, "_encoded", None) + return ("float", token, encoded) + if isinstance(obj, CBOR_ARRAY): + return ( + "array", + tuple(fingerprint(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, dict): + pairs = list(obj.val.items()) + else: + pairs = list(obj.val) + return ( + "map", + tuple( + (fingerprint(key), fingerprint(value)) + for key, value in pairs + ), + ) + if isinstance(obj, CBORMapData): + return ( + "mapdata", + tuple( + (fingerprint(key), fingerprint(value)) + for key, value in obj.cbor_pairs() + ), + ) + if isinstance(obj, CBOR_SEMANTIC_TAG): + tag_num, inner = obj.val + return ("tag", int(tag_num), fingerprint(inner)) + if isinstance(obj, CBOR_Object): + return (type(obj).__name__, obj.val) + if isinstance(obj, list): + return ("list", tuple(fingerprint(item) for item in obj)) + if isinstance(obj, dict): + return ( + "dict", + tuple( + (fingerprint(key), fingerprint(value)) + for key, value in obj.items() + ), + ) + return ("py", type(obj).__name__, obj) + def cache_fingerprint(self, x): # type: (Any) -> Any """Snapshot for Scapy mutable raw-cache comparison. @@ -566,7 +604,7 @@ def cache_fingerprint(self, x): clears the wire cache is visible even when the semantic float is unchanged. """ - return _cbor_any_cache_fingerprint(x) + return self._cache_fingerprint(x) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> Any @@ -574,7 +612,7 @@ def any2i(self, pkt, x): return x if isinstance(x, CBOR_UNDEFINED): return x - return python_to_cbor_object(x) + return self.python_to_cbor_object(x) def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult @@ -594,80 +632,6 @@ def encode_value(self, x): return CBORcodec_Object.encode_cbor_item(x) -def _cbor_any_cache_fingerprint(obj): - # type: (Any) -> Any - """Recursive rebuild-relevant fingerprint for ``CBORF_ANY`` values.""" - from scapy.cbor.cbor import CBORMapData - if obj is CBOR_ABSENT or obj is CBOR_NO_ITEM: - return ("sentinel", obj) - if isinstance(obj, CBOR_UNDEFINED): - return ("undefined",) - if isinstance(obj, CBOR_FLOAT): - fval = float(obj.val) - if math.isnan(fval): - token = ("nan",) # type: Any - elif math.isinf(fval): - token = ("inf", math.copysign(1.0, fval)) - elif fval == 0.0: - token = ("zero", math.copysign(1.0, fval)) - else: - token = ("num", fval) - encoded = getattr(obj, "_encoded", None) - return ("float", token, encoded) - if isinstance(obj, CBOR_ARRAY): - return ( - "array", - tuple(_cbor_any_cache_fingerprint(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, dict): - pairs = list(obj.val.items()) - else: - pairs = list(obj.val) - return ( - "map", - tuple( - ( - _cbor_any_cache_fingerprint(key), - _cbor_any_cache_fingerprint(value), - ) - for key, value in pairs - ), - ) - if isinstance(obj, CBORMapData): - return ( - "mapdata", - tuple( - ( - _cbor_any_cache_fingerprint(key), - _cbor_any_cache_fingerprint(value), - ) - for key, value in obj.cbor_pairs() - ), - ) - if isinstance(obj, CBOR_SEMANTIC_TAG): - tag_num, inner = obj.val - return ("tag", int(tag_num), _cbor_any_cache_fingerprint(inner)) - if isinstance(obj, CBOR_Object): - return (type(obj).__name__, obj.val) - if isinstance(obj, list): - return ("list", tuple(_cbor_any_cache_fingerprint(item) for item in obj)) - if isinstance(obj, dict): - return ( - "dict", - tuple( - ( - _cbor_any_cache_fingerprint(key), - _cbor_any_cache_fingerprint(value), - ) - for key, value in obj.items() - ), - ) - return ("py", type(obj).__name__, obj) - - ############################# # Simple CBOR Fields # ############################# @@ -876,7 +840,7 @@ def _decode_packet_value(self, pkt, data): if pkt_cls is None: return packet.Raw(data) try: - return _cbor_packet_from_bytes(pkt_cls, data, pkt) + return pkt_cls(data, _parent=pkt) # type: ignore except Exception as exc: raise CBOR_Decoding_Error( "Failed to decode byte-string packet content: %s" % exc @@ -2055,7 +2019,8 @@ def _dissect_value_bytes(fld, val_bytes): continue name = fld.name if name not in pair_values: - self._mark_map_field_absent(pkt, fld) + if isinstance(fld, CBORF_optional): + fld._field.mark_absent(pkt) continue _dissect_value_bytes(fld, pair_values[name]) @@ -2080,11 +2045,6 @@ def _dissect_value_bytes(fld, val_bytes): self._unknown_field.set_val(pkt, unknown_pairs) 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.mark_absent(pkt) - def build(self, pkt): # type: (CBOR_Packet) -> bytes return self.build_result(pkt).data @@ -2370,15 +2330,15 @@ def _parse_packet_item(self, pkt, s): """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) + child = self.cls(item_bytes, _parent=pkt) # type: ignore 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 + def _build_packet_item(self, val): + # type: (Any) -> CBORBuildResult """Encode a nested packet and enforce one top-level CBOR item.""" if val is None: raise CBOR_Encoding_Error( @@ -2396,7 +2356,7 @@ def i2m(self, pkt, x): # type: (CBOR_Packet, Any) -> bytes if x is None: return b"" - return self._build_packet_item(pkt, x).data + return self._build_packet_item(x).data def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> CBOR_Packet @@ -2404,7 +2364,7 @@ def any2i(self, pkt, x): def encode_value(self, x): # type: (Any) -> bytes - return self._build_packet_item(None, x).data # type: ignore + return self._build_packet_item(x).data def parse_value(self, pkt, s): # type: (CBOR_Packet, bytes) -> CBORParseResult @@ -2413,11 +2373,11 @@ def parse_value(self, pkt, s): def build_value(self, pkt, value): # type: (CBOR_Packet, Any) -> CBORBuildResult - return self._build_packet_item(pkt, value) + return self._build_packet_item(value) def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult - return self._build_packet_item(pkt, pkt.getfieldval(self.name)) + return self._build_packet_item(pkt.getfieldval(self.name)) def dissect_result(self, pkt, s): # type: (CBOR_Packet, bytes) -> CBORParseResult diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 439e33d7e95..1ba4b79e296 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -38,50 +38,6 @@ 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 packet with root-schema build/dissect and cache integration. @@ -92,6 +48,21 @@ class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): CBOR_root = None # type: Optional[Any] + def _raw_cache_is_valid(self): + # type: () -> bool + """Return True if ``raw_packet_cache`` still matches nested field state.""" + if self.raw_packet_cache is None or self.raw_packet_cache_fields is None: + return False + for fname, fval in self.raw_packet_cache_fields.items(): + fld, val = self.getfield_and_val(fname) + if self._raw_packet_cache_field_value(fld, val) != fval: + self.raw_packet_cache = None + self.raw_packet_cache_fields = None + self._cbor_raw_cache_items = None # type: ignore[attr-defined] + self.wirelen = None + return False + return True + def cbor_build_result(self): # type: () -> Any """Return ``CBORBuildResult`` for this packet's root schema. @@ -101,7 +72,7 @@ def cbor_build_result(self): packet merely to recover cardinality. """ from scapy.cbor.cborfields import CBORBuildResult - if _cbor_raw_cache_is_valid(self): + if self._raw_cache_is_valid(): items = getattr(self, "_cbor_raw_cache_items", None) if items is None: items = 1 @@ -126,34 +97,36 @@ def do_init_cached_fields(self, for_dissect_only=False): if f.holds_packets and f.name in self.fields: self.fields[f.name] = f.any2i(self, self.fields[f.name]) + def _materialize_cbor_default(self, attr): + # type: (str) -> Optional[Tuple[Any, Any]] + """Copy mutable/packet defaults into ``fields`` on first access.""" + if attr in self.fields or attr not in self.default_fields: + return None + fld = self.get_field(attr) + if fld is None or not ( + getattr(fld, "ismutable", False) or fld.holds_packets + ): + return None + 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] + 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] + materialized = self._materialize_cbor_default(attr) + if materialized is not None: + return materialized 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] + materialized = self._materialize_cbor_default(attr) + if materialized is not None: + return materialized[1] return super(CBOR_Packet, self).getfieldval(attr) def _raw_packet_cache_field_value(self, fld, val, copy=False): @@ -169,15 +142,33 @@ def _raw_packet_cache_field_value(self, fld, val, copy=False): def self_build(self): # type: () -> bytes - if _cbor_raw_cache_is_valid(self): + if self._raw_cache_is_valid(): return self.raw_packet_cache return self.CBOR_root.build(self) - def do_dissect(self, x): + def do_dissect(self, s): # type: (bytes) -> bytes - result = self.CBOR_root.dissect_result(self, x) - _finalize_cbor_raw_cache(self, x, result.remaining, result.items) - return result.remaining + from scapy.cbor.cborfields import CBOR_ABSENT + result = self.CBOR_root.dissect_result(self, s) + remain = result.remaining + self.raw_packet_cache = s[:-len(remain)] if remain else s + self._cbor_raw_cache_items = result.items # type: ignore[attr-defined] + self.raw_packet_cache_fields = {} + for f in self.fields_desc: + if f.name not in self.fields: + continue + fval = self.fields[f.name] + if fval is CBOR_ABSENT: + self.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: + self.raw_packet_cache_fields[f.name] = \ + self._raw_packet_cache_field_value(f, fval, copy=True) + self.explicit = 1 + return remain def copy(self): # type: () -> Packet From 06b63e32f689fb4d007479582c61410ba8b6c91b Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:02:29 +0200 Subject: [PATCH 25/48] cbor: add AdditionalInfo/SimpleValue enums and UINT64_MAX Introduce named constants for additional-info and simple-value codes and use MajorTypes/enums in codec and field major-type and head paths. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/__init__.py | 6 ++ scapy/cbor/cbor.py | 22 +++++ scapy/cbor/cborcodec.py | 207 +++++++++++++++++++++------------------ scapy/cbor/cborfields.py | 33 ++++--- 4 files changed, 158 insertions(+), 110 deletions(-) diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index 8d2316eb439..a12c85c63ec 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -14,6 +14,9 @@ CBOR_BadTag_Decoding_Error, CBOR_Codecs, CBOR_MajorTypes, + CBOR_AdditionalInfo, + CBOR_SimpleValue, + CBOR_UINT64_MAX, CBOR_Object, CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER, @@ -83,6 +86,9 @@ # Codecs "CBOR_Codecs", "CBOR_MajorTypes", + "CBOR_AdditionalInfo", + "CBOR_SimpleValue", + "CBOR_UINT64_MAX", # Objects "CBOR_Object", "CBOR_UNSIGNED_INTEGER", diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 5a162cefd3e..8f17c036218 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -289,6 +289,28 @@ class CBOR_MajorTypes(metaclass=CBOR_MajorTypes_metaclass): SIMPLE_AND_FLOAT = cast(CBORTag, 7) +class CBOR_AdditionalInfo(metaclass=Enum_metaclass): + """CBOR additional-info codes used with argument encoding (RFC 8949).""" + name = "CBOR_ADDITIONAL_INFO" + ONE_BYTE = 24 + TWO_BYTES = 25 + FOUR_BYTES = 26 + EIGHT_BYTES = 27 + INDEFINITE = 31 + + +class CBOR_SimpleValue(metaclass=Enum_metaclass): + """Well-known CBOR simple values encoded in major type 7.""" + name = "CBOR_SIMPLE_VALUE" + FALSE = 20 + TRUE = 21 + NULL = 22 + UNDEFINED = 23 + + +CBOR_UINT64_MAX = (1 << 64) - 1 + + class CBOR_Object_metaclass(type): def __new__(cls, name, # type: str diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index c19fe6db643..df961be88b9 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -23,6 +23,7 @@ ) from scapy.cbor.cbor import ( + CBOR_AdditionalInfo, CBOR_Codecs, CBOR_DECODING_ERROR, CBOR_Decoding_Error, @@ -30,6 +31,8 @@ CBOR_Error, CBOR_MajorTypes, CBOR_Object, + CBOR_SimpleValue, + CBOR_UINT64_MAX, _CBOR_ERROR, ) from scapy.compat import chb @@ -88,7 +91,7 @@ def CBOR_encode_head(major_type, value): 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: + if value < 0 or value > CBOR_UINT64_MAX: raise CBOR_Codec_Encoding_Error( "CBOR head value out of uint64 range: %r" % (value,)) if value < 24: @@ -96,26 +99,31 @@ def CBOR_encode_head(major_type, value): return chb((major_type << 5) | value) elif value < 256: # 1-byte value follows - return chb((major_type << 5) | 24) + chb(value) + return chb((major_type << 5) | int(CBOR_AdditionalInfo.ONE_BYTE)) + chb(value) elif value < 65536: # 2-byte value follows - return chb((major_type << 5) | 25) + struct.pack(">H", value) + return chb((major_type << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + struct.pack(">H", value) elif value < 4294967296: # 4-byte value follows - return chb((major_type << 5) | 26) + struct.pack(">I", value) + return chb((major_type << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + struct.pack(">I", value) else: # 8-byte value follows - return chb((major_type << 5) | 27) + struct.pack(">Q", value) + return chb((major_type << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + 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): + if major_type not in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + int(CBOR_MajorTypes.ARRAY), + int(CBOR_MajorTypes.MAP), +): raise CBOR_Codec_Encoding_Error( "Indefinite length not allowed for major type %d" % major_type ) - return chb((major_type << 5) | 31) + return chb((major_type << 5) | int(CBOR_AdditionalInfo.INDEFINITE)) def CBOR_encode_break(): @@ -166,14 +174,14 @@ def CBOR_decode_head(s): if additional_info < 24: # Value is in the additional info return major_type, additional_info, s[1:] - elif additional_info == 24: + elif additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): # 1-byte value follows if len(s) < 2: raise CBOR_Codec_Decoding_Error( "Not enough bytes for 1-byte value", remaining=_cbor_buf_bytes(s)) return major_type, s[1], s[2:] - elif additional_info == 25: + elif additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): # 2-byte value follows if len(s) < 3: raise CBOR_Codec_Decoding_Error( @@ -181,7 +189,7 @@ def CBOR_decode_head(s): remaining=_cbor_buf_bytes(s)) value = struct.unpack(">H", s[1:3])[0] return major_type, value, s[3:] - elif additional_info == 26: + elif additional_info == int(CBOR_AdditionalInfo.FOUR_BYTES): # 4-byte value follows if len(s) < 5: raise CBOR_Codec_Decoding_Error( @@ -189,7 +197,7 @@ def CBOR_decode_head(s): remaining=_cbor_buf_bytes(s)) value = struct.unpack(">I", s[1:5])[0] return major_type, value, s[5:] - elif additional_info == 27: + elif additional_info == int(CBOR_AdditionalInfo.EIGHT_BYTES): # 8-byte value follows if len(s) < 9: raise CBOR_Codec_Decoding_Error( @@ -197,12 +205,21 @@ def CBOR_decode_head(s): 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): + elif additional_info == int(CBOR_AdditionalInfo.INDEFINITE): + if major_type in ( + int(CBOR_MajorTypes.UNSIGNED_INTEGER), + int(CBOR_MajorTypes.NEGATIVE_INTEGER), + int(CBOR_MajorTypes.TAG), + ): raise CBOR_Codec_Decoding_Error( "Indefinite length not allowed for major type %d" % major_type, remaining=_cbor_buf_bytes(s)) - if major_type in (2, 3, 4, 5): + if major_type in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + int(CBOR_MajorTypes.ARRAY), + int(CBOR_MajorTypes.MAP), + ): return major_type, CBOR_INDEFINITE, s[1:] raise CBOR_Codec_Decoding_Error( "Indefinite length not allowed for major type %d" % @@ -221,23 +238,23 @@ 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 + return additional_info == int(CBOR_AdditionalInfo.INDEFINITE) if additional_info < 24: return True - if additional_info == 24: + if additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): return value >= 24 - if additional_info == 25: + if additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): return value >= 256 - if additional_info == 26: + if additional_info == int(CBOR_AdditionalInfo.FOUR_BYTES): return value >= 65536 - if additional_info == 27: + if additional_info == int(CBOR_AdditionalInfo.EIGHT_BYTES): return value >= (1 << 32) - return additional_info == 31 + return additional_info == int(CBOR_AdditionalInfo.INDEFINITE) def _cbor_float_from_bits(ai, bits): # type: (int, int) -> float - if ai == 25: + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): sign = (bits >> 15) & 0x1 exponent = (bits >> 10) & 0x1f fraction = bits & 0x3ff @@ -250,7 +267,7 @@ def _cbor_float_from_bits(ai, bits): float("-inf") if sign else float("inf") ) return ((-1) ** sign) * (1.0 + fraction / 1024.0) * (2 ** (exponent - 15)) - if ai == 26: + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): return struct.unpack(">f", struct.pack(">I", bits))[0] return struct.unpack(">d", struct.pack(">Q", bits))[0] @@ -306,15 +323,15 @@ def _cbor_nan_preferred_ai(ai, bits): RFC 8949 prefers a shorter NaN only when zero-padding the shorter significand reconstructs the original NaN payload. """ - if ai == 25: + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): return 25 - if ai == 26: + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): # 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: + if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): # binary64 NaN: 1+11+52. mant = int(bits) & ((1 << 52) - 1) if mant == 0: @@ -337,21 +354,21 @@ def _cbor_nan_components(ai, bits): The significand is zero-extended to a binary64-width 52-bit field so half / single / double representations of the same NaN share identity. """ - if ai == 25: + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): sign = (int(bits) >> 15) & 0x1 exponent = (int(bits) >> 10) & 0x1f fraction = int(bits) & 0x3ff if exponent != 31 or not fraction: return None return sign, fraction << 42 - if ai == 26: + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): sign = (int(bits) >> 31) & 0x1 exponent = (int(bits) >> 23) & 0xff fraction = int(bits) & 0x7fffff if exponent != 0xff or not fraction: return None return sign, fraction << 29 - if ai == 27: + if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): sign = (int(bits) >> 63) & 0x1 exponent = (int(bits) >> 52) & 0x7ff fraction = int(bits) & ((1 << 52) - 1) @@ -364,21 +381,21 @@ def _cbor_nan_components(ai, bits): def _cbor_encode_nan(sign, significand52, ai): # type: (int, int, int) -> bytes """Encode a NaN at float AI *ai* preserving *sign* and *significand52*.""" - if ai == 25: + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): fraction = (significand52 >> 42) & 0x3ff bits = (sign << 15) | (0x1f << 10) | fraction - return chb(0xf9) + struct.pack(">H", bits) - if ai == 26: + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + struct.pack(">H", bits) + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): fraction = (significand52 >> 29) & 0x7fffff bits = (sign << 31) | (0xff << 23) | fraction - return chb(0xfa) + struct.pack(">I", bits) - if ai == 27: + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + struct.pack(">I", bits) + if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): bits = ( (sign << 63) | (0x7ff << 52) | (significand52 & ((1 << 52) - 1)) ) - return chb(0xfb) + struct.pack(">Q", bits) + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + struct.pack(">Q", bits) raise CBOR_Codec_Encoding_Error("Invalid NaN float AI: %d" % ai) @@ -389,15 +406,15 @@ def _cbor_float_bits_from_encoded(encoded): if not wire: raise CBOR_Codec_Encoding_Error("empty CBOR float encoding") ai = wire[0] & 0x1f - if ai == 25: + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): if len(wire) < 3: raise CBOR_Codec_Encoding_Error("truncated half float") return ai, struct.unpack(">H", wire[1:3])[0] - if ai == 26: + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): if len(wire) < 5: raise CBOR_Codec_Encoding_Error("truncated single float") return ai, struct.unpack(">I", wire[1:5])[0] - if ai == 27: + if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): if len(wire) < 9: raise CBOR_Codec_Encoding_Error("truncated double float") return ai, struct.unpack(">Q", wire[1:9])[0] @@ -476,31 +493,31 @@ def _walk(): pos = start + 1 if ai < 24: value = ai # type: Union[int, CBOR_INDEFINITE] - elif ai == 24: + elif ai == int(CBOR_AdditionalInfo.ONE_BYTE): if pos + 1 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 1-byte value", remaining=s[start:]) value = s[pos] pos += 1 - elif ai == 25: + elif ai == int(CBOR_AdditionalInfo.TWO_BYTES): if pos + 2 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 2-byte value", remaining=s[start:]) value = struct.unpack(">H", s[pos:pos + 2])[0] pos += 2 - elif ai == 26: + elif ai == int(CBOR_AdditionalInfo.FOUR_BYTES): if pos + 4 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 4-byte value", remaining=s[start:]) value = struct.unpack(">I", s[pos:pos + 4])[0] pos += 4 - elif ai == 27: + elif ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): if pos + 8 > len(s): raise CBOR_Codec_Decoding_Error( "Not enough bytes for 8-byte value", remaining=s[start:]) value = struct.unpack(">Q", s[pos:pos + 8])[0] pos += 8 - elif ai == 31: + elif ai == int(CBOR_AdditionalInfo.INDEFINITE): value = CBOR_INDEFINITE else: raise CBOR_Codec_Decoding_Error( @@ -509,7 +526,7 @@ def _walk(): # Major type 7: simple values and floats. Check float preferred width. if major == 7: - if ai == 24 and isinstance(value, int) and value < 32: + if ai == int(CBOR_AdditionalInfo.ONE_BYTE) and isinstance(value, int) and value < 32: issues.append(( base_offset + start, "Non-shortest CBOR simple value encoding " @@ -745,7 +762,7 @@ def _encode_cbor_map_deterministic(pairs): ) encoded_pairs.append((key_bytes, value_bytes)) encoded_pairs.sort(key=lambda item: item[0]) - parts = [CBOR_encode_head(5, len(encoded_pairs))] + parts = [CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(encoded_pairs))] for key_bytes, value_bytes in encoded_pairs: parts.append(key_bytes) parts.append(value_bytes) @@ -803,7 +820,7 @@ def encode_cbor_item_deterministic(item): if isinstance(item, CBOR_SEMANTIC_TAG): tag_num, inner = item.val return ( - CBOR_encode_head(6, tag_num) + CBOR_encode_head(int(CBOR_MajorTypes.TAG), tag_num) + CBORcodec_Object.encode_cbor_item_deterministic(inner) ) if isinstance(item, CBOR_SIMPLE_VALUE): @@ -823,7 +840,7 @@ def encode_cbor_item_deterministic(item): for element in item ] return ( - CBOR_encode_head(4, len(encoded_items)) + CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), len(encoded_items)) + b"".join(encoded_items) ) if isinstance(item, bool): @@ -880,21 +897,21 @@ def decode_cbor_item(s, depth=0): major_type = initial_byte >> 5 # Dispatch to appropriate codec based on major type - if major_type == 0: + if major_type == int(CBOR_MajorTypes.UNSIGNED_INTEGER): return CBORcodec_UNSIGNED_INTEGER.dec(s, safe=False, _depth=depth) - elif major_type == 1: + elif major_type == int(CBOR_MajorTypes.NEGATIVE_INTEGER): return CBORcodec_NEGATIVE_INTEGER.dec(s, safe=False, _depth=depth) - elif major_type == 2: + elif major_type == int(CBOR_MajorTypes.BYTE_STRING): return CBORcodec_BYTE_STRING.dec(s, safe=False, _depth=depth) - elif major_type == 3: + elif major_type == int(CBOR_MajorTypes.TEXT_STRING): return CBORcodec_TEXT_STRING.dec(s, safe=False, _depth=depth) - elif major_type == 4: + elif major_type == int(CBOR_MajorTypes.ARRAY): return CBORcodec_ARRAY.dec(s, safe=False, _depth=depth) - elif major_type == 5: + elif major_type == int(CBOR_MajorTypes.MAP): return CBORcodec_MAP.dec(s, safe=False, _depth=depth) - elif major_type == 6: + elif major_type == int(CBOR_MajorTypes.TAG): return CBORcodec_SEMANTIC_TAG.dec(s, safe=False, _depth=depth) - elif major_type == 7: + elif major_type == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): return CBORcodec_SIMPLE_AND_FLOAT.dec(s, safe=False, _depth=depth) else: raise CBOR_Codec_Decoding_Error( @@ -923,10 +940,10 @@ 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: + if i > CBOR_UINT64_MAX: raise CBOR_Codec_Encoding_Error( "Unsigned integer exceeds uint64 range") - return CBOR_encode_head(0, i) + return CBOR_encode_head(int(CBOR_MajorTypes.UNSIGNED_INTEGER), i) @classmethod def do_dec(cls, @@ -938,7 +955,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[int], bytes] cls.check_string(s) major_type, value, remainder = CBOR_decode_head(s) - if major_type != 0: + if major_type != int(CBOR_MajorTypes.UNSIGNED_INTEGER): raise CBOR_Codec_Decoding_Error( "Expected major type 0 (unsigned integer), got %d" % major_type, remaining=s) @@ -958,11 +975,11 @@ def enc(cls, obj): raise CBOR_Codec_Encoding_Error( "Cannot encode non-negative value as negative integer. " "Use CBOR_UNSIGNED_INTEGER for non-negative values.") - if i < -(1 << 64): + if i < -(CBOR_UINT64_MAX + 1): raise CBOR_Codec_Encoding_Error( "Negative integer below CBOR int64 range") # CBOR negative integer: -1 - n - return CBOR_encode_head(1, -1 - i) + return CBOR_encode_head(int(CBOR_MajorTypes.NEGATIVE_INTEGER), -1 - i) @classmethod def do_dec(cls, @@ -974,7 +991,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[int], bytes] cls.check_string(s) major_type, value, remainder = CBOR_decode_head(s) - if major_type != 1: + if major_type != int(CBOR_MajorTypes.NEGATIVE_INTEGER): raise CBOR_Codec_Decoding_Error( "Expected major type 1 (negative integer), got %d" % major_type, remaining=s) @@ -993,7 +1010,7 @@ def enc(cls, obj): data = obj.val if isinstance(obj, CBOR_Object) else obj if not isinstance(data, bytes): data = bytes(data) - return CBOR_encode_head(2, len(data)) + data + return CBOR_encode_head(int(CBOR_MajorTypes.BYTE_STRING), len(data)) + data @classmethod def do_dec(cls, @@ -1005,7 +1022,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[bytes], bytes] cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 2: + if major_type != int(CBOR_MajorTypes.BYTE_STRING): raise CBOR_Codec_Decoding_Error( "Expected major type 2 (byte string), got %d" % major_type, remaining=s) @@ -1054,7 +1071,7 @@ def enc(cls, obj): text_bytes = text.encode('utf-8') else: text_bytes = bytes(text) - return CBOR_encode_head(3, len(text_bytes)) + text_bytes + return CBOR_encode_head(int(CBOR_MajorTypes.TEXT_STRING), len(text_bytes)) + text_bytes @classmethod def do_dec(cls, @@ -1066,7 +1083,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[str], bytes] cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 3: + if major_type != int(CBOR_MajorTypes.TEXT_STRING): raise CBOR_Codec_Decoding_Error( "Expected major type 3 (text string), got %d" % major_type, remaining=s) @@ -1120,7 +1137,7 @@ 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 - parts = [CBOR_encode_head(4, len(array))] + parts = [CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), len(array))] parts.extend( CBORcodec_Object.encode_cbor_item(item) for item in array @@ -1137,7 +1154,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[List[Any]], bytes] cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 4: + if major_type != int(CBOR_MajorTypes.ARRAY): raise CBOR_Codec_Decoding_Error( "Expected major type 4 (array), got %d" % major_type, remaining=s) @@ -1186,7 +1203,7 @@ def enc(cls, obj): pairs = list(mapping.items()) else: pairs = list(mapping) - parts = [CBOR_encode_head(5, len(pairs))] + parts = [CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(pairs))] for key, value in pairs: parts.append(CBORcodec_Object.encode_cbor_item(key)) parts.append(CBORcodec_Object.encode_cbor_item(value)) @@ -1203,7 +1220,7 @@ def do_dec(cls, from scapy.cbor.cbor import CBORMapData cls.check_string(s) major_type, length, remainder = CBOR_decode_head(s) - if major_type != 5: + if major_type != int(CBOR_MajorTypes.MAP): raise CBOR_Codec_Decoding_Error( "Expected major type 5 (map), got %d" % major_type, remaining=s) @@ -1265,11 +1282,11 @@ 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 - if tag_num < 0 or tag_num > 0xFFFFFFFFFFFFFFFF: + if tag_num < 0 or tag_num > CBOR_UINT64_MAX: raise CBOR_Codec_Encoding_Error( "Semantic tag number out of uint64 range") return ( - CBOR_encode_head(6, tag_num) + CBOR_encode_head(int(CBOR_MajorTypes.TAG), tag_num) + CBORcodec_Object.encode_cbor_item(item) ) @@ -1283,7 +1300,7 @@ def do_dec(cls, # type: (...) -> Tuple[CBOR_Object[Tuple[int, Any]], bytes] cls.check_string(s) major_type, tag_num, remainder = CBOR_decode_head(s) - if major_type != 6: + if major_type != int(CBOR_MajorTypes.TAG): raise CBOR_Codec_Decoding_Error( "Expected major type 6 (tag), got %d" % major_type, remaining=s) @@ -1310,13 +1327,13 @@ def enc(cls, obj): # Check if obj is a CBOR object instance (for special cases like UNDEFINED) if isinstance(obj, CBOR_UNDEFINED): - return chb(0xf7) # undefined + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.UNDEFINED)) elif isinstance(obj, CBOR_NULL): - return chb(0xf6) # null + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.NULL)) elif isinstance(obj, CBOR_TRUE): - return chb(0xf5) # true + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.TRUE)) elif isinstance(obj, CBOR_FALSE): - return chb(0xf4) # false + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.FALSE)) elif isinstance(obj, CBOR_Object): # For other CBOR objects, use their val attribute val = obj.val @@ -1324,32 +1341,32 @@ def enc(cls, obj): val = obj if val is False: - return chb(0xf4) # false + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.FALSE)) elif val is True: - return chb(0xf5) # true + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.TRUE)) elif val is None: - return chb(0xf6) # null + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.NULL)) elif isinstance(val, float): # 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: + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): half = _cbor_float_to_half_bits(val) if half is not None: - return chb(0xf9) + struct.pack(">H", half) - ai = 26 - if ai == 26: + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + struct.pack(">H", half) + ai = int(CBOR_AdditionalInfo.FOUR_BYTES) + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): try: - return chb(0xfa) + struct.pack(">f", val) + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + struct.pack(">f", val) except (OverflowError, struct.error): pass - return chb(0xfb) + struct.pack(">d", val) + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + struct.pack(">d", val) elif isinstance(val, int) and 0 <= val <= 23: # Simple value 0-23 - return CBOR_encode_head(7, val) + return CBOR_encode_head(int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), val) elif isinstance(val, int) and 32 <= val <= 255: - return b"\xf8" + chb(val) + return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.ONE_BYTE)) + chb(val) else: raise CBOR_Codec_Encoding_Error( "Cannot encode value as simple/float: %r" % val) @@ -1375,21 +1392,21 @@ def do_dec(cls, major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - if major_type != 7: + if major_type != int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): raise CBOR_Codec_Decoding_Error( "Expected major type 7 (simple/float), got %d" % major_type, remaining=s) # Check for special simple values (encoded directly in additional_info) - if additional_info == 20: + if additional_info == int(CBOR_SimpleValue.FALSE): return CBOR_FALSE(), s[1:] - elif additional_info == 21: + elif additional_info == int(CBOR_SimpleValue.TRUE): return CBOR_TRUE(), s[1:] - elif additional_info == 22: + elif additional_info == int(CBOR_SimpleValue.NULL): return CBOR_NULL(), s[1:] - elif additional_info == 23: + elif additional_info == int(CBOR_SimpleValue.UNDEFINED): return CBOR_UNDEFINED(), s[1:] - elif additional_info == 25: + elif additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): # Half precision float (2 bytes) - IEEE 754 binary16 if len(s) < 3: raise CBOR_Codec_Decoding_Error( @@ -1425,14 +1442,14 @@ def do_dec(cls, (2 ** (exponent - 15))) return CBOR_FLOAT(float_val, encoded=_cbor_buf_bytes(s[:3])), remainder - elif additional_info == 26: + elif additional_info == int(CBOR_AdditionalInfo.FOUR_BYTES): # 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, encoded=_cbor_buf_bytes(s[:5])), s[5:] - elif additional_info == 27: + elif additional_info == int(CBOR_AdditionalInfo.EIGHT_BYTES): # Double precision float (8 bytes) if len(s) < 9: raise CBOR_Codec_Decoding_Error( @@ -1444,7 +1461,7 @@ def do_dec(cls, return CBOR_SIMPLE_VALUE(additional_info), s[1:] else: # additional_info 24 means 1-byte simple value follows - if additional_info == 24: + if additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): if len(s) < 2: raise CBOR_Codec_Decoding_Error( "Not enough bytes for simple value", remaining=s) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 4d7e5ed38cb..ca91a84b1e7 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -21,8 +21,11 @@ from scapy.cbor.cbor import ( CBOR_Decoding_Error, CBOR_Encoding_Error, + CBOR_AdditionalInfo, CBOR_MajorTypes, CBOR_Object, + CBOR_SimpleValue, + CBOR_UINT64_MAX, CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER, CBOR_BYTE_STRING, @@ -647,7 +650,7 @@ def any2i(self, pkt, x): if x is None: return None # type: ignore i = int(x) - if i < 0 or i > 0xFFFFFFFFFFFFFFFF: + if i < 0 or i > CBOR_UINT64_MAX: raise CBOR_Encoding_Error( "Unsigned integer out of CBOR range: %r" % (i,)) return i @@ -680,7 +683,7 @@ def any2i(self, pkt, x): if x is None: return None # type: ignore i = int(x) - if i >= 0 or i < -(1 << 64): + if i >= 0 or i < -(CBOR_UINT64_MAX + 1): raise CBOR_Encoding_Error( "Negative integer out of CBOR range: %r" % (i,)) return i @@ -722,7 +725,7 @@ def any2i(self, pkt, x): if x is None: return None # type: ignore i = int(x) - if i < -(1 << 64) or i > 0xFFFFFFFFFFFFFFFF: + if i < -(CBOR_UINT64_MAX + 1) or i > CBOR_UINT64_MAX: raise CBOR_Encoding_Error( "Integer out of CBOR range: %r" % (i,)) return i @@ -781,7 +784,7 @@ def m2i(self, pkt, s): 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: + if major_type != int(CBOR_MajorTypes.BYTE_STRING): raise CBOR_Type_Mismatch( "Expected byte string, got major type %d" % major_type) if length is CBOR_INDEFINITE: @@ -861,7 +864,7 @@ def m2i(self, pkt, s): 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: + if major_type != int(CBOR_MajorTypes.BYTE_STRING): raise CBOR_Type_Mismatch( "Expected byte string, got major type %d" % major_type) if length is CBOR_INDEFINITE: @@ -922,7 +925,7 @@ def matches_next_item(self, pkt, s): 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) + return ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) and ai in (int(CBOR_SimpleValue.FALSE), int(CBOR_SimpleValue.TRUE)) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> bool @@ -969,7 +972,7 @@ 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 + return s[0] == ((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.NULL)) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> None @@ -1024,7 +1027,7 @@ 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 + return s[0] == ((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.UNDEFINED)) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> None @@ -1077,7 +1080,7 @@ def matches_next_item(self, pkt, s): 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) + return ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) and ai in (int(CBOR_AdditionalInfo.TWO_BYTES), int(CBOR_AdditionalInfo.FOUR_BYTES), int(CBOR_AdditionalInfo.EIGHT_BYTES)) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> float @@ -1431,7 +1434,7 @@ def dissect_result(self, pkt, 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: + if major_type != int(CBOR_MajorTypes.ARRAY): raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) remaining = self._dissect_children(pkt, remaining, count) @@ -1931,7 +1934,7 @@ def build_result(self, pkt): 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) + data = CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(pairs)) + b"".join(parts) return CBORBuildResult(data, 1) def dissect_result(self, pkt, s): @@ -1940,7 +1943,7 @@ def dissect_result(self, pkt, 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: + if major_type != int(CBOR_MajorTypes.MAP): raise CBOR_Type_Mismatch( "Expected major type 5 (map), got %d" % major_type) @@ -2088,7 +2091,7 @@ def __init__(self, ): # type: (...) -> None self.tag_num = tag_num - if tag_num < 0 or tag_num > 0xFFFFFFFFFFFFFFFF: + if tag_num < 0 or tag_num > CBOR_UINT64_MAX: raise CBOR_Encoding_Error( "Semantic tag number out of uint64 range") self.inner_field = inner_field @@ -2107,7 +2110,7 @@ def _parse_tag_head(self, s, require_match=True): 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: + if major_type != int(CBOR_MajorTypes.TAG): raise CBOR_Type_Mismatch( "Expected major type 6 (semantic tag), got %d" % major_type) if require_match and tag_num != self.tag_num: @@ -2117,7 +2120,7 @@ def _parse_tag_head(self, s, require_match=True): def _encode_tagged(self, inner_data): # type: (bytes) -> bytes - return CBOR_encode_head(6, self.tag_num) + inner_data + return CBOR_encode_head(int(CBOR_MajorTypes.TAG), self.tag_num) + inner_data def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[int, bytes] From 165d1b1d2dcd01d1f5c1fcda6effe351df304bed Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:04:26 +0200 Subject: [PATCH 26/48] cbor: simplify float decode and indefinite child budgeting Decode floats via _cbor_float_from_bits, count indefinite array items with a lightweight skip walk, precompute suffix min_items, and document the non-deterministic scanner as one top-level item. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 138 ++++++++++++++++++++++++--------------- scapy/cbor/cborfields.py | 23 +++---- 2 files changed, 94 insertions(+), 67 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index df961be88b9..228a542bb7d 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -157,6 +157,66 @@ def cbor_consume_break(s): return s[1:] +def cbor_skip_item(s): + # type: (Any) -> Any + """Advance past one well-formed CBOR item without building objects.""" + major_type, value, rem = CBOR_decode_head(s) + if major_type in ( + int(CBOR_MajorTypes.UNSIGNED_INTEGER), + int(CBOR_MajorTypes.NEGATIVE_INTEGER), + int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), + ): + return rem + if major_type in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = cbor_skip_item(rem) + return cbor_consume_break(rem) + length = int(value) + if len(rem) < length: + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string", remaining=_cbor_buf_bytes(s)) + return rem[length:] + if major_type == int(CBOR_MajorTypes.ARRAY): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = cbor_skip_item(rem) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = cbor_skip_item(rem) + return rem + if major_type == int(CBOR_MajorTypes.MAP): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = cbor_skip_item(rem) + rem = cbor_skip_item(rem) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = cbor_skip_item(rem) + rem = cbor_skip_item(rem) + return rem + if major_type == int(CBOR_MajorTypes.TAG): + return cbor_skip_item(rem) + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(s), + ) + + +def cbor_count_items_until_break(s): + # type: (Any) -> int + """Count definite top-level items before a break without building objects.""" + rem = s + count = 0 + while rem and not cbor_is_break(rem): + rem = cbor_skip_item(rem) + count += 1 + return count + + def CBOR_decode_head(s): # type: (Any) -> Tuple[int, Union[int, CBOR_INDEFINITE], Any] """ @@ -465,11 +525,12 @@ def _cbor_preferred_float_ai_from_encoded(ai, bits): def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): # type: (bytes, bool, int) -> List[Tuple[int, str]] - """Scan *s* for encodings that are not RFC 8949 core-deterministic. + """Scan one top-level CBOR item for non-core-deterministic encodings. - Returns a list of ``(absolute_offset, message)`` issues. Indefinite-length - items are rejected by default. Protocols that permit indefinite containers - (for example some BPv7 outer arrays) may pass ``allow_indefinite=True``. + Walks a single top-level item (and nested contents). Trailing bytes after + that item are ignored. Returns ``(absolute_offset, message)`` issues. + Indefinite-length items are rejected by default; protocols that permit + them may pass ``allow_indefinite=True``. """ issues = [] # type: List[Tuple[int, str]] index = [0] @@ -1406,56 +1467,24 @@ def do_dec(cls, return CBOR_NULL(), s[1:] elif additional_info == int(CBOR_SimpleValue.UNDEFINED): return CBOR_UNDEFINED(), s[1:] - elif additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): - # Half precision float (2 bytes) - IEEE 754 binary16 - if len(s) < 3: - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for half float", remaining=s) - half_bytes = s[1:3] - remainder = s[3:] - # Convert IEEE 754 binary16 to binary64 (double) - half_int = struct.unpack(">H", half_bytes)[0] - sign = (half_int >> 15) & 0x1 - exponent = (half_int >> 10) & 0x1f - fraction = half_int & 0x3ff - - # Handle special cases - if exponent == 0: - if fraction == 0: - # Zero - float_val = -0.0 if sign else 0.0 - else: - # Subnormal number - float_val = ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) - elif exponent == 31: - if fraction == 0: - # Infinity - float_val = float('-inf') if sign else float('inf') - else: - # NaN - float_val = float('nan') - else: - # Normalized number - float_val = ( - ((-1) ** sign) * - (1 + fraction / 1024.0) * - (2 ** (exponent - 15))) - - return CBOR_FLOAT(float_val, encoded=_cbor_buf_bytes(s[:3])), remainder - elif additional_info == int(CBOR_AdditionalInfo.FOUR_BYTES): - # 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, encoded=_cbor_buf_bytes(s[:5])), s[5:] - elif additional_info == int(CBOR_AdditionalInfo.EIGHT_BYTES): - # Double precision float (8 bytes) - if len(s) < 9: + elif additional_info in ( + int(CBOR_AdditionalInfo.TWO_BYTES), + int(CBOR_AdditionalInfo.FOUR_BYTES), + int(CBOR_AdditionalInfo.EIGHT_BYTES), + ): + width = { + int(CBOR_AdditionalInfo.TWO_BYTES): 2, + int(CBOR_AdditionalInfo.FOUR_BYTES): 4, + int(CBOR_AdditionalInfo.EIGHT_BYTES): 8, + }[additional_info] + if len(s) < 1 + width: raise CBOR_Codec_Decoding_Error( - "Not enough bytes for double float", remaining=s) - float_val = struct.unpack(">d", s[1:9])[0] - return CBOR_FLOAT(float_val, encoded=_cbor_buf_bytes(s[:9])), s[9:] + "Not enough bytes for float", remaining=s) + fmt = {2: ">H", 4: ">I", 8: ">Q"}[width] + bits = struct.unpack(fmt, s[1:1 + width])[0] + float_val = _cbor_float_from_bits(additional_info, bits) + encoded = _cbor_buf_bytes(s[:1 + width]) + return CBOR_FLOAT(float_val, encoded=encoded), s[1 + width:] elif additional_info < 24: # Simple value 0-23 return CBOR_SIMPLE_VALUE(additional_info), s[1:] @@ -1474,5 +1503,6 @@ def do_dec(cls, return CBOR_SIMPLE_VALUE(simple), s[2:] else: raise CBOR_Codec_Decoding_Error( - "Invalid additional info for major type 7: %d" % additional_info, + "Invalid additional info for major type 7: %d" + % additional_info, remaining=s) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index ca91a84b1e7..61c2b3ef597 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -48,6 +48,7 @@ CBOR_encode_head, CBOR_encode_indefinite_head, CBOR_encode_break, + cbor_count_items_until_break, cbor_is_break, cbor_consume_break, CBORcodec_Object, @@ -1252,16 +1253,9 @@ 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 + # Lightweight head/span walk — avoid building CBOR_Object trees + # just to learn the item budget before the schema pass. + item_count = cbor_count_items_until_break(remaining) remaining = self._dissect_children_budgeted( pkt, remaining, item_count ) @@ -1273,10 +1267,13 @@ def _dissect_children_budgeted(self, pkt, s, count): # type: (CBOR_Packet, bytes, int) -> bytes remaining = s items_left = count + nfields = len(self.seq) + # suffix_mins[i] == sum(min_items of seq[i:]) + suffix_mins = [0] * (nfields + 1) + for i in range(nfields - 1, -1, -1): + suffix_mins[i] = suffix_mins[i + 1] + self.seq[i].min_items(pkt) for index, field in enumerate(self.seq): - reserved = sum( - f.min_items(pkt) for f in self.seq[index + 1:] - ) + reserved = suffix_mins[index + 1] available = items_left - reserved needed = field.min_items(pkt) if available < 0: From 595f82991510f60acf18615e9cf4666b3482c321 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:08:08 +0200 Subject: [PATCH 27/48] cbor: tighten SEMANTIC_TAG API and map unknown_field defaults Make CBORF_SEMANTIC_TAG a (tag_num, inner_field) element wrapper, default map unknowns to _cbor_unknown, and reject duplicate field names so multi-map schemas must choose distinct unknown_field values. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 80 ++++++++++++---------- test/scapy/layers/cbor.uts | 84 +++++++++--------------- test/scapy/layers/cbor_cbor2_interop.uts | 4 +- 3 files changed, 79 insertions(+), 89 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 61c2b3ef597..27bb07bb2ac 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1197,11 +1197,19 @@ def is_empty(self, pkt): def get_fields_list(self): # type: () -> List[CBORF_field[Any]] - return [ + fields_list = [ child for field in self.seq for child in field.get_fields_list() ] + names = [f.name for f in fields_list] + if len(names) != len(set(names)): + dupes = sorted({n for n in names if names.count(n) > 1}) + raise ValueError( + "Duplicate CBOR field name(s) %s; for multiple maps use " + "distinct unknown_field= values" % (dupes,) + ) + return fields_list def _build_children(self, pkt): # type: (CBOR_Packet) -> Tuple[bytes, int] @@ -1842,10 +1850,11 @@ class CBORF_MAP(CBORF_element): (sorted by encoded key bytes), independent of declaration order. Unknown received key/value pairs are retained in a dedicated packet field - (``unknown_field``, defaulting to a unique ``_cbor_unknown_`` name) as - ordered ``(key, value)`` pairs. While the packet raw cache is valid the - exact received bytes are preserved; after any mutation unknown members are - re-encoded using core-deterministic CBOR together with known fields. + (``unknown_field``, default ``"_cbor_unknown"``) as ordered ``(key, value)`` + pairs. While the packet raw cache is valid the exact received bytes are + preserved; after any mutation unknown members are re-encoded using + core-deterministic CBOR together with known fields. Schemas with more than + one map must pass distinct ``unknown_field=`` names. Example:: @@ -1858,11 +1867,10 @@ class MyCBOR(CBOR_Packet): CBOR_tag = CBOR_MajorTypes.MAP holds_packets = 1 islist = 1 - _unknown_id = 0 def __init__(self, *seq, **kwargs): # type: (*Any, **Any) -> None - unknown_field = kwargs.pop("unknown_field", None) + unknown_field = kwargs.pop("unknown_field", "_cbor_unknown") if kwargs: raise TypeError( "CBORF_MAP() got unexpected keyword arguments: %s" @@ -1881,9 +1889,6 @@ def __init__(self, *seq, **kwargs): encoded_keys[name] = CBORcodec_TEXT_STRING.enc(name) self._field_by_name = field_by_name self._encoded_keys = encoded_keys - if unknown_field is None: - CBORF_MAP._unknown_id += 1 - unknown_field = "_cbor_unknown_%d" % CBORF_MAP._unknown_id if unknown_field in field_by_name: raise ValueError( "CBORF_MAP unknown_field %r collides with a known member" @@ -2062,9 +2067,9 @@ def max_items(self, pkt): return 1 -class CBORF_SEMANTIC_TAG(CBORF_field[int]): +class CBORF_SEMANTIC_TAG(CBORF_element): """ - CBOR semantic tag field (major type 6). + CBOR semantic tag wrapper (major type 6). Wraps an ``inner_field`` with the given numeric ``tag_num``. The tag number is schema metadata only: it is not stored as editable packet @@ -2074,32 +2079,28 @@ class CBORF_SEMANTIC_TAG(CBORF_field[int]): class TimestampPkt(CBOR_Packet): CBOR_root = CBORF_SEMANTIC_TAG( - "tag_info", None, 1, CBORF_INTEGER("ts", 0) + 1, CBORF_INTEGER("ts", 0) ) """ CBOR_tag = CBOR_MajorTypes.TAG holds_packets = 0 def __init__(self, - name, # type: str - default, # type: Any tag_num, # type: int inner_field, # type: CBORF_field[Any] ): # type: (...) -> None - self.tag_num = tag_num if tag_num < 0 or tag_num > CBOR_UINT64_MAX: raise CBOR_Encoding_Error( "Semantic tag number out of uint64 range") + self.tag_num = tag_num 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 CBOR_ABSENT: - # Tag number is schema metadata; absence applies to the value field. - self.inner_field.default = CBOR_ABSENT - if default is None: - default = tag_num - super(CBORF_SEMANTIC_TAG, self).__init__(name, default) + + @property + def name(self): + # type: () -> str + """Map/schema key identity comes from the tagged value field.""" + return self.inner_field.name def _parse_tag_head(self, s, require_match=True): # type: (bytes, bool) -> Tuple[int, bytes] @@ -2119,10 +2120,6 @@ def _encode_tagged(self, inner_data): # type: (bytes) -> bytes return CBOR_encode_head(int(CBOR_MajorTypes.TAG), 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): @@ -2131,11 +2128,14 @@ def matches_next_item(self, pkt, s): 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 + return ( + major_type == int(CBOR_MajorTypes.TAG) + 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) + _tag_num, remaining = self._parse_tag_head(s) inner = self.inner_field.dissect_result(pkt, remaining) if inner.items != 1: raise CBOR_Decoding_Error( @@ -2154,6 +2154,10 @@ def build_result(self, pkt): "Semantic tag content must be exactly one CBOR item") return CBORBuildResult(self._encode_tagged(inner.data), 1) + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + return self.build_result(pkt).data + def parse_value(self, pkt, s): # type: (CBOR_Packet, bytes) -> CBORParseResult _tag_num, remaining = self._parse_tag_head(s) @@ -2161,7 +2165,9 @@ def parse_value(self, pkt, s): 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) + return CBORParseResult( + value=inner.value, remaining=inner.remaining, items=1 + ) def build_value(self, pkt, value): # type: (CBOR_Packet, Any) -> CBORBuildResult @@ -2196,6 +2202,14 @@ def set_val(self, pkt, val): return self.inner_field.set_val(pkt, val) + def min_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + + def max_items(self, pkt): + # type: (CBOR_Packet) -> int + return 1 + ############################## # Complex CBOR Fields # @@ -2203,7 +2217,7 @@ def set_val(self, pkt, val): class CBORF_optional(CBORF_element): """ - Wrapper making a :class:`CBORF_field` optional. + Wrapper making a CBOR schema element optional. Absence is recorded as ``CBOR_ABSENT`` on every path (lookahead mismatch, exhausted parent array, missing map key). If the next item matches but @@ -2211,7 +2225,7 @@ class CBORF_optional(CBORF_element): """ def __init__(self, field): - # type: (CBORF_field[Any]) -> None + # type: (CBORF_element) -> None self._field = field def __getattr__(self, attr): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 1c5d201e13a..f6fab0f191e 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -346,12 +346,7 @@ from scapy.cborpacket import CBOR_Packet class OptionalTaggedThenFallback(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_optional( - CBORF_SEMANTIC_TAG( - "tag_number", - None, - 1, - CBORF_UNSIGNED_INTEGER("tagged_value", None), - ) + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ), CBORF_ANY("fallback", None), ) @@ -374,12 +369,7 @@ 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), - ) + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ) ) @@ -406,12 +396,7 @@ 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), - ) + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ) ) @@ -440,12 +425,7 @@ 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_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ), CBORF_ANY("fallback", None), ) @@ -871,7 +851,7 @@ class OptMapPkt(CBOR_Packet): 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_SEMANTIC_TAG(1, CBORF_INTEGER("ts", 0))), CBORF_optional(CBORF_TEXT_STRING("endpoint", "default")), ) @@ -1351,12 +1331,7 @@ class RRAbsentTag(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_UNSIGNED_INTEGER("head", 0), CBORF_optional( - CBORF_SEMANTIC_TAG( - "tag_number", - None, - 1, - CBORF_UNSIGNED_INTEGER("tagged_value", 7), - ) + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", 7)) ), ) @@ -1659,6 +1634,20 @@ try: except (CBOR_Decoding_Error, CBOR_Codec_Decoding_Error): pass + += Multi-map schemas require distinct unknown_field names +from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet +try: + class BadTwoMaps(CBOR_Packet): + CBOR_root = CBORF_ARRAY( + CBORF_MAP(CBORF_UNSIGNED_INTEGER("a", 0)), + CBORF_MAP(CBORF_UNSIGNED_INTEGER("b", 0)), + ) + assert False, "duplicate default unknown_field accepted" +except ValueError: + pass + = Duplicate fixed-map schema names are rejected at class construction try: CBORF_MAP( @@ -2087,15 +2076,12 @@ from scapy.cborpacket import CBOR_Packet class OptionalSemanticTagDefault(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_optional( - CBORF_SEMANTIC_TAG( - "tag", - CBOR_ABSENT, - 1, - CBORF_UNSIGNED_INTEGER("value", 0), - ) + CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("value", 0)) ) ) +OptionalSemanticTagDefault.CBOR_root.seq[0]._field.inner_field.default = CBOR_ABSENT + pkt = OptionalSemanticTagDefault() assert pkt.getfieldval("value") is CBOR_ABSENT assert bytes(pkt) == b"\x80" @@ -3035,18 +3021,18 @@ assert isinstance(pkt.payload, Raw) or pkt.original.endswith(b"\x03") 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 += CBORF_SEMANTIC_TAG 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)) +fld = CBORF_SEMANTIC_TAG(1, CBORF_INTEGER("ts", 0)) try: - fld.m2i(None, b"\xc2\x00") # tag 2 + fld._parse_tag_head(b"\xc2\x00") # tag 2 except CBOR_Type_Mismatch: pass else: - raise AssertionError("wrong tag accepted by m2i") + raise AssertionError("wrong tag accepted") = Deterministic encoder accepts CBOR_Object wrappers from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER, CBOR_TEXT_STRING, CBOR_MAP, CBORMapData @@ -3094,12 +3080,7 @@ 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_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("tagged_value", None)) ), CBORF_ANY("fallback", None), ) @@ -3696,12 +3677,7 @@ from scapy.cbor.cborfields import CBORF_SEMANTIC_TAG, CBORF_INTEGER from scapy.cborpacket import CBOR_Packet class TaggedTs(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG( - "tag_number", - None, - 1, - CBORF_INTEGER("ts", 0), - ) + CBOR_root = CBORF_SEMANTIC_TAG(1, CBORF_INTEGER("ts", 0)) assert "tag_number" not in [f.name for f in TaggedTs.fields_desc] assert "ts" in [f.name for f in TaggedTs.fields_desc] @@ -3724,9 +3700,11 @@ class TwoMaps(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_MAP( CBORF_UNSIGNED_INTEGER("a", 0), + unknown_field="unknown_a", ), CBORF_MAP( CBORF_UNSIGNED_INTEGER("b", 0), + unknown_field="unknown_b", ), ) diff --git a/test/scapy/layers/cbor_cbor2_interop.uts b/test/scapy/layers/cbor_cbor2_interop.uts index a650c1359fe..1c1c863b921 100644 --- a/test/scapy/layers/cbor_cbor2_interop.uts +++ b/test/scapy/layers/cbor_cbor2_interop.uts @@ -416,9 +416,7 @@ class RRCbor2UIntArray(CBOR_Packet): class RRCbor2TaggedText(CBOR_Packet): - CBOR_root = CBORF_SEMANTIC_TAG( - "tag_number", None, 60000, CBORF_TEXT_STRING("value", "") - ) + CBOR_root = CBORF_SEMANTIC_TAG(60000, CBORF_TEXT_STRING("value", "")) + Oracle version and API assumptions From 956b421f1f463f361957078d43e0912969f67163 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 10:10:02 +0200 Subject: [PATCH 28/48] cbor: wrap long enum expressions and raise ARRAY_OF interop limit Satisfy flake8 line length after MajorTypes/AdditionalInfo adoption and allow the cbor2 length-boundary ARRAY_OF corpus above conf.max_list_count. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 103 ++++++++++++++++++----- scapy/cbor/cborfields.py | 28 ++++-- test/scapy/layers/cbor_cbor2_interop.uts | 4 +- 3 files changed, 108 insertions(+), 27 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 228a542bb7d..747cc330dd8 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -99,16 +99,28 @@ def CBOR_encode_head(major_type, value): return chb((major_type << 5) | value) elif value < 256: # 1-byte value follows - return chb((major_type << 5) | int(CBOR_AdditionalInfo.ONE_BYTE)) + chb(value) + return ( + chb((major_type << 5) | int(CBOR_AdditionalInfo.ONE_BYTE)) + + chb(value) + ) elif value < 65536: # 2-byte value follows - return chb((major_type << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + struct.pack(">H", value) + return ( + chb((major_type << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + + struct.pack(">H", value) + ) elif value < 4294967296: # 4-byte value follows - return chb((major_type << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + struct.pack(">I", value) + return ( + chb((major_type << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + + struct.pack(">I", value) + ) else: # 8-byte value follows - return chb((major_type << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + struct.pack(">Q", value) + return ( + chb((major_type << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + + struct.pack(">Q", value) + ) def CBOR_encode_indefinite_head(major_type): @@ -119,7 +131,7 @@ def CBOR_encode_indefinite_head(major_type): int(CBOR_MajorTypes.TEXT_STRING), int(CBOR_MajorTypes.ARRAY), int(CBOR_MajorTypes.MAP), -): + ): raise CBOR_Codec_Encoding_Error( "Indefinite length not allowed for major type %d" % major_type ) @@ -444,18 +456,27 @@ def _cbor_encode_nan(sign, significand52, ai): if ai == int(CBOR_AdditionalInfo.TWO_BYTES): fraction = (significand52 >> 42) & 0x3ff bits = (sign << 15) | (0x1f << 10) | fraction - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + struct.pack(">H", bits) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.TWO_BYTES) + ) + struct.pack(">H", bits) if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): fraction = (significand52 >> 29) & 0x7fffff bits = (sign << 31) | (0xff << 23) | fraction - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + struct.pack(">I", bits) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.FOUR_BYTES) + ) + struct.pack(">I", bits) if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): bits = ( (sign << 63) | (0x7ff << 52) | (significand52 & ((1 << 52) - 1)) ) - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + struct.pack(">Q", bits) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.EIGHT_BYTES) + ) + struct.pack(">Q", bits) raise CBOR_Codec_Encoding_Error("Invalid NaN float AI: %d" % ai) @@ -587,7 +608,11 @@ def _walk(): # Major type 7: simple values and floats. Check float preferred width. if major == 7: - if ai == int(CBOR_AdditionalInfo.ONE_BYTE) and isinstance(value, int) and value < 32: + if ( + ai == int(CBOR_AdditionalInfo.ONE_BYTE) + and isinstance(value, int) + and value < 32 + ): issues.append(( base_offset + start, "Non-shortest CBOR simple value encoding " @@ -1132,7 +1157,10 @@ def enc(cls, obj): text_bytes = text.encode('utf-8') else: text_bytes = bytes(text) - return CBOR_encode_head(int(CBOR_MajorTypes.TEXT_STRING), len(text_bytes)) + text_bytes + return ( + CBOR_encode_head(int(CBOR_MajorTypes.TEXT_STRING), len(text_bytes)) + + text_bytes + ) @classmethod def do_dec(cls, @@ -1388,13 +1416,25 @@ def enc(cls, obj): # Check if obj is a CBOR object instance (for special cases like UNDEFINED) if isinstance(obj, CBOR_UNDEFINED): - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.UNDEFINED)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.UNDEFINED) + ) elif isinstance(obj, CBOR_NULL): - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.NULL)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.NULL) + ) elif isinstance(obj, CBOR_TRUE): - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.TRUE)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.TRUE) + ) elif isinstance(obj, CBOR_FALSE): - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.FALSE)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.FALSE) + ) elif isinstance(obj, CBOR_Object): # For other CBOR objects, use their val attribute val = obj.val @@ -1402,11 +1442,20 @@ def enc(cls, obj): val = obj if val is False: - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.FALSE)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.FALSE) + ) elif val is True: - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.TRUE)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.TRUE) + ) elif val is None: - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.NULL)) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.NULL) + ) elif isinstance(val, float): # Preferred serialization (RFC 8949): shortest float that # preserves the numeric value. Received non-preferred widths are @@ -1415,19 +1464,31 @@ def enc(cls, obj): if ai == int(CBOR_AdditionalInfo.TWO_BYTES): half = _cbor_float_to_half_bits(val) if half is not None: - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + struct.pack(">H", half) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.TWO_BYTES) + ) + struct.pack(">H", half) ai = int(CBOR_AdditionalInfo.FOUR_BYTES) if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): try: - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + struct.pack(">f", val) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.FOUR_BYTES) + ) + struct.pack(">f", val) except (OverflowError, struct.error): pass - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + struct.pack(">d", val) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.EIGHT_BYTES) + ) + struct.pack(">d", val) elif isinstance(val, int) and 0 <= val <= 23: # Simple value 0-23 return CBOR_encode_head(int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), val) elif isinstance(val, int) and 32 <= val <= 255: - return chb((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_AdditionalInfo.ONE_BYTE)) + chb(val) + return chb( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_AdditionalInfo.ONE_BYTE) + ) + chb(val) else: raise CBOR_Codec_Encoding_Error( "Cannot encode value as simple/float: %r" % val) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 27bb07bb2ac..abd6aba31cf 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -926,7 +926,13 @@ def matches_next_item(self, pkt, s): if not s or cbor_is_break(s): return False ai = s[0] & 0x1f - return ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) and ai in (int(CBOR_SimpleValue.FALSE), int(CBOR_SimpleValue.TRUE)) + return ( + ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) + and ai in ( + int(CBOR_SimpleValue.FALSE), + int(CBOR_SimpleValue.TRUE), + ) + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> bool @@ -973,7 +979,10 @@ 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] == ((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.NULL)) + return s[0] == ( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.NULL) + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> None @@ -1028,7 +1037,10 @@ 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] == ((int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) | int(CBOR_SimpleValue.UNDEFINED)) + return s[0] == ( + (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) + | int(CBOR_SimpleValue.UNDEFINED) + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> None @@ -1081,7 +1093,14 @@ def matches_next_item(self, pkt, s): if not s or cbor_is_break(s): return False ai = s[0] & 0x1f - return ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) and ai in (int(CBOR_AdditionalInfo.TWO_BYTES), int(CBOR_AdditionalInfo.FOUR_BYTES), int(CBOR_AdditionalInfo.EIGHT_BYTES)) + return ( + ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) + and ai in ( + int(CBOR_AdditionalInfo.TWO_BYTES), + int(CBOR_AdditionalInfo.FOUR_BYTES), + int(CBOR_AdditionalInfo.EIGHT_BYTES), + ) + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> float @@ -1795,7 +1814,6 @@ def build_result(self, pkt): return CBORBuildResult(data, 1) - class CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): """Per-map storage for unknown text-key extension pairs. diff --git a/test/scapy/layers/cbor_cbor2_interop.uts b/test/scapy/layers/cbor_cbor2_interop.uts index 1c1c863b921..8e87cb443f9 100644 --- a/test/scapy/layers/cbor_cbor2_interop.uts +++ b/test/scapy/layers/cbor_cbor2_interop.uts @@ -412,7 +412,9 @@ class RRCbor2Float(CBOR_Packet): class RRCbor2UIntArray(CBOR_Packet): - CBOR_root = CBORF_ARRAY_OF("values", [], CBORF_UNSIGNED_INTEGER) + CBOR_root = CBORF_ARRAY_OF( + "values", [], CBORF_UNSIGNED_INTEGER, max_count=4096 + ) class RRCbor2TaggedText(CBOR_Packet): From 248b02269bd2d49b47151735deb1889086c92fe7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 11:16:47 +0200 Subject: [PATCH 29/48] cbor: add CBOR_FloatAI for half/single/double encodings Distinguish float additional-info from argument-length codes in NaN and float codec paths. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/__init__.py | 2 + scapy/cbor/cbor.py | 16 +++++-- scapy/cbor/cborcodec.py | 97 +++++++++++++++++++++------------------- scapy/cbor/cborfields.py | 8 ++-- 4 files changed, 69 insertions(+), 54 deletions(-) diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index a12c85c63ec..58c2f1f410c 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -16,6 +16,7 @@ CBOR_MajorTypes, CBOR_AdditionalInfo, CBOR_SimpleValue, + CBOR_FloatAI, CBOR_UINT64_MAX, CBOR_Object, CBOR_UNSIGNED_INTEGER, @@ -88,6 +89,7 @@ "CBOR_MajorTypes", "CBOR_AdditionalInfo", "CBOR_SimpleValue", + "CBOR_FloatAI", "CBOR_UINT64_MAX", # Objects "CBOR_Object", diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 8f17c036218..56395fbf080 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -308,6 +308,14 @@ class CBOR_SimpleValue(metaclass=Enum_metaclass): UNDEFINED = 23 +class CBOR_FloatAI(metaclass=Enum_metaclass): + """Float additional-info codes under major type 7 (RFC 8949 §3.3).""" + name = "CBOR_FLOAT_AI" + HALF = 25 # IEEE binary16 + SINGLE = 26 # IEEE binary32 + DOUBLE = 27 # IEEE binary64 + + CBOR_UINT64_MAX = (1 << 64) - 1 @@ -753,12 +761,12 @@ def enc(self, codec=None): def _cbor_float_key_identity_from_encoded(encoded): # type: (bytes) -> Tuple[Any, ...] - """Map-key identity for a CBOR float encoding (AI 25/26/27).""" + """Map-key identity for a CBOR float encoding (half/single/double).""" wire = bytes(encoded) if not wire: raise ValueError("empty CBOR float encoding") ai = wire[0] & 0x1f - if ai == 25: + if ai == int(CBOR_FloatAI.HALF): if len(wire) < 3: raise ValueError("truncated half float") bits = struct.unpack(">H", wire[1:3])[0] @@ -782,7 +790,7 @@ def _cbor_float_key_identity_from_encoded(encoded): (2 ** (exponent - 15)) ) return _cbor_float_key_identity(float_val) - if ai == 26: + if ai == int(CBOR_FloatAI.SINGLE): if len(wire) < 5: raise ValueError("truncated single float") bits = struct.unpack(">I", wire[1:5])[0] @@ -793,7 +801,7 @@ def _cbor_float_key_identity_from_encoded(encoded): return ("nan", sign, fraction << 29) float_val = struct.unpack(">f", struct.pack(">I", bits))[0] return _cbor_float_key_identity(float_val) - if ai == 27: + if ai == int(CBOR_FloatAI.DOUBLE): if len(wire) < 9: raise ValueError("truncated double float") bits = struct.unpack(">Q", wire[1:9])[0] diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 747cc330dd8..3b331471c79 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -29,6 +29,7 @@ CBOR_Decoding_Error, CBOR_Encoding_Error, CBOR_Error, + CBOR_FloatAI, CBOR_MajorTypes, CBOR_Object, CBOR_SimpleValue, @@ -326,7 +327,7 @@ def cbor_argument_is_shortest(additional_info, value): def _cbor_float_from_bits(ai, bits): # type: (int, int) -> float - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if ai == int(CBOR_FloatAI.HALF): sign = (bits >> 15) & 0x1 exponent = (bits >> 10) & 0x1f fraction = bits & 0x3ff @@ -339,7 +340,7 @@ def _cbor_float_from_bits(ai, bits): float("-inf") if sign else float("inf") ) return ((-1) ** sign) * (1.0 + fraction / 1024.0) * (2 ** (exponent - 15)) - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if ai == int(CBOR_FloatAI.SINGLE): return struct.unpack(">f", struct.pack(">I", bits))[0] return struct.unpack(">d", struct.pack(">Q", bits))[0] @@ -370,9 +371,9 @@ def _cbor_float_to_half_bits(value): 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: + if _cbor_float_from_bits(int(CBOR_FloatAI.HALF), sign | half) != preferred: # Compare absolute then restore sign via copysign on left side - decoded = _cbor_float_from_bits(25, sign | half) + decoded = _cbor_float_from_bits(int(CBOR_FloatAI.HALF), sign | half) if decoded != math.copysign(abs(value), -1.0 if sign else 1.0): return None return sign | half @@ -382,7 +383,7 @@ def _cbor_float_to_half_bits(value): if mant64 & ((1 << 42) - 1): return None bits = sign | (half_exp << 10) | half_mant - decoded = _cbor_float_from_bits(25, bits) + decoded = _cbor_float_from_bits(int(CBOR_FloatAI.HALF), bits) if decoded != math.copysign(abs(value), -1.0 if sign else 1.0): return None return bits @@ -395,27 +396,27 @@ def _cbor_nan_preferred_ai(ai, bits): RFC 8949 prefers a shorter NaN only when zero-padding the shorter significand reconstructs the original NaN payload. """ - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): - return 25 - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if ai == int(CBOR_FloatAI.HALF): + return int(CBOR_FloatAI.HALF) + if ai == int(CBOR_FloatAI.SINGLE): # binary32 NaN: 1+8+23. Prefer half when low 13 significand bits are 0. mant = int(bits) & 0x7FFFFF if mant and (mant & ((1 << 13) - 1)) == 0: - return 25 - return 26 - if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + return int(CBOR_FloatAI.HALF) + return int(CBOR_FloatAI.SINGLE) + if ai == int(CBOR_FloatAI.DOUBLE): # binary64 NaN: 1+11+52. mant = int(bits) & ((1 << 52) - 1) if mant == 0: # Infinity, not NaN — caller should not use this helper. - return 27 + return int(CBOR_FloatAI.DOUBLE) # Prefer half when only the top 10 significand bits are used. if (mant & ((1 << 42) - 1)) == 0: - return 25 + return int(CBOR_FloatAI.HALF) # Prefer single when only the top 23 significand bits are used. if (mant & ((1 << 29) - 1)) == 0: - return 26 - return 27 + return int(CBOR_FloatAI.SINGLE) + return int(CBOR_FloatAI.DOUBLE) return ai @@ -426,21 +427,21 @@ def _cbor_nan_components(ai, bits): The significand is zero-extended to a binary64-width 52-bit field so half / single / double representations of the same NaN share identity. """ - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if ai == int(CBOR_FloatAI.HALF): sign = (int(bits) >> 15) & 0x1 exponent = (int(bits) >> 10) & 0x1f fraction = int(bits) & 0x3ff if exponent != 31 or not fraction: return None return sign, fraction << 42 - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if ai == int(CBOR_FloatAI.SINGLE): sign = (int(bits) >> 31) & 0x1 exponent = (int(bits) >> 23) & 0xff fraction = int(bits) & 0x7fffff if exponent != 0xff or not fraction: return None return sign, fraction << 29 - if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + if ai == int(CBOR_FloatAI.DOUBLE): sign = (int(bits) >> 63) & 0x1 exponent = (int(bits) >> 52) & 0x7ff fraction = int(bits) & ((1 << 52) - 1) @@ -453,21 +454,21 @@ def _cbor_nan_components(ai, bits): def _cbor_encode_nan(sign, significand52, ai): # type: (int, int, int) -> bytes """Encode a NaN at float AI *ai* preserving *sign* and *significand52*.""" - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if ai == int(CBOR_FloatAI.HALF): fraction = (significand52 >> 42) & 0x3ff bits = (sign << 15) | (0x1f << 10) | fraction return chb( (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.TWO_BYTES) + | int(CBOR_FloatAI.HALF) ) + struct.pack(">H", bits) - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if ai == int(CBOR_FloatAI.SINGLE): fraction = (significand52 >> 29) & 0x7fffff bits = (sign << 31) | (0xff << 23) | fraction return chb( (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.FOUR_BYTES) + | int(CBOR_FloatAI.SINGLE) ) + struct.pack(">I", bits) - if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + if ai == int(CBOR_FloatAI.DOUBLE): bits = ( (sign << 63) | (0x7ff << 52) | @@ -475,7 +476,7 @@ def _cbor_encode_nan(sign, significand52, ai): ) return chb( (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.EIGHT_BYTES) + | int(CBOR_FloatAI.DOUBLE) ) + struct.pack(">Q", bits) raise CBOR_Codec_Encoding_Error("Invalid NaN float AI: %d" % ai) @@ -487,15 +488,15 @@ def _cbor_float_bits_from_encoded(encoded): if not wire: raise CBOR_Codec_Encoding_Error("empty CBOR float encoding") ai = wire[0] & 0x1f - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if ai == int(CBOR_FloatAI.HALF): if len(wire) < 3: raise CBOR_Codec_Encoding_Error("truncated half float") return ai, struct.unpack(">H", wire[1:3])[0] - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if ai == int(CBOR_FloatAI.SINGLE): if len(wire) < 5: raise CBOR_Codec_Encoding_Error("truncated single float") return ai, struct.unpack(">I", wire[1:5])[0] - if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + if ai == int(CBOR_FloatAI.DOUBLE): if len(wire) < 9: raise CBOR_Codec_Encoding_Error("truncated double float") return ai, struct.unpack(">Q", wire[1:9])[0] @@ -518,21 +519,21 @@ def _cbor_preferred_nan_encoding(encoded): def _cbor_preferred_float_ai(value): # type: (float) -> int - """Return the preferred float AI (25/26/27) for a numeric *value*.""" + """Return the preferred float AI 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 + return int(CBOR_FloatAI.HALF) if _cbor_float_to_half_bits(value) is not None: - return 25 + return int(CBOR_FloatAI.HALF) try: single = struct.unpack(">f", struct.pack(">f", value))[0] except (OverflowError, struct.error): - return 27 + return int(CBOR_FloatAI.DOUBLE) if single == value or (math.isinf(single) and math.isinf(value)): - return 26 - return 27 + return int(CBOR_FloatAI.SINGLE) + return int(CBOR_FloatAI.DOUBLE) def _cbor_preferred_float_ai_from_encoded(ai, bits): @@ -618,7 +619,11 @@ def _walk(): "Non-shortest CBOR simple value encoding " "(AI=24, value=%d)" % value, )) - if ai in (25, 26, 27) and value is not CBOR_INDEFINITE: + if ai in ( + int(CBOR_FloatAI.HALF), + int(CBOR_FloatAI.SINGLE), + int(CBOR_FloatAI.DOUBLE), + ) 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(( @@ -1461,25 +1466,25 @@ def enc(cls, obj): # 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 == int(CBOR_AdditionalInfo.TWO_BYTES): + if ai == int(CBOR_FloatAI.HALF): half = _cbor_float_to_half_bits(val) if half is not None: return chb( (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.TWO_BYTES) + | int(CBOR_FloatAI.HALF) ) + struct.pack(">H", half) - ai = int(CBOR_AdditionalInfo.FOUR_BYTES) - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + ai = int(CBOR_FloatAI.SINGLE) + if ai == int(CBOR_FloatAI.SINGLE): try: return chb( (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.FOUR_BYTES) + | int(CBOR_FloatAI.SINGLE) ) + struct.pack(">f", val) except (OverflowError, struct.error): pass return chb( (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.EIGHT_BYTES) + | int(CBOR_FloatAI.DOUBLE) ) + struct.pack(">d", val) elif isinstance(val, int) and 0 <= val <= 23: # Simple value 0-23 @@ -1529,14 +1534,14 @@ def do_dec(cls, elif additional_info == int(CBOR_SimpleValue.UNDEFINED): return CBOR_UNDEFINED(), s[1:] elif additional_info in ( - int(CBOR_AdditionalInfo.TWO_BYTES), - int(CBOR_AdditionalInfo.FOUR_BYTES), - int(CBOR_AdditionalInfo.EIGHT_BYTES), + int(CBOR_FloatAI.HALF), + int(CBOR_FloatAI.SINGLE), + int(CBOR_FloatAI.DOUBLE), ): width = { - int(CBOR_AdditionalInfo.TWO_BYTES): 2, - int(CBOR_AdditionalInfo.FOUR_BYTES): 4, - int(CBOR_AdditionalInfo.EIGHT_BYTES): 8, + int(CBOR_FloatAI.HALF): 2, + int(CBOR_FloatAI.SINGLE): 4, + int(CBOR_FloatAI.DOUBLE): 8, }[additional_info] if len(s) < 1 + width: raise CBOR_Codec_Decoding_Error( diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index abd6aba31cf..59effa24094 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -21,7 +21,7 @@ from scapy.cbor.cbor import ( CBOR_Decoding_Error, CBOR_Encoding_Error, - CBOR_AdditionalInfo, + CBOR_FloatAI, CBOR_MajorTypes, CBOR_Object, CBOR_SimpleValue, @@ -1096,9 +1096,9 @@ def matches_next_item(self, pkt, s): return ( ((s[0] >> 5) & 0x7) == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) and ai in ( - int(CBOR_AdditionalInfo.TWO_BYTES), - int(CBOR_AdditionalInfo.FOUR_BYTES), - int(CBOR_AdditionalInfo.EIGHT_BYTES), + int(CBOR_FloatAI.HALF), + int(CBOR_FloatAI.SINGLE), + int(CBOR_FloatAI.DOUBLE), ) ) From 36612b38a0ba23778a6e3433744589a525c09e89 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 11:23:50 +0200 Subject: [PATCH 30/48] cbor: reject duplicate map keys when encoding Prevent CBORF_MAP rebuilds and generic CBORMapData encode from emitting invalid CBOR after unknown-member or pair mutation. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 17 ++++++++ scapy/cbor/cborfields.py | 12 ++++++ test/scapy/layers/cbor.uts | 84 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 3b331471c79..8cd206a2590 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -841,10 +841,26 @@ def encode_cbor_item(item): raise CBOR_Codec_Encoding_Error( "Cannot encode type: %s" % type(item)) + @staticmethod + def _reject_duplicate_map_keys(pairs): + # type: (Any) -> None + """Raise if *pairs* contain CBOR-equivalent duplicate keys.""" + from scapy.cbor.cbor import _cbor_key_norm + seen_norms = set() # type: Set[Any] + for key, _value in pairs: + norm = _cbor_key_norm(key) + if norm in seen_norms: + raise CBOR_Codec_Encoding_Error( + "Duplicate CBOR map key: %r" % (key,) + ) + seen_norms.add(norm) + @staticmethod def _encode_cbor_map_deterministic(pairs): # type: (Any) -> bytes """Encode map pairs in RFC 8949 core-deterministic key order.""" + pairs = list(pairs) + CBORcodec_Object._reject_duplicate_map_keys(pairs) encoded_pairs = [] # type: List[Tuple[bytes, bytes]] for key, value in pairs: key_bytes = CBORcodec_Object.encode_cbor_item_deterministic(key) @@ -1297,6 +1313,7 @@ def enc(cls, obj): pairs = list(mapping.items()) else: pairs = list(mapping) + CBORcodec_Object._reject_duplicate_map_keys(pairs) parts = [CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(pairs))] for key, value in pairs: parts.append(CBORcodec_Object.encode_cbor_item(key)) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 59effa24094..442c700ef36 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1934,6 +1934,7 @@ 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]] + seen = set() # type: set[str] for fld in self.seq: value_result = fld.build_result(pkt) if value_result.items == 0: @@ -1944,8 +1945,19 @@ def build_result(self, pkt): % fld.name ) pairs.append((self._encoded_keys[fld.name], value_result.data)) + seen.add(fld.name) unknown = pkt.getfieldval(self._unknown_field.name) or [] for key, value in unknown: + if not isinstance(key, str): + raise CBOR_Encoding_Error( + "CBOR map unknown key must be a text string, got %r" + % (key,) + ) + if key in seen: + raise CBOR_Encoding_Error( + "Duplicate CBOR map key: %r" % (key,) + ) + seen.add(key) key_bytes = CBORcodec_TEXT_STRING.enc(key) value_bytes = CBORcodec_Object.encode_cbor_item_deterministic(value) pairs.append((key_bytes, value_bytes)) diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index f6fab0f191e..a542718149d 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2103,6 +2103,90 @@ except CBOR_Decoding_Error: else: raise AssertionError("duplicate fixed-map key was silently accepted") + += Fixed-schema maps reject unknown keys that collide with known members on encode +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class VersionMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("version", 1), + ) + +pkt = VersionMap(version=1) +pkt._cbor_unknown = [("version", CBOR_UNSIGNED_INTEGER(42))] +try: + bytes(pkt) + assert False, "encode accepted unknown key colliding with known member" +except CBOR_Encoding_Error: + pass + + += Fixed-schema maps reject duplicate unknown text keys on encode +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class ExtMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + ) + +pkt = ExtMap(a=1) +pkt._cbor_unknown = [ + ("x", CBOR_UNSIGNED_INTEGER(1)), + ("x", CBOR_UNSIGNED_INTEGER(2)), +] +try: + bytes(pkt) + assert False, "encode accepted duplicate unknown map keys" +except CBOR_Encoding_Error: + pass + + += Fixed-schema maps still encode distinct unknown extension keys +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER +from scapy.cbor.cbor import CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class ExtOkMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + ) + +pkt = ExtOkMap(a=1) +pkt._cbor_unknown = [("x", CBOR_UNSIGNED_INTEGER(2))] +assert bytes(pkt) == b"\xa2\x61a\x01\x61x\x02" + + += Generic CBOR maps reject duplicate keys on encode +from scapy.cbor.cbor import CBOR_MAP, CBORMapData, CBOR_UNSIGNED_INTEGER +from scapy.cbor.cborcodec import CBOR_Codec_Encoding_Error, CBORcodec_MAP + +dup = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), 0), + (CBOR_UNSIGNED_INTEGER(1), 1), +]) +try: + CBORcodec_MAP.enc(dup) + assert False, "generic map encode accepted duplicate integer keys" +except CBOR_Codec_Encoding_Error: + pass + +try: + CBOR_MAP(dup).enc() + assert False, "CBOR_MAP.enc accepted duplicate integer keys" +except CBOR_Codec_Encoding_Error: + pass + +ok = CBORMapData([ + (CBOR_UNSIGNED_INTEGER(1), 0), + (CBOR_UNSIGNED_INTEGER(2), 1), +]) +assert CBORcodec_MAP.enc(ok) == b"\xa2\x01\x00\x02\x01" + + + Finding 10 - Indefinite arrays should scale linearly without repeated suffix pre-decodes = Indefinite array span work remains linear in the input size From a8030edcdfc46d40f1576f09c051c7596a8eeff9 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:15:38 +0200 Subject: [PATCH 31/48] cbor: store CBOR_MAP as CBORMapData for key equivalence Normalize constructors and expand map pairs in key norms so list-of-pairs map keys compare unordered like CBORMapData. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 45 +++++++++++++++++++++++++++++--------- scapy/cbor/cborcodec.py | 20 ++++------------- scapy/cbor/cborfields.py | 17 ++++---------- test/scapy/layers/cbor.uts | 38 ++++++++++++++++++++++++++++++++ 4 files changed, 81 insertions(+), 39 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 56395fbf080..054f87aab9a 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -618,24 +618,43 @@ def __repr__(self): return "CBORMapData(%r)" % (self.items(),) +def _cbor_map_pairs(mapping): + # type: (Any) -> List[Tuple[Any, Any]] + """Return ordered ``(key, value)`` pairs from a CBOR map representation. + + Accepts :class:`CBOR_MAP`, :class:`CBORMapData`, ``dict``, or a sequence + of pairs. Used by encode, display, and key-normalization paths. + """ + if isinstance(mapping, CBOR_MAP): + mapping = mapping.val + if isinstance(mapping, CBORMapData): + return mapping.cbor_pairs() + if isinstance(mapping, dict): + return list(mapping.items()) + return list(mapping) + + class CBOR_MAP(CBOR_Object[Any]): """CBOR map (major type 5). - Decoded maps use :class:`CBORMapData` (ordered pairs). Manually - constructed maps may still use a plain ``dict``. + Always stores :class:`CBORMapData`. Constructors accept ``CBORMapData``, + ``dict``, or a sequence of ``(key, value)`` pairs. """ tag = CBOR_MajorTypes.MAP + def __init__(self, val): + # type: (Any) -> None + if isinstance(val, CBORMapData): + super(CBOR_MAP, self).__init__(val) + elif isinstance(val, dict): + super(CBOR_MAP, self).__init__(CBORMapData(list(val.items()))) + else: + super(CBOR_MAP, self).__init__(CBORMapData(list(val))) + def strshow(self, lvl=0): # type: (int) -> str s = (" " * lvl) + ("# CBOR_MAP:") + "\n" - 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: + for k, v in _cbor_map_pairs(self.val): s += (" " * (lvl + 1)) + "Key: " if hasattr(k, 'strshow'): s += k.strshow(0).strip() + "\n" @@ -867,7 +886,13 @@ def _cbor_key_norm(value): if isinstance(value, CBOR_ARRAY): return ("array", tuple(_cbor_key_norm(v) for v in value.val)) if isinstance(value, CBOR_MAP): - return _cbor_key_norm(value.val) + return ( + "map", + frozenset( + (_cbor_key_norm(k), _cbor_key_norm(v)) + for k, v in _cbor_map_pairs(value) + ), + ) if isinstance(value, CBOR_SEMANTIC_TAG): tag_num, inner = value.val return ("tag", int(tag_num), _cbor_key_norm(inner)) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 8cd206a2590..bf28d85338d 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -897,6 +897,7 @@ def encode_cbor_item_deterministic(item): CBOR_SIMPLE_VALUE, CBOR_UNDEFINED, CBORMapData, + _cbor_map_pairs, ) if isinstance(item, CBOR_Object): @@ -913,16 +914,8 @@ def encode_cbor_item_deterministic(item): list(item.val) ) if isinstance(item, CBOR_MAP): - if isinstance(item.val, CBORMapData): - return CBORcodec_Object._encode_cbor_map_deterministic( - item.val.cbor_pairs() - ) - if isinstance(item.val, list): - return CBORcodec_Object._encode_cbor_map_deterministic( - item.val - ) return CBORcodec_Object._encode_cbor_map_deterministic( - list(item.val.items()) + _cbor_map_pairs(item) ) if isinstance(item, CBOR_SEMANTIC_TAG): tag_num, inner = item.val @@ -1305,14 +1298,9 @@ class CBORcodec_MAP(CBORcodec_Object[Any]): @classmethod def enc(cls, obj): # type: (Any) -> bytes - from scapy.cbor.cbor import CBOR_Object, CBORMapData + from scapy.cbor.cbor import CBOR_Object, _cbor_map_pairs mapping = obj.val if isinstance(obj, CBOR_Object) else obj - if isinstance(mapping, CBORMapData): - pairs = mapping.cbor_pairs() - elif isinstance(mapping, dict): - pairs = list(mapping.items()) - else: - pairs = list(mapping) + pairs = _cbor_map_pairs(mapping) CBORcodec_Object._reject_duplicate_map_keys(pairs) parts = [CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(pairs))] for key, value in pairs: diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 442c700ef36..cc1bdad3d36 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -202,13 +202,8 @@ def cbor_object_to_python(obj): if isinstance(obj, CBOR_ARRAY): return [cbor_object_to_python(item) for item in obj.val] if isinstance(obj, CBOR_MAP): - 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()) + from scapy.cbor.cbor import _cbor_map_pairs + pairs = _cbor_map_pairs(obj) return CBORMapData([ (cbor_object_to_python(k), cbor_object_to_python(v)) for k, v in pairs @@ -562,12 +557,8 @@ def _cache_fingerprint(obj): tuple(fingerprint(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, dict): - pairs = list(obj.val.items()) - else: - pairs = list(obj.val) + from scapy.cbor.cbor import _cbor_map_pairs + pairs = _cbor_map_pairs(obj) return ( "map", tuple( diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index a542718149d..744f65d958c 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3641,6 +3641,44 @@ assert typed != CBORMapData([ (CBOR_UNSIGNED_INTEGER(1), "b"), ]) + += CBOR_MAP normalizes constructors to CBORMapData and equates list-of-pairs keys +from scapy.cbor.cbor import ( + CBOR_MAP, + CBOR_TEXT_STRING, + CBOR_UNSIGNED_INTEGER, + CBORMapData, + _cbor_key_equivalent, +) +from scapy.cbor.cborcodec import CBOR_Codec_Encoding_Error, CBORcodec_MAP + +empty = CBOR_MAP({}) +assert isinstance(empty.val, CBORMapData) +assert empty.val == {} + +a = CBOR_TEXT_STRING("a") +b = CBOR_TEXT_STRING("b") +one = CBOR_UNSIGNED_INTEGER(1) +two = CBOR_UNSIGNED_INTEGER(2) +k1 = CBOR_MAP([(a, one), (b, two)]) +k2 = CBOR_MAP([(b, two), (a, one)]) +k3 = CBOR_MAP({"a": 1, "b": 2}) +k4 = CBOR_MAP(CBORMapData([(a, one), (b, two)])) +assert isinstance(k1.val, CBORMapData) +assert _cbor_key_equivalent(k1, k2) +assert _cbor_key_equivalent(k1, k4) +# Native dict keys normalize to the same text/int norms as CBOR objects. +assert _cbor_key_equivalent(k1, k3) + +# Encode must reject reorder-equivalent map-valued keys. +dup = CBORMapData([(k1, 0), (k2, 1)]) +try: + CBORcodec_MAP.enc(dup) + assert False, "encode accepted reorder-equivalent map-valued keys" +except CBOR_Codec_Encoding_Error: + pass + + = CBORMapData.as_dict rejects unhashable array and map keys from scapy.cbor.cbor import ( CBOR_ARRAY, From b04e5fb9679f744bf9f06c5733e0dc0f08900025 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:16:11 +0200 Subject: [PATCH 32/48] cbor: count SEQUENCE items with skip instead of double decode Generalize cbor_count_items for capped and until-break walks so SEQUENCE budgeting avoids throwaway CBOR_Object trees. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 20 ++++++++++++++++---- scapy/cbor/cborfields.py | 19 ++++--------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index bf28d85338d..b6f32615c3f 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -219,17 +219,29 @@ def cbor_skip_item(s): ) -def cbor_count_items_until_break(s): - # type: (Any) -> int - """Count definite top-level items before a break without building objects.""" +def cbor_count_items(s, max_count=None, until_break=False): + # type: (Any, Optional[int], bool) -> int + """Count top-level CBOR items with ``cbor_skip_item`` (no object trees). + + When *until_break* is true, stop at a break byte without consuming it. + When *max_count* is set, stop after that many items even if more remain. + """ rem = s count = 0 - while rem and not cbor_is_break(rem): + while rem and not (until_break and cbor_is_break(rem)): + if max_count is not None and count >= max_count: + break rem = cbor_skip_item(rem) count += 1 return count +def cbor_count_items_until_break(s): + # type: (Any) -> int + """Count definite top-level items before a break without building objects.""" + return cbor_count_items(s, until_break=True) + + def CBOR_decode_head(s): # type: (Any) -> Tuple[int, Union[int, CBOR_INDEFINITE], Any] """ diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index cc1bdad3d36..bfbdf89da8f 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -48,6 +48,7 @@ CBOR_encode_head, CBOR_encode_indefinite_head, CBOR_encode_break, + cbor_count_items, cbor_count_items_until_break, cbor_is_break, cbor_consume_break, @@ -1375,24 +1376,12 @@ 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 + item_count = cbor_count_items( + s, max_count=self.max_items(pkt), until_break=False + ) 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) From 7cd636b457a895c2a5a4c04a13939bd476469428 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:16:57 +0200 Subject: [PATCH 33/48] cbor: restore SEQUENCE build/dissect after skip-count change CBORF_SEQUENCE inherits CBORF_element, so those wrappers are required rather than redundant with CBORF_field. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index bfbdf89da8f..e5ff6717a47 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1382,6 +1382,14 @@ def dissect_result(self, pkt, s): 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) From 8e546cdd4aaef20a0da1559e0a41c660ab575452 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:18:26 +0200 Subject: [PATCH 34/48] cbor: fold SEQUENCE_OF next_cls_cb into homogeneous init Validate packet classes from static pkt_cls and dynamic callbacks through one helper so SEQUENCE_OF no longer bypasses the shared base. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 71 ++++++++++++++++++++-------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index e5ff6717a47..61d5f4f7eaa 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1491,6 +1491,7 @@ def __init__(self, name, # type: str default, # type: Any pkt_cls=None, # type: _ARRAY_T + next_cls_cb=None, # type: Optional[Callable[..., Optional[Type[Packet]]]] # noqa: E501 max_count=None, # type: Optional[int] ): # type: (...) -> None @@ -1499,30 +1500,26 @@ def __init__(self, self.holds_packets = 0 self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] self.max_count = max_count - self._init_element_type(pkt_cls) - super(_CBORF_HOMOGENEOUS, self).__init__(name, default) - - def _init_element_type(self, pkt_cls): - # type: (_ARRAY_T) -> None - 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 + if next_cls_cb is not None: + if pkt_cls is not None: + raise ValueError( + "Pass only next_cls_cb, or only pkt_cls" + ) + self.next_cls_cb = next_cls_cb + self.holds_packets = 1 + elif pkt_cls is None: + raise ValueError("Provide pkt_cls or next_cls_cb") + elif isinstance(pkt_cls, type) and issubclass(pkt_cls, CBORF_field) or \ + isinstance(pkt_cls, CBORF_field): + if isinstance(pkt_cls, type): + self.item_field = pkt_cls("_item", None) # type: ignore else: - self.item_field = chosen + self.item_field = pkt_cls self.holds_packets = 0 - elif ( - isinstance(chosen, type) - and issubclass(chosen, Packet) - and hasattr(chosen, "CBOR_root") - ): - self.cls = cast("Type[CBOR_Packet]", chosen) - self.holds_packets = 1 else: - raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") + self.cls = _require_cbor_packet_cls(pkt_cls) + self.holds_packets = 1 + super(_CBORF_HOMOGENEOUS, self).__init__(name, default) def _list_limit(self): # type: () -> int @@ -1564,6 +1561,7 @@ def _decode_element(self, pkt, s, values=None): ) if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: return CBOR_NO_ITEM, s + pkt_cls = _require_cbor_packet_cls(pkt_cls) item_bytes, remaining = cbor_item_span(s) try: child = pkt_cls(item_bytes, _parent=pkt) # type: ignore @@ -1609,6 +1607,18 @@ def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.name) +def _require_cbor_packet_cls(pkt_cls): + # type: (Any) -> Type[CBOR_Packet] + """Validate a Packet subclass with CBOR_root for collection elements.""" + if ( + isinstance(pkt_cls, type) + and issubclass(pkt_cls, Packet) + and hasattr(pkt_cls, "CBOR_root") + ): + return cast("Type[CBOR_Packet]", pkt_cls) + raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") + + class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): """ Unframed sequence of homogeneous elements (no CBOR array head). @@ -1642,22 +1652,13 @@ def __init__(self, max_count=None, # type: Optional[int] ): # type: (...) -> None - self.next_cls_cb = None # type: Optional[Callable[..., Optional[Type[Packet]]]] self.count_from = count_from - 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.cls = None - self.item_field = None - self.holds_packets = 1 - self.max_count = max_count - CBORF_field.__init__(self, name, default) - return super(CBORF_SEQUENCE_OF, self).__init__( - name, default, pkt_cls=pkt_cls, max_count=max_count + name, + default, + pkt_cls=pkt_cls, + next_cls_cb=next_cls_cb, + max_count=max_count, ) @property From 1b781e1ec0f99abce7f0a1f0085b01944f222d08 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:20:14 +0200 Subject: [PATCH 35/48] cbor: thin CBORF_PACKET and inline single-use helpers Rely on CBORF_field lifecycle for packet fields, move compound build/dissect to the shared base, and fold one-call unwrap/float helpers into their callers. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 109 +++++++++++++++++++-------------------- scapy/cbor/cborcodec.py | 44 +++++++--------- scapy/cbor/cborfields.py | 107 ++++++++++---------------------------- 3 files changed, 98 insertions(+), 162 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 054f87aab9a..21a781910af 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -778,62 +778,6 @@ def enc(self, codec=None): return super(CBOR_FLOAT, self).enc(codec) -def _cbor_float_key_identity_from_encoded(encoded): - # type: (bytes) -> Tuple[Any, ...] - """Map-key identity for a CBOR float encoding (half/single/double).""" - wire = bytes(encoded) - if not wire: - raise ValueError("empty CBOR float encoding") - ai = wire[0] & 0x1f - if ai == int(CBOR_FloatAI.HALF): - if len(wire) < 3: - raise ValueError("truncated half float") - bits = struct.unpack(">H", wire[1:3])[0] - sign = (bits >> 15) & 0x1 - exponent = (bits >> 10) & 0x1f - fraction = bits & 0x3ff - if exponent == 31 and fraction: - # Zero-extend the 10-bit significand to binary64 width. - return ("nan", sign, fraction << 42) - if exponent == 0: - if fraction == 0: - float_val = -0.0 if sign else 0.0 - else: - float_val = ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) - elif exponent == 31: - float_val = float("-inf") if sign else float("inf") - else: - float_val = ( - ((-1) ** sign) * - (1.0 + fraction / 1024.0) * - (2 ** (exponent - 15)) - ) - return _cbor_float_key_identity(float_val) - if ai == int(CBOR_FloatAI.SINGLE): - if len(wire) < 5: - raise ValueError("truncated single float") - bits = struct.unpack(">I", wire[1:5])[0] - sign = (bits >> 31) & 0x1 - exponent = (bits >> 23) & 0xff - fraction = bits & 0x7fffff - if exponent == 0xff and fraction: - return ("nan", sign, fraction << 29) - float_val = struct.unpack(">f", struct.pack(">I", bits))[0] - return _cbor_float_key_identity(float_val) - if ai == int(CBOR_FloatAI.DOUBLE): - if len(wire) < 9: - raise ValueError("truncated double float") - bits = struct.unpack(">Q", wire[1:9])[0] - sign = (bits >> 63) & 0x1 - exponent = (bits >> 52) & 0x7ff - fraction = bits & ((1 << 52) - 1) - if exponent == 0x7ff and fraction: - return ("nan", sign, fraction) - float_val = struct.unpack(">d", struct.pack(">Q", bits))[0] - return _cbor_float_key_identity(float_val) - raise ValueError("not a CBOR float encoding: ai=%d" % ai) - - def _cbor_float_key_identity(value, encoded=None): # type: (float, Optional[bytes]) -> Tuple[Any, ...] """Return RFC 8949 floating-point map-key identity for *value*. @@ -844,7 +788,58 @@ def _cbor_float_key_identity(value, encoded=None): and sign survive Python's NaN canonicalization. """ if encoded is not None: - return _cbor_float_key_identity_from_encoded(encoded) + wire = bytes(encoded) + if not wire: + raise ValueError("empty CBOR float encoding") + ai = wire[0] & 0x1f + if ai == int(CBOR_FloatAI.HALF): + if len(wire) < 3: + raise ValueError("truncated half float") + bits = struct.unpack(">H", wire[1:3])[0] + sign = (bits >> 15) & 0x1 + exponent = (bits >> 10) & 0x1f + fraction = bits & 0x3ff + if exponent == 31 and fraction: + return ("nan", sign, fraction << 42) + if exponent == 0: + if fraction == 0: + float_val = -0.0 if sign else 0.0 + else: + float_val = ( + ((-1) ** sign) * (fraction / 1024.0) * (2 ** -14) + ) + elif exponent == 31: + float_val = float("-inf") if sign else float("inf") + else: + float_val = ( + ((-1) ** sign) * + (1.0 + fraction / 1024.0) * + (2 ** (exponent - 15)) + ) + return _cbor_float_key_identity(float_val) + if ai == int(CBOR_FloatAI.SINGLE): + if len(wire) < 5: + raise ValueError("truncated single float") + bits = struct.unpack(">I", wire[1:5])[0] + sign = (bits >> 31) & 0x1 + exponent = (bits >> 23) & 0xff + fraction = bits & 0x7fffff + if exponent == 0xff and fraction: + return ("nan", sign, fraction << 29) + float_val = struct.unpack(">f", struct.pack(">I", bits))[0] + return _cbor_float_key_identity(float_val) + if ai == int(CBOR_FloatAI.DOUBLE): + if len(wire) < 9: + raise ValueError("truncated double float") + bits = struct.unpack(">Q", wire[1:9])[0] + sign = (bits >> 63) & 0x1 + exponent = (bits >> 52) & 0x7ff + fraction = bits & ((1 << 52) - 1) + if exponent == 0x7ff and fraction: + return ("nan", sign, fraction) + float_val = struct.unpack(">d", struct.pack(">Q", bits))[0] + return _cbor_float_key_identity(float_val) + raise ValueError("not a CBOR float encoding: ai=%d" % ai) fval = float(value) if math.isnan(fval): bits = struct.unpack(">Q", struct.pack(">d", fval))[0] diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index b6f32615c3f..8cd1d7768ce 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -515,20 +515,6 @@ def _cbor_float_bits_from_encoded(encoded): raise CBOR_Codec_Encoding_Error("not a CBOR float encoding: ai=%d" % ai) -def _cbor_preferred_nan_encoding(encoded): - # type: (bytes) -> bytes - """Shortest CBOR float encoding preserving NaN sign and significand.""" - ai, bits = _cbor_float_bits_from_encoded(encoded) - comps = _cbor_nan_components(ai, bits) - if comps is None: - raise CBOR_Codec_Encoding_Error( - "encoded float is not a NaN: %r" % (bytes(encoded),) - ) - sign, significand52 = comps - preferred = _cbor_nan_preferred_ai(ai, bits) - return _cbor_encode_nan(sign, significand52, preferred) - - def _cbor_preferred_float_ai(value): # type: (float) -> int """Return the preferred float AI for a numeric *value*.""" @@ -548,15 +534,6 @@ def _cbor_preferred_float_ai(value): return int(CBOR_FloatAI.DOUBLE) -def _cbor_preferred_float_ai_from_encoded(ai, bits): - # type: (int, int) -> int - """Preferred float AI using the original encoded width and bit pattern.""" - comps = _cbor_nan_components(ai, bits) - if comps is not None: - return _cbor_nan_preferred_ai(ai, bits) - return _cbor_preferred_float_ai(_cbor_float_from_bits(ai, bits)) - - def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): # type: (bytes, bool, int) -> List[Tuple[int, str]] """Scan one top-level CBOR item for non-core-deterministic encodings. @@ -636,8 +613,14 @@ def _walk(): int(CBOR_FloatAI.SINGLE), int(CBOR_FloatAI.DOUBLE), ) and value is not CBOR_INDEFINITE: - preferred = _cbor_preferred_float_ai_from_encoded(ai, int(value)) - if preferred is not None and preferred < ai: + comps = _cbor_nan_components(ai, int(value)) + if comps is not None: + preferred = _cbor_nan_preferred_ai(ai, int(value)) + else: + preferred = _cbor_preferred_float_ai( + _cbor_float_from_bits(ai, int(value)) + ) + if preferred < ai: issues.append(( base_offset + start, "Non-shortest CBOR float encoding (AI=%d, preferred AI=%d)" @@ -918,7 +901,16 @@ def encode_cbor_item_deterministic(item): if isinstance(item, CBOR_FLOAT): encoded = getattr(item, "_encoded", None) if encoded is not None and math.isnan(float(item.val)): - return _cbor_preferred_nan_encoding(encoded) + ai, bits = _cbor_float_bits_from_encoded(encoded) + comps = _cbor_nan_components(ai, bits) + if comps is None: + raise CBOR_Codec_Encoding_Error( + "encoded float is not a NaN: %r" + % (bytes(encoded),) + ) + sign, significand52 = comps + preferred = _cbor_nan_preferred_ai(ai, bits) + return _cbor_encode_nan(sign, significand52, preferred) # Finite floats ignore original width; rebuild preferred form. return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item.val)) if isinstance(item, CBOR_ARRAY): diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 61d5f4f7eaa..0843e62beb9 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -822,17 +822,14 @@ def __init__(self, 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) -> Optional[Type[Packet]] - if self.pkt_cls is not None: - return self.pkt_cls - if self.cls_cb is not None: - return self.cls_cb(pkt, data) - return None - def _decode_packet_value(self, pkt, data): # type: (CBOR_Packet, bytes) -> Packet - pkt_cls = self._resolve_packet_class(pkt, data) + if self.pkt_cls is not None: + pkt_cls = self.pkt_cls + elif self.cls_cb is not None: + pkt_cls = self.cls_cb(pkt, data) + else: + pkt_cls = None if pkt_cls is None: return packet.Raw(data) try: @@ -1241,21 +1238,17 @@ def _mark_absent(self, pkt, field): # Condition false or skipped: leave value untouched. pass - def _unwrap_transparent_cbor_wrapper(self, field): - # type: (Any) -> Any - """Unwrap optional/conditional wrappers that add no CBOR framing.""" - while True: - if isinstance(field, CBORF_optional): - field = field._field - elif isinstance(field, CBORF_CONDITIONAL): - field = field.fld - else: - return field - def _reject_ambiguous_unbounded_sequences(self): # type: () -> None for index, field in enumerate(self.seq): - inner = self._unwrap_transparent_cbor_wrapper(field) + inner = field + while True: + if isinstance(inner, CBORF_optional): + inner = inner._field + elif isinstance(inner, CBORF_CONDITIONAL): + inner = inner.fld + else: + break if not ( isinstance(inner, CBORF_SEQUENCE_OF) and getattr(inner, "is_unbounded", False) @@ -1268,6 +1261,14 @@ def _reject_ambiguous_unbounded_sequences(self): "in the sequence (or provide count_from=)" ) + 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 _dissect_children(self, pkt, s, count): # type: (CBOR_Packet, bytes, Union[int, CBOR_INDEFINITE]) -> bytes remaining = s @@ -1382,14 +1383,6 @@ def dissect_result(self, pkt, s): 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) @@ -1452,14 +1445,6 @@ def dissect_result(self, pkt, s): remaining = self._dissect_children(pkt, remaining, count) return CBORParseResult(remaining=remaining, items=1) - 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 1 @@ -2358,9 +2343,8 @@ def __init__(self, self.cls = pkt_cls super(CBORF_PACKET, self).__init__(name, default) - def _parse_packet_item(self, pkt, s): + def m2i(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 = self.cls(item_bytes, _parent=pkt) # type: ignore @@ -2370,54 +2354,19 @@ def _parse_packet_item(self, pkt, s): raise CBOR_Decoding_Error(str(exc)) return child, remain - def _build_packet_item(self, val): - # type: (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[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"" - return self._build_packet_item(x).data + raise CBOR_Encoding_Error( + "Required field %r is None" % self.name) + return _encode_exactly_one_cbor_item( + x, context="field %r" % self.name + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> CBOR_Packet return cast('CBOR_Packet', _cbor_attach_parent(pkt, x)) - def encode_value(self, x): - # type: (Any) -> bytes - return self._build_packet_item(x).data - - 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(value) - - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult - return self._build_packet_item(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 return packet.fuzz(self.cls()) From d57561445ccab36aaf1e125798b613bea627eb4d Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:22:08 +0200 Subject: [PATCH 36/48] cbor: use MajorTypes and reserved AI in non-det scanner MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finish magic-number cleanup for additional-info 28–30 and scanner major-type checks. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 3 +++ scapy/cbor/cborcodec.py | 35 ++++++++++++++++++++++++++--------- scapy/cbor/cborfields.py | 2 +- 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 21a781910af..015ac2c5162 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -296,6 +296,9 @@ class CBOR_AdditionalInfo(metaclass=Enum_metaclass): TWO_BYTES = 25 FOUR_BYTES = 26 EIGHT_BYTES = 27 + RESERVED_28 = 28 + RESERVED_29 = 29 + RESERVED_30 = 30 INDEFINITE = 31 diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 8cd1d7768ce..9c30d374af9 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -309,7 +309,11 @@ def CBOR_decode_head(s): 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): + elif additional_info in ( + int(CBOR_AdditionalInfo.RESERVED_28), + int(CBOR_AdditionalInfo.RESERVED_29), + int(CBOR_AdditionalInfo.RESERVED_30), + ): raise CBOR_Codec_Decoding_Error( "Reserved additional info: %d" % additional_info, remaining=_cbor_buf_bytes(s)) @@ -591,13 +595,20 @@ def _walk(): pos += 8 elif ai == int(CBOR_AdditionalInfo.INDEFINITE): value = CBOR_INDEFINITE + elif ai in ( + int(CBOR_AdditionalInfo.RESERVED_28), + int(CBOR_AdditionalInfo.RESERVED_29), + int(CBOR_AdditionalInfo.RESERVED_30), + ): + raise CBOR_Codec_Decoding_Error( + "Reserved additional info: %d" % ai, remaining=s[start:]) else: raise CBOR_Codec_Decoding_Error( "Invalid additional info: %d" % ai, remaining=s[start:]) index[0] = pos # Major type 7: simple values and floats. Check float preferred width. - if major == 7: + if major == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): if ( ai == int(CBOR_AdditionalInfo.ONE_BYTE) and isinstance(value, int) @@ -634,7 +645,10 @@ def _walk(): base_offset + start, "Indefinite-length item is not allowed", )) - if major in (2, 3): + if major in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): 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]:]): @@ -642,7 +656,7 @@ def _walk(): "Expected break byte (0xff)", remaining=s[index[0]:]) index[0] += 1 return - if major == 4: + if major == int(CBOR_MajorTypes.ARRAY): 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]:]): @@ -650,7 +664,7 @@ def _walk(): "Expected break byte (0xff)", remaining=s[index[0]:]) index[0] += 1 return - if major == 5: + if major == int(CBOR_MajorTypes.MAP): key_encodings = [] # type: List[bytes] while index[0] < len(s) and not cbor_is_break(s[index[0]:]): key_start = index[0] @@ -679,18 +693,21 @@ def _walk(): % (ai, value), )) - if major in (2, 3): + if major in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): length = int(value) if index[0] + length > len(s): raise CBOR_Codec_Decoding_Error( "Truncated byte/text string", remaining=s[start:]) index[0] += length return - if major == 4: + if major == int(CBOR_MajorTypes.ARRAY): for _ in range(int(value)): _walk() return - if major == 5: + if major == int(CBOR_MajorTypes.MAP): key_encodings = [] # type: List[bytes] for _ in range(int(value)): key_start = index[0] @@ -703,7 +720,7 @@ def _walk(): "CBOR map keys are not in bytewise lexicographic order", )) return - if major == 6: + if major == int(CBOR_MajorTypes.TAG): _walk() return diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 0843e62beb9..7712a1db8fa 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -203,7 +203,7 @@ def cbor_object_to_python(obj): if isinstance(obj, CBOR_ARRAY): return [cbor_object_to_python(item) for item in obj.val] if isinstance(obj, CBOR_MAP): - from scapy.cbor.cbor import _cbor_map_pairs + from scapy.cbor.cbor import CBORMapData, _cbor_map_pairs pairs = _cbor_map_pairs(obj) return CBORMapData([ (cbor_object_to_python(k), cbor_object_to_python(v)) From c333dbb2b077004cc46d0bbfb7905513c26933ef Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 12:50:56 +0200 Subject: [PATCH 37/48] cbor: add CBOR_encode_initial and fold indefinite helpers into fields Centralize initial-byte encoding so major/AI EnumElements need no call-site int casts, and use that API directly for indefinite arrays and break. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 178 +++++++++++++++++---------------------- scapy/cbor/cborfields.py | 35 ++++---- 2 files changed, 93 insertions(+), 120 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 9c30d374af9..2578a21f73e 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -80,15 +80,23 @@ def __init__(self, self.decoded = decoded +def CBOR_encode_initial(major_type, additional_info): + # type: (Any, Any) -> bytes + """Encode a CBOR initial byte (3-bit major type + 5-bit additional info).""" + return chb((int(major_type) << 5) | int(additional_info)) + + def CBOR_encode_head(major_type, value): - # type: (int, int) -> bytes + # type: (Any, int) -> bytes """ - Encode CBOR initial byte and additional info. + Encode CBOR initial byte and additional info for a definite argument. Format: 3 bits major type + 5 bits additional info """ if value is None: raise CBOR_Codec_Encoding_Error( - "Indefinite length requires CBOR_encode_indefinite_head") + "Indefinite length requires CBOR_encode_initial(..., " + "CBOR_AdditionalInfo.INDEFINITE)" + ) if not isinstance(value, int) or isinstance(value, bool): raise CBOR_Codec_Encoding_Error( "CBOR head value must be an integer, got %r" % (value,)) @@ -97,54 +105,33 @@ def CBOR_encode_head(major_type, value): "CBOR head value out of uint64 range: %r" % (value,)) if value < 24: # Value fits in 5 bits - return chb((major_type << 5) | value) + return CBOR_encode_initial(major_type, value) elif value < 256: # 1-byte value follows return ( - chb((major_type << 5) | int(CBOR_AdditionalInfo.ONE_BYTE)) + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.ONE_BYTE) + chb(value) ) elif value < 65536: # 2-byte value follows return ( - chb((major_type << 5) | int(CBOR_AdditionalInfo.TWO_BYTES)) + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.TWO_BYTES) + struct.pack(">H", value) ) elif value < 4294967296: # 4-byte value follows return ( - chb((major_type << 5) | int(CBOR_AdditionalInfo.FOUR_BYTES)) + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.FOUR_BYTES) + struct.pack(">I", value) ) else: # 8-byte value follows return ( - chb((major_type << 5) | int(CBOR_AdditionalInfo.EIGHT_BYTES)) + CBOR_encode_initial(major_type, CBOR_AdditionalInfo.EIGHT_BYTES) + 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 ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), - int(CBOR_MajorTypes.ARRAY), - int(CBOR_MajorTypes.MAP), - ): - raise CBOR_Codec_Encoding_Error( - "Indefinite length not allowed for major type %d" % major_type - ) - return chb((major_type << 5) | int(CBOR_AdditionalInfo.INDEFINITE)) - - -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``.""" @@ -236,12 +223,6 @@ def cbor_count_items(s, max_count=None, until_break=False): return count -def cbor_count_items_until_break(s): - # type: (Any) -> int - """Count definite top-level items before a break without building objects.""" - return cbor_count_items(s, until_break=True) - - def CBOR_decode_head(s): # type: (Any) -> Tuple[int, Union[int, CBOR_INDEFINITE], Any] """ @@ -473,27 +454,30 @@ def _cbor_encode_nan(sign, significand52, ai): if ai == int(CBOR_FloatAI.HALF): fraction = (significand52 >> 42) & 0x3ff bits = (sign << 15) | (0x1f << 10) | fraction - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_FloatAI.HALF) - ) + struct.pack(">H", bits) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.HALF) + + struct.pack(">H", bits) + ) if ai == int(CBOR_FloatAI.SINGLE): fraction = (significand52 >> 29) & 0x7fffff bits = (sign << 31) | (0xff << 23) | fraction - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_FloatAI.SINGLE) - ) + struct.pack(">I", bits) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.SINGLE) + + struct.pack(">I", bits) + ) if ai == int(CBOR_FloatAI.DOUBLE): bits = ( (sign << 63) | (0x7ff << 52) | (significand52 & ((1 << 52) - 1)) ) - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_FloatAI.DOUBLE) - ) + struct.pack(">Q", bits) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.DOUBLE) + + struct.pack(">Q", bits) + ) raise CBOR_Codec_Encoding_Error("Invalid NaN float AI: %d" % ai) @@ -881,7 +865,7 @@ def _encode_cbor_map_deterministic(pairs): ) encoded_pairs.append((key_bytes, value_bytes)) encoded_pairs.sort(key=lambda item: item[0]) - parts = [CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(encoded_pairs))] + parts = [CBOR_encode_head(CBOR_MajorTypes.MAP, len(encoded_pairs))] for key_bytes, value_bytes in encoded_pairs: parts.append(key_bytes) parts.append(value_bytes) @@ -941,7 +925,7 @@ def encode_cbor_item_deterministic(item): if isinstance(item, CBOR_SEMANTIC_TAG): tag_num, inner = item.val return ( - CBOR_encode_head(int(CBOR_MajorTypes.TAG), tag_num) + CBOR_encode_head(CBOR_MajorTypes.TAG, tag_num) + CBORcodec_Object.encode_cbor_item_deterministic(inner) ) if isinstance(item, CBOR_SIMPLE_VALUE): @@ -961,7 +945,7 @@ def encode_cbor_item_deterministic(item): for element in item ] return ( - CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), len(encoded_items)) + CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(encoded_items)) + b"".join(encoded_items) ) if isinstance(item, bool): @@ -1064,7 +1048,7 @@ def enc(cls, obj): if i > CBOR_UINT64_MAX: raise CBOR_Codec_Encoding_Error( "Unsigned integer exceeds uint64 range") - return CBOR_encode_head(int(CBOR_MajorTypes.UNSIGNED_INTEGER), i) + return CBOR_encode_head(CBOR_MajorTypes.UNSIGNED_INTEGER, i) @classmethod def do_dec(cls, @@ -1100,7 +1084,7 @@ def enc(cls, obj): raise CBOR_Codec_Encoding_Error( "Negative integer below CBOR int64 range") # CBOR negative integer: -1 - n - return CBOR_encode_head(int(CBOR_MajorTypes.NEGATIVE_INTEGER), -1 - i) + return CBOR_encode_head(CBOR_MajorTypes.NEGATIVE_INTEGER, -1 - i) @classmethod def do_dec(cls, @@ -1131,7 +1115,7 @@ def enc(cls, obj): data = obj.val if isinstance(obj, CBOR_Object) else obj if not isinstance(data, bytes): data = bytes(data) - return CBOR_encode_head(int(CBOR_MajorTypes.BYTE_STRING), len(data)) + data + return CBOR_encode_head(CBOR_MajorTypes.BYTE_STRING, len(data)) + data @classmethod def do_dec(cls, @@ -1193,7 +1177,7 @@ def enc(cls, obj): else: text_bytes = bytes(text) return ( - CBOR_encode_head(int(CBOR_MajorTypes.TEXT_STRING), len(text_bytes)) + CBOR_encode_head(CBOR_MajorTypes.TEXT_STRING, len(text_bytes)) + text_bytes ) @@ -1261,7 +1245,7 @@ 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 - parts = [CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), len(array))] + parts = [CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(array))] parts.extend( CBORcodec_Object.encode_cbor_item(item) for item in array @@ -1323,7 +1307,7 @@ def enc(cls, obj): mapping = obj.val if isinstance(obj, CBOR_Object) else obj pairs = _cbor_map_pairs(mapping) CBORcodec_Object._reject_duplicate_map_keys(pairs) - parts = [CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(pairs))] + parts = [CBOR_encode_head(CBOR_MajorTypes.MAP, len(pairs))] for key, value in pairs: parts.append(CBORcodec_Object.encode_cbor_item(key)) parts.append(CBORcodec_Object.encode_cbor_item(value)) @@ -1406,7 +1390,7 @@ def enc(cls, obj): raise CBOR_Codec_Encoding_Error( "Semantic tag number out of uint64 range") return ( - CBOR_encode_head(int(CBOR_MajorTypes.TAG), tag_num) + CBOR_encode_head(CBOR_MajorTypes.TAG, tag_num) + CBORcodec_Object.encode_cbor_item(item) ) @@ -1447,25 +1431,17 @@ def enc(cls, obj): # Check if obj is a CBOR object instance (for special cases like UNDEFINED) if isinstance(obj, CBOR_UNDEFINED): - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.UNDEFINED) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.UNDEFINED) elif isinstance(obj, CBOR_NULL): - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.NULL) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.NULL) elif isinstance(obj, CBOR_TRUE): - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.TRUE) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.TRUE) elif isinstance(obj, CBOR_FALSE): - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.FALSE) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.FALSE) elif isinstance(obj, CBOR_Object): # For other CBOR objects, use their val attribute val = obj.val @@ -1473,20 +1449,14 @@ def enc(cls, obj): val = obj if val is False: - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.FALSE) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.FALSE) elif val is True: - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.TRUE) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.TRUE) elif val is None: - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.NULL) - ) + return CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.NULL) elif isinstance(val, float): # Preferred serialization (RFC 8949): shortest float that # preserves the numeric value. Received non-preferred widths are @@ -1495,31 +1465,35 @@ def enc(cls, obj): if ai == int(CBOR_FloatAI.HALF): half = _cbor_float_to_half_bits(val) if half is not None: - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_FloatAI.HALF) - ) + struct.pack(">H", half) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.HALF) + + struct.pack(">H", half) + ) ai = int(CBOR_FloatAI.SINGLE) if ai == int(CBOR_FloatAI.SINGLE): try: - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_FloatAI.SINGLE) - ) + struct.pack(">f", val) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.SINGLE) + + struct.pack(">f", val) + ) except (OverflowError, struct.error): pass - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_FloatAI.DOUBLE) - ) + struct.pack(">d", val) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.DOUBLE) + + struct.pack(">d", val) + ) elif isinstance(val, int) and 0 <= val <= 23: # Simple value 0-23 - return CBOR_encode_head(int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), val) + return CBOR_encode_head(CBOR_MajorTypes.SIMPLE_AND_FLOAT, val) elif isinstance(val, int) and 32 <= val <= 255: - return chb( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_AdditionalInfo.ONE_BYTE) - ) + chb(val) + return ( + CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_AdditionalInfo.ONE_BYTE) + + chb(val) + ) else: raise CBOR_Codec_Encoding_Error( "Cannot encode value as simple/float: %r" % val) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 7712a1db8fa..dc9f3d8393c 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -19,6 +19,7 @@ from dataclasses import dataclass from scapy.cbor.cbor import ( + CBOR_AdditionalInfo, CBOR_Decoding_Error, CBOR_Encoding_Error, CBOR_FloatAI, @@ -46,10 +47,8 @@ CBOR_INDEFINITE, CBOR_decode_head, CBOR_encode_head, - CBOR_encode_indefinite_head, - CBOR_encode_break, + CBOR_encode_initial, cbor_count_items, - cbor_count_items_until_break, cbor_is_break, cbor_consume_break, CBORcodec_Object, @@ -968,10 +967,9 @@ 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] == ( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.NULL) - ) + return s[0] == CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.NULL + )[0] def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> None @@ -1026,10 +1024,9 @@ 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] == ( - (int(CBOR_MajorTypes.SIMPLE_AND_FLOAT) << 5) - | int(CBOR_SimpleValue.UNDEFINED) - ) + return s[0] == CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, CBOR_SimpleValue.UNDEFINED + )[0] def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> None @@ -1275,7 +1272,7 @@ def _dissect_children(self, pkt, s, count): if count is CBOR_INDEFINITE: # Lightweight head/span walk — avoid building CBOR_Object trees # just to learn the item budget before the schema pass. - item_count = cbor_count_items_until_break(remaining) + item_count = cbor_count_items(remaining, until_break=True) remaining = self._dissect_children_budgeted( pkt, remaining, item_count ) @@ -1424,12 +1421,14 @@ def build_result(self, pkt): items_data, total_items = self._build_children(pkt) if self.encode_indefinite: data = ( - CBOR_encode_indefinite_head(int(CBOR_MajorTypes.ARRAY)) + + CBOR_encode_initial( + CBOR_MajorTypes.ARRAY, CBOR_AdditionalInfo.INDEFINITE + ) + items_data + - CBOR_encode_break() + b'\xff' ) else: - data = CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), total_items) + data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, total_items) data += items_data return CBORBuildResult(data, 1) @@ -1783,7 +1782,7 @@ def build_result(self, pkt): raise CBOR_Encoding_Error( "Required collection field %r is None" % self.name) parts = [self._encode_element(pkt, item) for item in val] - data = CBOR_encode_head(int(CBOR_MajorTypes.ARRAY), len(val)) + data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(val)) data += b"".join(parts) return CBORBuildResult(data, 1) @@ -1940,7 +1939,7 @@ def build_result(self, pkt): for key_bytes, value_bytes in pairs: parts.append(key_bytes) parts.append(value_bytes) - data = CBOR_encode_head(int(CBOR_MajorTypes.MAP), len(pairs)) + b"".join(parts) + data = CBOR_encode_head(CBOR_MajorTypes.MAP, len(pairs)) + b"".join(parts) return CBORBuildResult(data, 1) def dissect_result(self, pkt, s): @@ -2122,7 +2121,7 @@ def _parse_tag_head(self, s, require_match=True): def _encode_tagged(self, inner_data): # type: (bytes) -> bytes - return CBOR_encode_head(int(CBOR_MajorTypes.TAG), self.tag_num) + inner_data + return CBOR_encode_head(CBOR_MajorTypes.TAG, self.tag_num) + inner_data def matches_next_item(self, pkt, s): # type: (CBOR_Packet, bytes) -> bool From 47d858cf89260b2b3b23152056958779b7bb1a40 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 13:01:55 +0200 Subject: [PATCH 38/48] cbor: bound skip/count nesting and finish scanner cleanup Enforce MAX_CBOR_NESTING in the lightweight pre-scan (nested under cbor_count_items, via memoryview), cap indefinite ARRAY counts by schema budget, and move class-owned helpers off the module surface. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 119 +++++++++++++++++---------------- scapy/cbor/cborfields.py | 132 ++++++++++++++++++++----------------- test/scapy/layers/cbor.uts | 48 ++++++++++++++ 3 files changed, 182 insertions(+), 117 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 2578a21f73e..5a90b88cf2e 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -103,7 +103,7 @@ def CBOR_encode_head(major_type, value): if value < 0 or value > CBOR_UINT64_MAX: raise CBOR_Codec_Encoding_Error( "CBOR head value out of uint64 range: %r" % (value,)) - if value < 24: + if value < int(CBOR_AdditionalInfo.ONE_BYTE): # Value fits in 5 bits return CBOR_encode_initial(major_type, value) elif value < 256: @@ -157,68 +157,71 @@ def cbor_consume_break(s): return s[1:] -def cbor_skip_item(s): - # type: (Any) -> Any - """Advance past one well-formed CBOR item without building objects.""" - major_type, value, rem = CBOR_decode_head(s) - if major_type in ( - int(CBOR_MajorTypes.UNSIGNED_INTEGER), - int(CBOR_MajorTypes.NEGATIVE_INTEGER), - int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), - ): - return rem - if major_type in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), - ): - if value is CBOR_INDEFINITE: - while rem and not cbor_is_break(rem): - rem = cbor_skip_item(rem) - return cbor_consume_break(rem) - length = int(value) - if len(rem) < length: - raise CBOR_Codec_Decoding_Error( - "Truncated byte/text string", remaining=_cbor_buf_bytes(s)) - return rem[length:] - if major_type == int(CBOR_MajorTypes.ARRAY): - if value is CBOR_INDEFINITE: - while rem and not cbor_is_break(rem): - rem = cbor_skip_item(rem) - return cbor_consume_break(rem) - for _ in range(int(value)): - rem = cbor_skip_item(rem) - return rem - if major_type == int(CBOR_MajorTypes.MAP): - if value is CBOR_INDEFINITE: - while rem and not cbor_is_break(rem): - rem = cbor_skip_item(rem) - rem = cbor_skip_item(rem) - return cbor_consume_break(rem) - for _ in range(int(value)): - rem = cbor_skip_item(rem) - rem = cbor_skip_item(rem) - return rem - if major_type == int(CBOR_MajorTypes.TAG): - return cbor_skip_item(rem) - raise CBOR_Codec_Decoding_Error( - "Invalid major type: %d" % major_type, - remaining=_cbor_buf_bytes(s), - ) - - def cbor_count_items(s, max_count=None, until_break=False): # type: (Any, Optional[int], bool) -> int - """Count top-level CBOR items with ``cbor_skip_item`` (no object trees). + """Count top-level CBOR items without building object trees. When *until_break* is true, stop at a break byte without consuming it. When *max_count* is set, stop after that many items even if more remain. """ - rem = s + def _skip_item(rem, depth=0): + # type: (Any, int) -> Any + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=_cbor_buf_bytes(rem)) + major_type, value, rem = CBOR_decode_head(rem) + if major_type in ( + int(CBOR_MajorTypes.UNSIGNED_INTEGER), + int(CBOR_MajorTypes.NEGATIVE_INTEGER), + int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), + ): + return rem + if major_type in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _skip_item(rem, depth + 1) + return cbor_consume_break(rem) + length = int(value) + if len(rem) < length: + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string", + remaining=_cbor_buf_bytes(rem)) + return rem[length:] + if major_type == int(CBOR_MajorTypes.ARRAY): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _skip_item(rem, depth + 1) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = _skip_item(rem, depth + 1) + return rem + if major_type == int(CBOR_MajorTypes.MAP): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _skip_item(rem, depth + 1) + rem = _skip_item(rem, depth + 1) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = _skip_item(rem, depth + 1) + rem = _skip_item(rem, depth + 1) + return rem + if major_type == int(CBOR_MajorTypes.TAG): + return _skip_item(rem, depth + 1) + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(rem), + ) + + rem = s if isinstance(s, memoryview) else memoryview(s) count = 0 while rem and not (until_break and cbor_is_break(rem)): if max_count is not None and count >= max_count: break - rem = cbor_skip_item(rem) + rem = _skip_item(rem) count += 1 return count @@ -237,7 +240,7 @@ def CBOR_decode_head(s): major_type = initial_byte >> 5 additional_info = initial_byte & 0x1f - if additional_info < 24: + if additional_info < int(CBOR_AdditionalInfo.ONE_BYTE): # Value is in the additional info return major_type, additional_info, s[1:] elif additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): @@ -309,10 +312,10 @@ def cbor_argument_is_shortest(additional_info, value): """Return True when *additional_info* is the shortest encoding for *value*.""" if value is CBOR_INDEFINITE: return additional_info == int(CBOR_AdditionalInfo.INDEFINITE) - if additional_info < 24: + if additional_info < int(CBOR_AdditionalInfo.ONE_BYTE): return True if additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): - return value >= 24 + return value >= int(CBOR_AdditionalInfo.ONE_BYTE) if additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): return value >= 256 if additional_info == int(CBOR_AdditionalInfo.FOUR_BYTES): @@ -551,7 +554,7 @@ def _walk(): major = initial >> 5 ai = initial & 0x1f pos = start + 1 - if ai < 24: + if ai < int(CBOR_AdditionalInfo.ONE_BYTE): value = ai # type: Union[int, CBOR_INDEFINITE] elif ai == int(CBOR_AdditionalInfo.ONE_BYTE): if pos + 1 > len(s): @@ -1551,7 +1554,7 @@ def do_dec(cls, float_val = _cbor_float_from_bits(additional_info, bits) encoded = _cbor_buf_bytes(s[:1 + width]) return CBOR_FLOAT(float_val, encoded=encoded), s[1 + width:] - elif additional_info < 24: + elif additional_info < int(CBOR_AdditionalInfo.ONE_BYTE): # Simple value 0-23 return CBOR_SIMPLE_VALUE(additional_info), s[1:] else: diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index dc9f3d8393c..0203159e2f2 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -188,31 +188,6 @@ def _cbor_attach_parent(parent, child): return child -def cbor_object_to_python(obj): - # type: (Any) -> Any - """Convert a :class:`CBOR_Object` tree to native Python values. - - Prefer keeping :class:`CBOR_Object` for arbitrary CBOR (``CBORF_ANY``). - Tags, simples, and undefined stay as ``CBOR_Object`` instances. - """ - if not isinstance(obj, CBOR_Object): - return obj - if isinstance(obj, (CBOR_UNDEFINED, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE)): - return obj - if isinstance(obj, CBOR_ARRAY): - return [cbor_object_to_python(item) for item in obj.val] - if isinstance(obj, CBOR_MAP): - from scapy.cbor.cbor import CBORMapData, _cbor_map_pairs - pairs = _cbor_map_pairs(obj) - return CBORMapData([ - (cbor_object_to_python(k), cbor_object_to_python(v)) - for k, v in pairs - ]) - if isinstance(obj, CBOR_FLOAT): - return float(obj.val) - return obj.val - - class CBORF_element(object): """Base class for CBOR packet field elements.""" @@ -325,6 +300,32 @@ def i2m(self, pkt, x): # Absent/optional skipping is handled in build_result(). return self.encode_value(x) + @staticmethod + def _object_to_python(obj): + # type: (Any) -> Any + """Convert a :class:`CBOR_Object` tree to native Python values. + + Prefer keeping :class:`CBOR_Object` for arbitrary CBOR (``CBORF_ANY``). + Tags, simples, and undefined stay as ``CBOR_Object`` instances. + """ + if not isinstance(obj, CBOR_Object): + return obj + if isinstance(obj, (CBOR_UNDEFINED, CBOR_SEMANTIC_TAG, CBOR_SIMPLE_VALUE)): + return obj + if isinstance(obj, CBOR_ARRAY): + return [CBORF_field._object_to_python(item) for item in obj.val] + if isinstance(obj, CBOR_MAP): + from scapy.cbor.cbor import CBORMapData, _cbor_map_pairs + pairs = _cbor_map_pairs(obj) + return CBORMapData([ + (CBORF_field._object_to_python(k), + CBORF_field._object_to_python(v)) + for k, v in pairs + ]) + if isinstance(obj, CBOR_FLOAT): + return float(obj.val) + return obj.val + def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> _I if x is CBOR_ABSENT or x is CBOR_NO_ITEM: @@ -332,7 +333,7 @@ def any2i(self, pkt, x): if isinstance(x, CBOR_UNDEFINED): return cast(_I, x) if isinstance(x, CBOR_Object): - x = cbor_object_to_python(x) + x = self._object_to_python(x) return self.h2i(pkt, x) def build_result(self, pkt): @@ -1097,7 +1098,7 @@ def any2i(self, pkt, x): if isinstance(x, CBOR_FLOAT): return float(x.val) if isinstance(x, CBOR_Object): - return float(cbor_object_to_python(x)) + return float(self._object_to_python(x)) return float(x) def m2i(self, pkt, s): @@ -1266,20 +1267,6 @@ def dissect(self, pkt, s): # type: (CBOR_Packet, bytes) -> bytes return self.dissect_result(pkt, s).remaining - def _dissect_children(self, pkt, s, count): - # type: (CBOR_Packet, bytes, Union[int, CBOR_INDEFINITE]) -> bytes - remaining = s - if count is CBOR_INDEFINITE: - # Lightweight head/span walk — avoid building CBOR_Object trees - # just to learn the item budget before the schema pass. - item_count = cbor_count_items(remaining, until_break=True) - remaining = self._dissect_children_budgeted( - pkt, remaining, item_count - ) - return cbor_consume_break(remaining) - - return self._dissect_children_budgeted(pkt, remaining, count) - def _dissect_children_budgeted(self, pkt, s, count): # type: (CBOR_Packet, bytes, int) -> bytes remaining = s @@ -1374,9 +1361,12 @@ 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. - item_count = cbor_count_items( - s, max_count=self.max_items(pkt), until_break=False - ) + try: + item_count = cbor_count_items( + s, max_count=self.max_items(pkt), until_break=False + ) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) remaining = self._dissect_children_budgeted(pkt, s, item_count) return CBORParseResult(remaining=remaining, items=item_count) @@ -1441,7 +1431,31 @@ def dissect_result(self, pkt, s): if major_type != int(CBOR_MajorTypes.ARRAY): raise CBOR_Type_Mismatch( "Expected major type 4 (array), got %d" % major_type) - remaining = self._dissect_children(pkt, remaining, count) + if count is CBOR_INDEFINITE: + # Lightweight head/span walk — avoid building CBOR_Object trees + # just to learn the item budget before the schema pass. + child_max = sum(f.max_items(pkt) for f in self.seq) + try: + item_count = cbor_count_items( + remaining, + max_count=child_max + 1, + until_break=True, + ) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + if item_count > child_max: + raise CBOR_Decoding_Error("CBOR item count mismatch") + remaining = self._dissect_children_budgeted( + pkt, remaining, item_count + ) + try: + remaining = cbor_consume_break(remaining) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) + else: + remaining = self._dissect_children_budgeted( + pkt, remaining, count + ) return CBORParseResult(remaining=remaining, items=1) def min_items(self, pkt): @@ -1501,10 +1515,22 @@ def __init__(self, self.item_field = pkt_cls self.holds_packets = 0 else: - self.cls = _require_cbor_packet_cls(pkt_cls) + self.cls = self._require_packet_cls(pkt_cls) self.holds_packets = 1 super(_CBORF_HOMOGENEOUS, self).__init__(name, default) + @staticmethod + def _require_packet_cls(pkt_cls): + # type: (Any) -> Type[CBOR_Packet] + """Validate a Packet subclass with CBOR_root for collection elements.""" + if ( + isinstance(pkt_cls, type) + and issubclass(pkt_cls, Packet) + and hasattr(pkt_cls, "CBOR_root") + ): + return cast("Type[CBOR_Packet]", pkt_cls) + raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") + def _list_limit(self): # type: () -> int if self.max_count is not None: @@ -1545,7 +1571,7 @@ def _decode_element(self, pkt, s, values=None): ) if pkt_cls is CBOR_NO_ITEM or pkt_cls is None: return CBOR_NO_ITEM, s - pkt_cls = _require_cbor_packet_cls(pkt_cls) + pkt_cls = self._require_packet_cls(pkt_cls) item_bytes, remaining = cbor_item_span(s) try: child = pkt_cls(item_bytes, _parent=pkt) # type: ignore @@ -1591,18 +1617,6 @@ def __repr__(self): return "<%s %s>" % (self.__class__.__name__, self.name) -def _require_cbor_packet_cls(pkt_cls): - # type: (Any) -> Type[CBOR_Packet] - """Validate a Packet subclass with CBOR_root for collection elements.""" - if ( - isinstance(pkt_cls, type) - and issubclass(pkt_cls, Packet) - and hasattr(pkt_cls, "CBOR_root") - ): - return cast("Type[CBOR_Packet]", pkt_cls) - raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") - - class CBORF_SEQUENCE_OF(_CBORF_HOMOGENEOUS): """ Unframed sequence of homogeneous elements (no CBOR array head). diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 744f65d958c..65b88c49b9c 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3361,6 +3361,54 @@ assert isinstance(only_recursive, CBOR_UNSIGNED_INTEGER) simple = RandCBORObject(objlist=[CBOR_TEXT_STRING, CBOR_NULL])._fix() assert bytes(simple) += Lightweight count path rejects nesting beyond MAX_CBOR_NESTING +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + MAX_CBOR_NESTING, + cbor_count_items, +) +from scapy.cbor.cborfields import ( + CBORF_ARRAY_INDEFINITE, + CBORF_SEQUENCE, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cborpacket import CBOR_Packet + +# Nested arrays: 0x81 (array of 1) repeated, then unsigned 0. +too_deep = b"\x81" * (MAX_CBOR_NESTING + 1) + b"\x00" +try: + cbor_count_items(too_deep) + assert False, "expected nesting-depth error" +except CBOR_Codec_Decoding_Error as err: + assert "nesting" in str(err).lower() +except RecursionError: + assert False, "pre-scan leaked RecursionError" + +class DeepSeq(CBOR_Packet): + CBOR_root = CBORF_SEQUENCE(CBORF_UNSIGNED_INTEGER("n", 0)) + +try: + DeepSeq(too_deep) + assert False, "SEQUENCE accepted over-nested input" +except Exception as err: + assert not isinstance(err, RecursionError) + assert "nesting" in str(err).lower() + +# Indefinite array pre-scan uses the same depth-limited skip path. +indef_too_deep = b"\x9f" + too_deep + b"\xff" + +class DeepIndef(CBOR_Packet): + CBOR_root = CBORF_ARRAY_INDEFINITE( + CBORF_UNSIGNED_INTEGER("n", 0), + ) + +try: + DeepIndef(indef_too_deep) + assert False, "indefinite ARRAY accepted over-nested input" +except Exception as err: + assert not isinstance(err, RecursionError) + assert "nesting" in str(err).lower() + = CBOR object display helpers and decoding-error repr import copy from scapy.cbor.cbor import ( From dc812a33471f7dab20e17c51ddbe0841c9a5c739 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 13:45:25 +0200 Subject: [PATCH 39/48] cbor: share structural skipper and harden indefinite string walks Extract _cbor_skip_item for count and item-span paths with codec-matching chunk rules, apply the same depth/chunk checks to the non-det walker, and fold remaining single-use helpers plus map-equality duplication. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cbor.py | 58 +++--- scapy/cbor/cborcodec.py | 349 ++++++++++++++++++++++--------------- scapy/cbor/cborfields.py | 46 ++--- test/scapy/layers/cbor.uts | 40 +++++ 4 files changed, 290 insertions(+), 203 deletions(-) diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index 015ac2c5162..cd379cdbdca 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -575,46 +575,30 @@ def __eq__(self, other): 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: - matched = False - for idx, (other_key, other_value) in enumerate(other_items): - if used[idx]: - continue - if not _cbor_key_equivalent(map_key, other_key): - continue - if value != other_value: - return False - used[idx] = True - matched = True - break - if not matched: - return False - return True - if isinstance(other, CBORMapData): + elif isinstance(other, CBORMapData): # RFC 8949 maps are unordered; pair order is not identity. - if len(self._pairs) != len(other._pairs): - return False - used = [False] * len(other._pairs) - for map_key, value in self._pairs: - matched = False - for idx, (other_key, other_value) in enumerate(other._pairs): - if used[idx]: - continue - if not _cbor_key_equivalent(map_key, other_key): - continue - if value != other_value: - return False - used[idx] = True - matched = True - break - if not matched: + other_items = other._pairs + else: + return NotImplemented + if len(self._pairs) != len(other_items): + return False + used = [False] * len(other_items) + for map_key, value in self._pairs: + matched = False + for idx, (other_key, other_value) in enumerate(other_items): + if used[idx]: + continue + if not _cbor_key_equivalent(map_key, other_key): + continue + if value != other_value: return False - return True - return NotImplemented + used[idx] = True + matched = True + break + if not matched: + return False + return True def __repr__(self): # type: () -> str diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 5a90b88cf2e..fe06e7de661 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -132,6 +132,9 @@ def CBOR_encode_head(major_type, value): ) +CBOR_BREAK_BYTE = 0xFF + + def _cbor_buf_bytes(buf): # type: (Any) -> bytes """Materialize a bytes/memoryview slice as ``bytes``.""" @@ -145,7 +148,7 @@ def _cbor_buf_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 + return bool(s) and s[0] == CBOR_BREAK_BYTE def cbor_consume_break(s): @@ -157,6 +160,76 @@ def cbor_consume_break(s): return s[1:] +def _cbor_skip_item(s, depth=0): + # type: (Any, int) -> Any + """Advance past one well-formed CBOR item without building objects.""" + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=_cbor_buf_bytes(s)) + major_type, value, rem = CBOR_decode_head(s) + if major_type in ( + int(CBOR_MajorTypes.UNSIGNED_INTEGER), + int(CBOR_MajorTypes.NEGATIVE_INTEGER), + int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), + ): + return rem + if major_type in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): + if value is CBOR_INDEFINITE: + expected_type = major_type + while rem and not cbor_is_break(rem): + chunk_type, chunk_len, rem = CBOR_decode_head(rem) + if chunk_type != expected_type: + raise CBOR_Codec_Decoding_Error( + "Indefinite string chunk must be major type %d, " + "got %d" % (expected_type, chunk_type), + remaining=_cbor_buf_bytes(rem)) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite string", + remaining=_cbor_buf_bytes(rem)) + length = int(chunk_len) + if len(rem) < length: + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string chunk", + remaining=_cbor_buf_bytes(rem)) + rem = rem[length:] + return cbor_consume_break(rem) + length = int(value) + if len(rem) < length: + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string", + remaining=_cbor_buf_bytes(rem)) + return rem[length:] + if major_type == int(CBOR_MajorTypes.ARRAY): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _cbor_skip_item(rem, depth + 1) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = _cbor_skip_item(rem, depth + 1) + return rem + if major_type == int(CBOR_MajorTypes.MAP): + if value is CBOR_INDEFINITE: + while rem and not cbor_is_break(rem): + rem = _cbor_skip_item(rem, depth + 1) + rem = _cbor_skip_item(rem, depth + 1) + return cbor_consume_break(rem) + for _ in range(int(value)): + rem = _cbor_skip_item(rem, depth + 1) + rem = _cbor_skip_item(rem, depth + 1) + return rem + if major_type == int(CBOR_MajorTypes.TAG): + return _cbor_skip_item(rem, depth + 1) + raise CBOR_Codec_Decoding_Error( + "Invalid major type: %d" % major_type, + remaining=_cbor_buf_bytes(rem), + ) + + def cbor_count_items(s, max_count=None, until_break=False): # type: (Any, Optional[int], bool) -> int """Count top-level CBOR items without building object trees. @@ -164,64 +237,12 @@ def cbor_count_items(s, max_count=None, until_break=False): When *until_break* is true, stop at a break byte without consuming it. When *max_count* is set, stop after that many items even if more remain. """ - def _skip_item(rem, depth=0): - # type: (Any, int) -> Any - if depth > MAX_CBOR_NESTING: - raise CBOR_Codec_Decoding_Error( - "Maximum CBOR nesting depth exceeded", - remaining=_cbor_buf_bytes(rem)) - major_type, value, rem = CBOR_decode_head(rem) - if major_type in ( - int(CBOR_MajorTypes.UNSIGNED_INTEGER), - int(CBOR_MajorTypes.NEGATIVE_INTEGER), - int(CBOR_MajorTypes.SIMPLE_AND_FLOAT), - ): - return rem - if major_type in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), - ): - if value is CBOR_INDEFINITE: - while rem and not cbor_is_break(rem): - rem = _skip_item(rem, depth + 1) - return cbor_consume_break(rem) - length = int(value) - if len(rem) < length: - raise CBOR_Codec_Decoding_Error( - "Truncated byte/text string", - remaining=_cbor_buf_bytes(rem)) - return rem[length:] - if major_type == int(CBOR_MajorTypes.ARRAY): - if value is CBOR_INDEFINITE: - while rem and not cbor_is_break(rem): - rem = _skip_item(rem, depth + 1) - return cbor_consume_break(rem) - for _ in range(int(value)): - rem = _skip_item(rem, depth + 1) - return rem - if major_type == int(CBOR_MajorTypes.MAP): - if value is CBOR_INDEFINITE: - while rem and not cbor_is_break(rem): - rem = _skip_item(rem, depth + 1) - rem = _skip_item(rem, depth + 1) - return cbor_consume_break(rem) - for _ in range(int(value)): - rem = _skip_item(rem, depth + 1) - rem = _skip_item(rem, depth + 1) - return rem - if major_type == int(CBOR_MajorTypes.TAG): - return _skip_item(rem, depth + 1) - raise CBOR_Codec_Decoding_Error( - "Invalid major type: %d" % major_type, - remaining=_cbor_buf_bytes(rem), - ) - rem = s if isinstance(s, memoryview) else memoryview(s) count = 0 while rem and not (until_break and cbor_is_break(rem)): if max_count is not None and count >= max_count: break - rem = _skip_item(rem) + rem = _cbor_skip_item(rem) count += 1 return count @@ -307,24 +328,6 @@ def CBOR_decode_head(s): 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 == int(CBOR_AdditionalInfo.INDEFINITE) - if additional_info < int(CBOR_AdditionalInfo.ONE_BYTE): - return True - if additional_info == int(CBOR_AdditionalInfo.ONE_BYTE): - return value >= int(CBOR_AdditionalInfo.ONE_BYTE) - if additional_info == int(CBOR_AdditionalInfo.TWO_BYTES): - return value >= 256 - if additional_info == int(CBOR_AdditionalInfo.FOUR_BYTES): - return value >= 65536 - if additional_info == int(CBOR_AdditionalInfo.EIGHT_BYTES): - return value >= (1 << 32) - return additional_info == int(CBOR_AdditionalInfo.INDEFINITE) - - def _cbor_float_from_bits(ai, bits): # type: (int, int) -> float if ai == int(CBOR_FloatAI.HALF): @@ -451,61 +454,6 @@ def _cbor_nan_components(ai, bits): return None -def _cbor_encode_nan(sign, significand52, ai): - # type: (int, int, int) -> bytes - """Encode a NaN at float AI *ai* preserving *sign* and *significand52*.""" - if ai == int(CBOR_FloatAI.HALF): - fraction = (significand52 >> 42) & 0x3ff - bits = (sign << 15) | (0x1f << 10) | fraction - return ( - CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, - CBOR_FloatAI.HALF) - + struct.pack(">H", bits) - ) - if ai == int(CBOR_FloatAI.SINGLE): - fraction = (significand52 >> 29) & 0x7fffff - bits = (sign << 31) | (0xff << 23) | fraction - return ( - CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, - CBOR_FloatAI.SINGLE) - + struct.pack(">I", bits) - ) - if ai == int(CBOR_FloatAI.DOUBLE): - bits = ( - (sign << 63) | - (0x7ff << 52) | - (significand52 & ((1 << 52) - 1)) - ) - return ( - CBOR_encode_initial(CBOR_MajorTypes.SIMPLE_AND_FLOAT, - CBOR_FloatAI.DOUBLE) - + struct.pack(">Q", bits) - ) - raise CBOR_Codec_Encoding_Error("Invalid NaN float AI: %d" % ai) - - -def _cbor_float_bits_from_encoded(encoded): - # type: (bytes) -> Tuple[int, int] - """Return ``(ai, bits)`` for a definite CBOR float item.""" - wire = bytes(encoded) - if not wire: - raise CBOR_Codec_Encoding_Error("empty CBOR float encoding") - ai = wire[0] & 0x1f - if ai == int(CBOR_FloatAI.HALF): - if len(wire) < 3: - raise CBOR_Codec_Encoding_Error("truncated half float") - return ai, struct.unpack(">H", wire[1:3])[0] - if ai == int(CBOR_FloatAI.SINGLE): - if len(wire) < 5: - raise CBOR_Codec_Encoding_Error("truncated single float") - return ai, struct.unpack(">I", wire[1:5])[0] - if ai == int(CBOR_FloatAI.DOUBLE): - if len(wire) < 9: - raise CBOR_Codec_Encoding_Error("truncated double float") - return ai, struct.unpack(">Q", wire[1:9])[0] - raise CBOR_Codec_Encoding_Error("not a CBOR float encoding: ai=%d" % ai) - - def _cbor_preferred_float_ai(value): # type: (float) -> int """Return the preferred float AI for a numeric *value*.""" @@ -537,14 +485,18 @@ def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): issues = [] # type: List[Tuple[int, str]] index = [0] - def _walk(): - # type: () -> None + def _walk(depth=0): + # type: (int) -> None + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=s[index[0]:]) start = index[0] if start >= len(s): raise CBOR_Codec_Decoding_Error( "Empty CBOR data", remaining=s[start:]) initial = s[start] - if initial == 0xff: + if initial == CBOR_BREAK_BYTE: issues.append(( base_offset + start, "Standalone break byte (0xff)", @@ -637,7 +589,53 @@ def _walk(): int(CBOR_MajorTypes.TEXT_STRING), ): while index[0] < len(s) and not cbor_is_break(s[index[0]:]): - _walk() + chunk_start = index[0] + chunk_major, chunk_len, rem = CBOR_decode_head(s[chunk_start:]) + consumed = len(s) - chunk_start - len(rem) + if chunk_major != major: + raise CBOR_Codec_Decoding_Error( + "Indefinite string chunk must be major type %d, " + "got %d" % (major, chunk_major), + remaining=s[chunk_start:]) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite string", + remaining=s[chunk_start:]) + chunk_ai = s[chunk_start] & 0x1f + # Shortest-argument check for the chunk head. + if chunk_ai == int(CBOR_AdditionalInfo.ONE_BYTE): + if int(chunk_len) < int(CBOR_AdditionalInfo.ONE_BYTE): + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) + elif chunk_ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if int(chunk_len) < 256: + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) + elif chunk_ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if int(chunk_len) < 65536: + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) + elif chunk_ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + if int(chunk_len) < (1 << 32): + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) + if len(rem) < int(chunk_len): + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string chunk", + remaining=s[chunk_start:]) + index[0] = chunk_start + consumed + int(chunk_len) if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): raise CBOR_Codec_Decoding_Error( "Expected break byte (0xff)", remaining=s[index[0]:]) @@ -645,7 +643,7 @@ def _walk(): return if major == int(CBOR_MajorTypes.ARRAY): while index[0] < len(s) and not cbor_is_break(s[index[0]:]): - _walk() + _walk(depth + 1) if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): raise CBOR_Codec_Decoding_Error( "Expected break byte (0xff)", remaining=s[index[0]:]) @@ -655,9 +653,9 @@ def _walk(): key_encodings = [] # type: List[bytes] while index[0] < len(s) and not cbor_is_break(s[index[0]:]): key_start = index[0] - _walk() + _walk(depth + 1) key_encodings.append(bytes(s[key_start:index[0]])) - _walk() + _walk(depth + 1) if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): raise CBOR_Codec_Decoding_Error( "Expected break byte (0xff)", remaining=s[index[0]:]) @@ -673,7 +671,19 @@ def _walk(): remaining=s[start:], ) - if not cbor_argument_is_shortest(ai, value): + # Shortest-argument check (was cbor_argument_is_shortest). + shortest = True + if ai == int(CBOR_AdditionalInfo.ONE_BYTE): + shortest = int(value) >= int(CBOR_AdditionalInfo.ONE_BYTE) + elif ai == int(CBOR_AdditionalInfo.TWO_BYTES): + shortest = int(value) >= 256 + elif ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + shortest = int(value) >= 65536 + elif ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + shortest = int(value) >= (1 << 32) + elif ai >= int(CBOR_AdditionalInfo.ONE_BYTE): + shortest = ai == int(CBOR_AdditionalInfo.INDEFINITE) + if not shortest: issues.append(( base_offset + start, "Non-shortest CBOR argument encoding (AI=%d, value=%r)" @@ -692,15 +702,15 @@ def _walk(): return if major == int(CBOR_MajorTypes.ARRAY): for _ in range(int(value)): - _walk() + _walk(depth + 1) return if major == int(CBOR_MajorTypes.MAP): key_encodings = [] # type: List[bytes] for _ in range(int(value)): key_start = index[0] - _walk() + _walk(depth + 1) key_encodings.append(bytes(s[key_start:index[0]])) - _walk() + _walk(depth + 1) if key_encodings != sorted(key_encodings): issues.append(( base_offset + start, @@ -708,7 +718,7 @@ def _walk(): )) return if major == int(CBOR_MajorTypes.TAG): - _walk() + _walk(depth + 1) return try: @@ -905,7 +915,29 @@ def encode_cbor_item_deterministic(item): if isinstance(item, CBOR_FLOAT): encoded = getattr(item, "_encoded", None) if encoded is not None and math.isnan(float(item.val)): - ai, bits = _cbor_float_bits_from_encoded(encoded) + wire = bytes(encoded) + if not wire: + raise CBOR_Codec_Encoding_Error( + "empty CBOR float encoding") + ai = wire[0] & 0x1f + if ai == int(CBOR_FloatAI.HALF): + if len(wire) < 3: + raise CBOR_Codec_Encoding_Error( + "truncated half float") + bits = struct.unpack(">H", wire[1:3])[0] + elif ai == int(CBOR_FloatAI.SINGLE): + if len(wire) < 5: + raise CBOR_Codec_Encoding_Error( + "truncated single float") + bits = struct.unpack(">I", wire[1:5])[0] + elif ai == int(CBOR_FloatAI.DOUBLE): + if len(wire) < 9: + raise CBOR_Codec_Encoding_Error( + "truncated double float") + bits = struct.unpack(">Q", wire[1:9])[0] + else: + raise CBOR_Codec_Encoding_Error( + "not a CBOR float encoding: ai=%d" % ai) comps = _cbor_nan_components(ai, bits) if comps is None: raise CBOR_Codec_Encoding_Error( @@ -914,7 +946,38 @@ def encode_cbor_item_deterministic(item): ) sign, significand52 = comps preferred = _cbor_nan_preferred_ai(ai, bits) - return _cbor_encode_nan(sign, significand52, preferred) + if preferred == int(CBOR_FloatAI.HALF): + fraction = (significand52 >> 42) & 0x3ff + nan_bits = (sign << 15) | (0x1f << 10) | fraction + return ( + CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.HALF) + + struct.pack(">H", nan_bits) + ) + if preferred == int(CBOR_FloatAI.SINGLE): + fraction = (significand52 >> 29) & 0x7fffff + nan_bits = (sign << 31) | (0xff << 23) | fraction + return ( + CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.SINGLE) + + struct.pack(">I", nan_bits) + ) + if preferred == int(CBOR_FloatAI.DOUBLE): + nan_bits = ( + (sign << 63) | + (0x7ff << 52) | + (significand52 & ((1 << 52) - 1)) + ) + return ( + CBOR_encode_initial( + CBOR_MajorTypes.SIMPLE_AND_FLOAT, + CBOR_FloatAI.DOUBLE) + + struct.pack(">Q", nan_bits) + ) + raise CBOR_Codec_Encoding_Error( + "Invalid NaN float AI: %d" % preferred) # Finite floats ignore original width; rebuild preferred form. return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item.val)) if isinstance(item, CBOR_ARRAY): @@ -1141,7 +1204,7 @@ def do_dec(cls, remainder = cbor_consume_break(remainder) break chunk_mt, chunk_len, remainder = CBOR_decode_head(remainder) - if chunk_mt != 2: + if chunk_mt != int(CBOR_MajorTypes.BYTE_STRING): raise CBOR_Codec_Decoding_Error( "Indefinite byte string chunk must be major type 2", remaining=remainder) @@ -1205,7 +1268,7 @@ def do_dec(cls, remainder = cbor_consume_break(remainder) break chunk_mt, chunk_len, remainder = CBOR_decode_head(remainder) - if chunk_mt != 3: + if chunk_mt != int(CBOR_MajorTypes.TEXT_STRING): raise CBOR_Codec_Decoding_Error( "Indefinite text string chunk must be major type 3", remaining=remainder) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 0203159e2f2..2703775408a 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -43,11 +43,13 @@ CBOR_SIMPLE_VALUE, ) from scapy.cbor.cborcodec import ( + CBOR_BREAK_BYTE, CBOR_Codec_Decoding_Error, CBOR_INDEFINITE, CBOR_decode_head, CBOR_encode_head, CBOR_encode_initial, + _cbor_skip_item, cbor_count_items, cbor_is_break, cbor_consume_break, @@ -136,10 +138,10 @@ def __deepcopy__(self, memo): 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"" + rem = s if isinstance(s, memoryview) else memoryview(s) + after = _cbor_skip_item(rem) + n = len(s) - len(after) + return bytes(s[:n]), bytes(s[n:]) def _encode_exactly_one_cbor_item(val, context="value"): @@ -709,7 +711,10 @@ def matches_next_item(self, pkt, s): major_type, _info, _rem = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error: return False - return major_type in (0, 1) + return major_type in ( + int(CBOR_MajorTypes.UNSIGNED_INTEGER), + int(CBOR_MajorTypes.NEGATIVE_INTEGER), + ) def any2i(self, pkt, x): # type: (CBOR_Packet, Any) -> int @@ -728,10 +733,10 @@ def m2i(self, pkt, s): if not s: raise CBOR_Decoding_Error("Empty CBOR data") major_type = (s[0] >> 5) & 0x7 - if major_type == 0: + if major_type == int(CBOR_MajorTypes.UNSIGNED_INTEGER): obj, remain = CBORcodec_UNSIGNED_INTEGER.dec(s) return obj.val, remain - elif major_type == 1: + elif major_type == int(CBOR_MajorTypes.NEGATIVE_INTEGER): obj, remain = CBORcodec_NEGATIVE_INTEGER.dec(s) return obj.val, remain raise CBOR_Type_Mismatch( @@ -1415,7 +1420,7 @@ def build_result(self, pkt): CBOR_MajorTypes.ARRAY, CBOR_AdditionalInfo.INDEFINITE ) + items_data + - b'\xff' + bytes([CBOR_BREAK_BYTE]) ) else: data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, total_items) @@ -1972,8 +1977,16 @@ def dissect_result(self, pkt, s): pair_values = {} # type: Dict[str, bytes] unknown_pairs = [] # type: List[Tuple[str, Any]] - def _map_text_key(key_obj): - # type: (Any) -> str + 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" + ) if not isinstance(key_obj, CBOR_TEXT_STRING): raise CBOR_Decoding_Error( "CBOR map field key must be a text string, got %r" @@ -1985,19 +1998,6 @@ def _map_text_key(key_obj): "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 diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 65b88c49b9c..088ee659883 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -3409,6 +3409,46 @@ except Exception as err: assert not isinstance(err, RecursionError) assert "nesting" in str(err).lower() += Lightweight skip rejects malformed indefinite string chunks +from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error, cbor_count_items +from scapy.cbor.cborfields import cbor_item_span + +# Indefinite byte string (0x5f) whose "chunk" is an array (0x80), then break. +bad_bstr = b"\x5f\x80\xff" +try: + cbor_count_items(bad_bstr) + assert False, "count accepted wrong-type indefinite byte chunk" +except CBOR_Codec_Decoding_Error as err: + assert "chunk" in str(err).lower() or "major type" in str(err).lower() + +try: + cbor_item_span(bad_bstr) + assert False, "span accepted wrong-type indefinite byte chunk" +except CBOR_Codec_Decoding_Error as err: + assert "chunk" in str(err).lower() or "major type" in str(err).lower() + +# Indefinite text string (0x7f) with an unsigned integer chunk (0x01). +bad_tstr = b"\x7f\x01\xff" +try: + cbor_item_span(bad_tstr) + assert False, "span accepted wrong-type indefinite text chunk" +except CBOR_Codec_Decoding_Error: + pass + +# Nested indefinite byte string chunk is rejected. +nested_indef = b"\x5f\x5f\xff\xff" +try: + cbor_count_items(nested_indef) + assert False, "count accepted nested indefinite byte string" +except CBOR_Codec_Decoding_Error: + pass + +# Well-formed indefinite byte string still spans correctly. +good = b"\x5f\x41a\xff" + b"\x00" +item, rest = cbor_item_span(good) +assert item == b"\x5f\x41a\xff" +assert rest == b"\x00" + = CBOR object display helpers and decoding-error repr import copy from scapy.cbor.cbor import ( From e58400e180d92927e8999fa2fd9974d1504f0a59 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 14:57:45 +0200 Subject: [PATCH 40/48] cbor: restore raw well-formed decode and house item span in codec Keep structural cbor_item_span for framing, but validate RawVal and raw packet bytes with a full decode; nest shortest-argument checks and cover the non-det walker depth/chunk paths. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborcodec.py | 79 ++++++++++++++++----------------- scapy/cbor/cborfields.py | 22 ++------- test/scapy/layers/cbor.uts | 91 +++++++++++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 62 deletions(-) diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index fe06e7de661..4bff1996236 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -247,6 +247,20 @@ def cbor_count_items(s, max_count=None, until_break=False): return count +def cbor_item_span(s): + # type: (Any) -> Tuple[bytes, bytes] + """Split *s* into the first structural CBOR item and the remainder. + + Uses :func:`_cbor_skip_item` for boundary finding only. Callers that must + reject semantically invalid CBOR (invalid UTF-8, duplicate map keys, …) + should decode with :meth:`CBORcodec_Object.decode_cbor_item` instead. + """ + rem = s if isinstance(s, memoryview) else memoryview(s) + after = _cbor_skip_item(rem) + n = len(s) - len(after) + return bytes(s[:n]), bytes(s[n:]) + + def CBOR_decode_head(s): # type: (Any) -> Tuple[int, Union[int, CBOR_INDEFINITE], Any] """ @@ -485,6 +499,22 @@ def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): issues = [] # type: List[Tuple[int, str]] index = [0] + def _argument_is_shortest(ai, value): + # type: (int, Union[int, CBOR_INDEFINITE]) -> bool + if value is CBOR_INDEFINITE: + return ai == int(CBOR_AdditionalInfo.INDEFINITE) + if ai < int(CBOR_AdditionalInfo.ONE_BYTE): + return True + if ai == int(CBOR_AdditionalInfo.ONE_BYTE): + return int(value) >= int(CBOR_AdditionalInfo.ONE_BYTE) + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + return int(value) >= 256 + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + return int(value) >= 65536 + if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + return int(value) >= (1 << 32) + return ai == int(CBOR_AdditionalInfo.INDEFINITE) + def _walk(depth=0): # type: (int) -> None if depth > MAX_CBOR_NESTING: @@ -602,35 +632,12 @@ def _walk(depth=0): "Nested indefinite string", remaining=s[chunk_start:]) chunk_ai = s[chunk_start] & 0x1f - # Shortest-argument check for the chunk head. - if chunk_ai == int(CBOR_AdditionalInfo.ONE_BYTE): - if int(chunk_len) < int(CBOR_AdditionalInfo.ONE_BYTE): - issues.append(( - base_offset + chunk_start, - "Non-shortest CBOR argument encoding " - "(AI=%d, value=%r)" % (chunk_ai, chunk_len), - )) - elif chunk_ai == int(CBOR_AdditionalInfo.TWO_BYTES): - if int(chunk_len) < 256: - issues.append(( - base_offset + chunk_start, - "Non-shortest CBOR argument encoding " - "(AI=%d, value=%r)" % (chunk_ai, chunk_len), - )) - elif chunk_ai == int(CBOR_AdditionalInfo.FOUR_BYTES): - if int(chunk_len) < 65536: - issues.append(( - base_offset + chunk_start, - "Non-shortest CBOR argument encoding " - "(AI=%d, value=%r)" % (chunk_ai, chunk_len), - )) - elif chunk_ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): - if int(chunk_len) < (1 << 32): - issues.append(( - base_offset + chunk_start, - "Non-shortest CBOR argument encoding " - "(AI=%d, value=%r)" % (chunk_ai, chunk_len), - )) + if not _argument_is_shortest(chunk_ai, chunk_len): + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) if len(rem) < int(chunk_len): raise CBOR_Codec_Decoding_Error( "Truncated byte/text string chunk", @@ -671,19 +678,7 @@ def _walk(depth=0): remaining=s[start:], ) - # Shortest-argument check (was cbor_argument_is_shortest). - shortest = True - if ai == int(CBOR_AdditionalInfo.ONE_BYTE): - shortest = int(value) >= int(CBOR_AdditionalInfo.ONE_BYTE) - elif ai == int(CBOR_AdditionalInfo.TWO_BYTES): - shortest = int(value) >= 256 - elif ai == int(CBOR_AdditionalInfo.FOUR_BYTES): - shortest = int(value) >= 65536 - elif ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): - shortest = int(value) >= (1 << 32) - elif ai >= int(CBOR_AdditionalInfo.ONE_BYTE): - shortest = ai == int(CBOR_AdditionalInfo.INDEFINITE) - if not shortest: + if not _argument_is_shortest(ai, value): issues.append(( base_offset + start, "Non-shortest CBOR argument encoding (AI=%d, value=%r)" diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 2703775408a..5b65b86bf39 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -49,8 +49,8 @@ CBOR_decode_head, CBOR_encode_head, CBOR_encode_initial, - _cbor_skip_item, cbor_count_items, + cbor_item_span, cbor_is_break, cbor_consume_break, CBORcodec_Object, @@ -135,15 +135,6 @@ def __deepcopy__(self, memo): 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.""" - rem = s if isinstance(s, memoryview) else memoryview(s) - after = _cbor_skip_item(rem) - n = len(s) - len(after) - return bytes(s[:n]), bytes(s[n:]) - - def _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. @@ -163,7 +154,7 @@ def _encode_exactly_one_cbor_item(val, context="value"): else: data = bytes(val) try: - item, remaining = cbor_item_span(data) + _obj, remaining = CBORcodec_Object.decode_cbor_item(data) except Exception as exc: raise CBOR_Encoding_Error( "%s did not encode a well-formed CBOR item: %s" @@ -174,11 +165,6 @@ def _encode_exactly_one_cbor_item(val, context="value"): "%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 @@ -286,13 +272,13 @@ def i2m(self, pkt, x): if isinstance(x, fields.RawVal): data = bytes(x) try: - item, remaining = cbor_item_span(data) + _obj, remaining = CBORcodec_Object.decode_cbor_item(data) except Exception as exc: raise CBOR_Encoding_Error( "RawVal for %r is not well-formed CBOR: %s" % (self.name, exc) ) - if remaining or item != data: + if remaining: raise CBOR_Encoding_Error( "RawVal for %r must contain exactly one CBOR item" % self.name diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 088ee659883..da3b78a4372 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2346,6 +2346,30 @@ assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000000000001")) == [] # binary64 quiet NaN with only top significand bits set prefers binary16 assert cbor_find_non_deterministic(bytes.fromhex("fb7ffc000000000000")) += Non-det walker enforces nesting depth and indefinite string chunks +from scapy.cbor.cborcodec import ( + MAX_CBOR_NESTING, + cbor_find_non_deterministic, +) + +# Malformed indefinite strings: checker must not raise RecursionError. +bad_bstr = b"\x5f\x61a\xff" # byte string with a text chunk +bad_tstr = b"\x7f\x41a\xff" # text string with a byte chunk +nested_indef = b"\x5f\x5f\xff\xff" +for wire in (bad_bstr, bad_tstr, nested_indef): + issues = cbor_find_non_deterministic(wire, allow_indefinite=True) + assert isinstance(issues, list) + +# Over-nested arrays: swallowed as malformed, not RecursionError. +too_deep = b"\x81" * (MAX_CBOR_NESTING + 1) + b"\x00" +issues = cbor_find_non_deterministic(too_deep) +assert isinstance(issues, list) + +# Well-formed indefinite byte string still scans without error issues. +assert cbor_find_non_deterministic( + b"\x5f\x41a\xff", allow_indefinite=True +) == [] + + Cache item counts and packet-field cardinality @@ -2605,6 +2629,66 @@ try: except CBOR_Encoding_Error: pass += RawVal and raw packet encode reject semantically invalid CBOR +from scapy.cbor.cborfields import ( + CBORF_ANY, + CBORF_PACKET, + CBORF_UNSIGNED_INTEGER, + _encode_exactly_one_cbor_item, +) +from scapy.cbor.cborcodec import cbor_item_span +from scapy.cborpacket import CBOR_Packet +from scapy.fields import RawVal +from scapy.cbor.cbor import CBOR_Encoding_Error + +bad_utf8 = b"\x61\xff" +duplicate_map = bytes.fromhex("a201000101") + +# Structural span only finds boundaries; it does not UTF-8 / key-validate. +item, rest = cbor_item_span(bad_utf8) +assert item == bad_utf8 and rest == b"" +item, rest = cbor_item_span(duplicate_map) +assert item == duplicate_map and rest == b"" + +class RawAnyPkt(CBOR_Packet): + CBOR_root = CBORF_ANY("value", None) + +try: + bytes(RawAnyPkt(value=RawVal(bad_utf8))) + assert False, "RawVal accepted invalid UTF-8 text string" +except CBOR_Encoding_Error: + pass + +try: + bytes(RawAnyPkt(value=RawVal(duplicate_map))) + assert False, "RawVal accepted duplicate map keys" +except CBOR_Encoding_Error: + pass + +try: + _encode_exactly_one_cbor_item(bad_utf8, context="raw") + assert False, "_encode_exactly_one_cbor_item accepted invalid UTF-8" +except CBOR_Encoding_Error: + pass + +try: + _encode_exactly_one_cbor_item(duplicate_map, context="raw") + assert False, "_encode_exactly_one_cbor_item accepted duplicate keys" +except CBOR_Encoding_Error: + pass + +class RawChild(CBOR_Packet): + CBOR_root = CBORF_UNSIGNED_INTEGER("n", 0) + +class RawParent(CBOR_Packet): + CBOR_root = CBORF_PACKET("child", None, RawChild) + +try: + bytes(RawParent(child=bad_utf8)) + assert False, "packet-valued raw bytes accepted invalid UTF-8" +except CBOR_Encoding_Error: + pass + = 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 @@ -3410,8 +3494,11 @@ except Exception as err: assert "nesting" in str(err).lower() = Lightweight skip rejects malformed indefinite string chunks -from scapy.cbor.cborcodec import CBOR_Codec_Decoding_Error, cbor_count_items -from scapy.cbor.cborfields import cbor_item_span +from scapy.cbor.cborcodec import ( + CBOR_Codec_Decoding_Error, + cbor_count_items, + cbor_item_span, +) # Indefinite byte string (0x5f) whose "chunk" is an array (0x80), then break. bad_bstr = b"\x5f\x80\xff" From 2b6423e30e15c33e72238d7273ca7625ae304044 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 15:48:16 +0200 Subject: [PATCH 41/48] cbor: move non-det scanner to tests and trim field/API scaffolding Make build_result/dissect_result canonical, drop dead badsequence and unused enum/flags exports, simplify map pair collection, and keep the deterministic scanner out of production. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/__init__.py | 10 - scapy/cbor/cbor.py | 60 +++--- scapy/cbor/cborcodec.py | 286 +-------------------------- scapy/cbor/cborfields.py | 189 +++++------------- test/scapy/layers/cbor.uts | 50 +++-- test/scapy/layers/cbor_test_utils.py | 263 ++++++++++++++++++++++++ 6 files changed, 386 insertions(+), 472 deletions(-) create mode 100644 test/scapy/layers/cbor_test_utils.py diff --git a/scapy/cbor/__init__.py b/scapy/cbor/__init__.py index 58c2f1f410c..80d5e93d943 100644 --- a/scapy/cbor/__init__.py +++ b/scapy/cbor/__init__.py @@ -14,10 +14,7 @@ CBOR_BadTag_Decoding_Error, CBOR_Codecs, CBOR_MajorTypes, - CBOR_AdditionalInfo, CBOR_SimpleValue, - CBOR_FloatAI, - CBOR_UINT64_MAX, CBOR_Object, CBOR_UNSIGNED_INTEGER, CBOR_NEGATIVE_INTEGER, @@ -70,8 +67,6 @@ CBORF_ARRAY_INDEFINITE, CBORF_MAP, CBORF_SEMANTIC_TAG, - CBORF_UNSIGNED_ENUM, - CBORF_UNSIGNED_FLAGS, CBORF_optional, CBORF_CONDITIONAL, CBORF_PACKET, @@ -87,10 +82,7 @@ # Codecs "CBOR_Codecs", "CBOR_MajorTypes", - "CBOR_AdditionalInfo", "CBOR_SimpleValue", - "CBOR_FloatAI", - "CBOR_UINT64_MAX", # Objects "CBOR_Object", "CBOR_UNSIGNED_INTEGER", @@ -144,8 +136,6 @@ "CBORF_MAP", "CBORF_SEMANTIC_TAG", # Complex fields - "CBORF_UNSIGNED_ENUM", - "CBORF_UNSIGNED_FLAGS", "CBORF_optional", "CBORF_CONDITIONAL", "CBORF_PACKET", diff --git a/scapy/cbor/cbor.py b/scapy/cbor/cbor.py index cd379cdbdca..7e86f60c4cd 100644 --- a/scapy/cbor/cbor.py +++ b/scapy/cbor/cbor.py @@ -471,12 +471,6 @@ def cbor_pairs(self): # type: () -> List[Tuple[Any, Any]] return list(self._pairs) - @property - def pairs(self): - # type: () -> List[Tuple[Any, Any]] - """Ordered ``(key, value)`` pairs (primary map representation).""" - return self.cbor_pairs() - def as_dict(self): # type: () -> Dict[Any, Any] """Convert to a Python dict, raising if CBOR key distinctions would be lost.""" @@ -765,6 +759,28 @@ def enc(self, codec=None): return super(CBOR_FLOAT, self).enc(codec) +def _cbor_float_wire_parts(encoded): + # type: (bytes) -> Tuple[int, int] + """Return ``(ai, bits)`` for a definite CBOR float encoding.""" + wire = bytes(encoded) + if not wire: + raise ValueError("empty CBOR float encoding") + ai = wire[0] & 0x1f + if ai == int(CBOR_FloatAI.HALF): + if len(wire) < 3: + raise ValueError("truncated half float") + return ai, struct.unpack(">H", wire[1:3])[0] + if ai == int(CBOR_FloatAI.SINGLE): + if len(wire) < 5: + raise ValueError("truncated single float") + return ai, struct.unpack(">I", wire[1:5])[0] + if ai == int(CBOR_FloatAI.DOUBLE): + if len(wire) < 9: + raise ValueError("truncated double float") + return ai, struct.unpack(">Q", wire[1:9])[0] + raise ValueError("not a CBOR float encoding: ai=%d" % ai) + + def _cbor_float_key_identity(value, encoded=None): # type: (float, Optional[bytes]) -> Tuple[Any, ...] """Return RFC 8949 floating-point map-key identity for *value*. @@ -775,14 +791,8 @@ def _cbor_float_key_identity(value, encoded=None): and sign survive Python's NaN canonicalization. """ if encoded is not None: - wire = bytes(encoded) - if not wire: - raise ValueError("empty CBOR float encoding") - ai = wire[0] & 0x1f + ai, bits = _cbor_float_wire_parts(encoded) if ai == int(CBOR_FloatAI.HALF): - if len(wire) < 3: - raise ValueError("truncated half float") - bits = struct.unpack(">H", wire[1:3])[0] sign = (bits >> 15) & 0x1 exponent = (bits >> 10) & 0x1f fraction = bits & 0x3ff @@ -805,9 +815,6 @@ def _cbor_float_key_identity(value, encoded=None): ) return _cbor_float_key_identity(float_val) if ai == int(CBOR_FloatAI.SINGLE): - if len(wire) < 5: - raise ValueError("truncated single float") - bits = struct.unpack(">I", wire[1:5])[0] sign = (bits >> 31) & 0x1 exponent = (bits >> 23) & 0xff fraction = bits & 0x7fffff @@ -815,18 +822,15 @@ def _cbor_float_key_identity(value, encoded=None): return ("nan", sign, fraction << 29) float_val = struct.unpack(">f", struct.pack(">I", bits))[0] return _cbor_float_key_identity(float_val) - if ai == int(CBOR_FloatAI.DOUBLE): - if len(wire) < 9: - raise ValueError("truncated double float") - bits = struct.unpack(">Q", wire[1:9])[0] - sign = (bits >> 63) & 0x1 - exponent = (bits >> 52) & 0x7ff - fraction = bits & ((1 << 52) - 1) - if exponent == 0x7ff and fraction: - return ("nan", sign, fraction) - float_val = struct.unpack(">d", struct.pack(">Q", bits))[0] - return _cbor_float_key_identity(float_val) - raise ValueError("not a CBOR float encoding: ai=%d" % ai) + # DOUBLE + sign = (bits >> 63) & 0x1 + exponent = (bits >> 52) & 0x7ff + fraction = bits & ((1 << 52) - 1) + if exponent == 0x7ff and fraction: + return ("nan", sign, fraction) + float_val = struct.unpack(">d", struct.pack(">Q", bits))[0] + return _cbor_float_key_identity(float_val) + fval = float(value) if math.isnan(fval): bits = struct.unpack(">Q", struct.pack(">d", fval))[0] diff --git a/scapy/cbor/cborcodec.py b/scapy/cbor/cborcodec.py index 4bff1996236..d20a8a65277 100644 --- a/scapy/cbor/cborcodec.py +++ b/scapy/cbor/cborcodec.py @@ -487,243 +487,6 @@ def _cbor_preferred_float_ai(value): return int(CBOR_FloatAI.DOUBLE) -def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): - # type: (bytes, bool, int) -> List[Tuple[int, str]] - """Scan one top-level CBOR item for non-core-deterministic encodings. - - Walks a single top-level item (and nested contents). Trailing bytes after - that item are ignored. Returns ``(absolute_offset, message)`` issues. - Indefinite-length items are rejected by default; protocols that permit - them may pass ``allow_indefinite=True``. - """ - issues = [] # type: List[Tuple[int, str]] - index = [0] - - def _argument_is_shortest(ai, value): - # type: (int, Union[int, CBOR_INDEFINITE]) -> bool - if value is CBOR_INDEFINITE: - return ai == int(CBOR_AdditionalInfo.INDEFINITE) - if ai < int(CBOR_AdditionalInfo.ONE_BYTE): - return True - if ai == int(CBOR_AdditionalInfo.ONE_BYTE): - return int(value) >= int(CBOR_AdditionalInfo.ONE_BYTE) - if ai == int(CBOR_AdditionalInfo.TWO_BYTES): - return int(value) >= 256 - if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): - return int(value) >= 65536 - if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): - return int(value) >= (1 << 32) - return ai == int(CBOR_AdditionalInfo.INDEFINITE) - - def _walk(depth=0): - # type: (int) -> None - if depth > MAX_CBOR_NESTING: - raise CBOR_Codec_Decoding_Error( - "Maximum CBOR nesting depth exceeded", - remaining=s[index[0]:]) - start = index[0] - if start >= len(s): - raise CBOR_Codec_Decoding_Error( - "Empty CBOR data", remaining=s[start:]) - initial = s[start] - if initial == CBOR_BREAK_BYTE: - issues.append(( - base_offset + start, - "Standalone break byte (0xff)", - )) - index[0] = start + 1 - return - major = initial >> 5 - ai = initial & 0x1f - pos = start + 1 - if ai < int(CBOR_AdditionalInfo.ONE_BYTE): - value = ai # type: Union[int, CBOR_INDEFINITE] - elif ai == int(CBOR_AdditionalInfo.ONE_BYTE): - if pos + 1 > len(s): - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 1-byte value", remaining=s[start:]) - value = s[pos] - pos += 1 - elif ai == int(CBOR_AdditionalInfo.TWO_BYTES): - if pos + 2 > len(s): - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 2-byte value", remaining=s[start:]) - value = struct.unpack(">H", s[pos:pos + 2])[0] - pos += 2 - elif ai == int(CBOR_AdditionalInfo.FOUR_BYTES): - if pos + 4 > len(s): - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 4-byte value", remaining=s[start:]) - value = struct.unpack(">I", s[pos:pos + 4])[0] - pos += 4 - elif ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): - if pos + 8 > len(s): - raise CBOR_Codec_Decoding_Error( - "Not enough bytes for 8-byte value", remaining=s[start:]) - value = struct.unpack(">Q", s[pos:pos + 8])[0] - pos += 8 - elif ai == int(CBOR_AdditionalInfo.INDEFINITE): - value = CBOR_INDEFINITE - elif ai in ( - int(CBOR_AdditionalInfo.RESERVED_28), - int(CBOR_AdditionalInfo.RESERVED_29), - int(CBOR_AdditionalInfo.RESERVED_30), - ): - raise CBOR_Codec_Decoding_Error( - "Reserved additional info: %d" % ai, remaining=s[start:]) - else: - raise CBOR_Codec_Decoding_Error( - "Invalid additional info: %d" % ai, remaining=s[start:]) - index[0] = pos - - # Major type 7: simple values and floats. Check float preferred width. - if major == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): - if ( - ai == int(CBOR_AdditionalInfo.ONE_BYTE) - and isinstance(value, int) - and value < 32 - ): - issues.append(( - base_offset + start, - "Non-shortest CBOR simple value encoding " - "(AI=24, value=%d)" % value, - )) - if ai in ( - int(CBOR_FloatAI.HALF), - int(CBOR_FloatAI.SINGLE), - int(CBOR_FloatAI.DOUBLE), - ) and value is not CBOR_INDEFINITE: - comps = _cbor_nan_components(ai, int(value)) - if comps is not None: - preferred = _cbor_nan_preferred_ai(ai, int(value)) - else: - preferred = _cbor_preferred_float_ai( - _cbor_float_from_bits(ai, int(value)) - ) - if preferred < ai: - issues.append(( - base_offset + start, - "Non-shortest CBOR float encoding (AI=%d, preferred AI=%d)" - % (ai, preferred), - )) - return - - if value is CBOR_INDEFINITE: - if not allow_indefinite: - issues.append(( - base_offset + start, - "Indefinite-length item is not allowed", - )) - if major in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), - ): - while index[0] < len(s) and not cbor_is_break(s[index[0]:]): - chunk_start = index[0] - chunk_major, chunk_len, rem = CBOR_decode_head(s[chunk_start:]) - consumed = len(s) - chunk_start - len(rem) - if chunk_major != major: - raise CBOR_Codec_Decoding_Error( - "Indefinite string chunk must be major type %d, " - "got %d" % (major, chunk_major), - remaining=s[chunk_start:]) - if chunk_len is CBOR_INDEFINITE: - raise CBOR_Codec_Decoding_Error( - "Nested indefinite string", - remaining=s[chunk_start:]) - chunk_ai = s[chunk_start] & 0x1f - if not _argument_is_shortest(chunk_ai, chunk_len): - issues.append(( - base_offset + chunk_start, - "Non-shortest CBOR argument encoding " - "(AI=%d, value=%r)" % (chunk_ai, chunk_len), - )) - if len(rem) < int(chunk_len): - raise CBOR_Codec_Decoding_Error( - "Truncated byte/text string chunk", - remaining=s[chunk_start:]) - index[0] = chunk_start + consumed + int(chunk_len) - if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): - raise CBOR_Codec_Decoding_Error( - "Expected break byte (0xff)", remaining=s[index[0]:]) - index[0] += 1 - return - if major == int(CBOR_MajorTypes.ARRAY): - while index[0] < len(s) and not cbor_is_break(s[index[0]:]): - _walk(depth + 1) - if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): - raise CBOR_Codec_Decoding_Error( - "Expected break byte (0xff)", remaining=s[index[0]:]) - index[0] += 1 - return - if major == int(CBOR_MajorTypes.MAP): - key_encodings = [] # type: List[bytes] - while index[0] < len(s) and not cbor_is_break(s[index[0]:]): - key_start = index[0] - _walk(depth + 1) - key_encodings.append(bytes(s[key_start:index[0]])) - _walk(depth + 1) - if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): - raise CBOR_Codec_Decoding_Error( - "Expected break byte (0xff)", remaining=s[index[0]:]) - index[0] += 1 - if key_encodings != sorted(key_encodings): - issues.append(( - base_offset + start, - "CBOR map keys are not in bytewise lexicographic order", - )) - return - raise CBOR_Codec_Decoding_Error( - "Indefinite length not allowed for major type %d" % major, - remaining=s[start:], - ) - - if not _argument_is_shortest(ai, value): - issues.append(( - base_offset + start, - "Non-shortest CBOR argument encoding (AI=%d, value=%r)" - % (ai, value), - )) - - if major in ( - int(CBOR_MajorTypes.BYTE_STRING), - int(CBOR_MajorTypes.TEXT_STRING), - ): - length = int(value) - if index[0] + length > len(s): - raise CBOR_Codec_Decoding_Error( - "Truncated byte/text string", remaining=s[start:]) - index[0] += length - return - if major == int(CBOR_MajorTypes.ARRAY): - for _ in range(int(value)): - _walk(depth + 1) - return - if major == int(CBOR_MajorTypes.MAP): - key_encodings = [] # type: List[bytes] - for _ in range(int(value)): - key_start = index[0] - _walk(depth + 1) - key_encodings.append(bytes(s[key_start:index[0]])) - _walk(depth + 1) - if key_encodings != sorted(key_encodings): - issues.append(( - base_offset + start, - "CBOR map keys are not in bytewise lexicographic order", - )) - return - if major == int(CBOR_MajorTypes.TAG): - _walk(depth + 1) - return - - try: - _walk() - except CBOR_Codec_Decoding_Error: - # Malformed input is reported by normal decoding, not this checker. - pass - return issues - - # [ CBOR codec classes ] # @@ -901,6 +664,7 @@ def encode_cbor_item_deterministic(item): CBOR_SIMPLE_VALUE, CBOR_UNDEFINED, CBORMapData, + _cbor_float_wire_parts, _cbor_map_pairs, ) @@ -910,29 +674,10 @@ def encode_cbor_item_deterministic(item): if isinstance(item, CBOR_FLOAT): encoded = getattr(item, "_encoded", None) if encoded is not None and math.isnan(float(item.val)): - wire = bytes(encoded) - if not wire: - raise CBOR_Codec_Encoding_Error( - "empty CBOR float encoding") - ai = wire[0] & 0x1f - if ai == int(CBOR_FloatAI.HALF): - if len(wire) < 3: - raise CBOR_Codec_Encoding_Error( - "truncated half float") - bits = struct.unpack(">H", wire[1:3])[0] - elif ai == int(CBOR_FloatAI.SINGLE): - if len(wire) < 5: - raise CBOR_Codec_Encoding_Error( - "truncated single float") - bits = struct.unpack(">I", wire[1:5])[0] - elif ai == int(CBOR_FloatAI.DOUBLE): - if len(wire) < 9: - raise CBOR_Codec_Encoding_Error( - "truncated double float") - bits = struct.unpack(">Q", wire[1:9])[0] - else: - raise CBOR_Codec_Encoding_Error( - "not a CBOR float encoding: ai=%d" % ai) + try: + ai, bits = _cbor_float_wire_parts(encoded) + except ValueError as exc: + raise CBOR_Codec_Encoding_Error(str(exc)) comps = _cbor_nan_components(ai, bits) if comps is None: raise CBOR_Codec_Encoding_Error( @@ -1009,26 +754,7 @@ def encode_cbor_item_deterministic(item): CBOR_encode_head(CBOR_MajorTypes.ARRAY, 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): - # Deterministic encoding always rebuilds from the semantic float - # value (shortest exact representation). Never reuse source wire. - # Plain NaNs without retained CBOR bytes use quiet binary16. - return CBORcodec_SIMPLE_AND_FLOAT.enc(float(item)) - if item is None: - return CBORcodec_SIMPLE_AND_FLOAT.enc(None) - raise CBOR_Codec_Encoding_Error( - "Cannot deterministically encode type: %s" % type(item) - ) + return CBORcodec_Object.encode_cbor_item(item) @staticmethod def decode_cbor_item(s, depth=0): diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 5b65b86bf39..64d4607ab1a 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -90,10 +90,6 @@ from scapy.cborpacket import CBOR_Packet # noqa: F401 -class CBORF_badsequence(Exception): - pass - - class CBOR_Type_Mismatch(CBOR_Decoding_Error): """Raised when a CBOR field encounters an unexpected major type.""" @@ -181,21 +177,19 @@ class CBORF_element(object): def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult - data = self.build(pkt) - return CBORBuildResult(data, self.min_items(pkt)) + raise NotImplementedError 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)) + raise NotImplementedError def build(self, pkt): # type: (CBOR_Packet) -> bytes - raise NotImplementedError + return self.build_result(pkt).data def dissect(self, pkt, s): # type: (CBOR_Packet, bytes) -> bytes - raise NotImplementedError + return self.dissect_result(pkt, s).remaining def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -354,14 +348,6 @@ def build_value(self, pkt, value): items=1, ) - 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 1 @@ -1127,52 +1113,6 @@ def randval(self): # Structured CBOR Fields # ############################## -class CBORF_UNSIGNED_ENUM(CBORF_UNSIGNED_INTEGER): - """ - 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) - - def i2repr(self, pkt, x): - return self._enum.i2repr(pkt, x) - - 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): - """ - 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.""" @@ -1250,14 +1190,6 @@ def _reject_ambiguous_unbounded_sequences(self): "in the sequence (or provide count_from=)" ) - 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 _dissect_children_budgeted(self, pkt, s, count): # type: (CBOR_Packet, bytes, int) -> bytes remaining = s @@ -1291,23 +1223,17 @@ def _dissect_children_budgeted(self, pkt, s, count): field._field.parse_value(pkt, remaining) 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 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) if result.items > items_left: raise CBOR_Decoding_Error( "CBOR field consumed more items than remaining" @@ -1966,13 +1892,10 @@ def dissect_result(self, pkt, s): 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" - ) + try: + key_obj, after_key = CBORcodec_Object.decode_cbor_item(remaining) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) if not isinstance(key_obj, CBOR_TEXT_STRING): raise CBOR_Decoding_Error( "CBOR map field key must be a text string, got %r" @@ -1984,15 +1907,19 @@ def _collect_pair(): "Duplicate CBOR map field name: %r" % (key,) ) seen_keys.add(key) - val_bytes, remaining = cbor_item_span(after_key) if key in field_map: + try: + val_bytes, remaining = cbor_item_span(after_key) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) pair_values[key] = val_bytes else: - 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" + try: + val_obj, remaining = CBORcodec_Object.decode_cbor_item( + after_key ) + except CBOR_Codec_Decoding_Error as e: + raise CBOR_Decoding_Error(str(e)) unknown_pairs.append((key, val_obj)) if count is CBOR_INDEFINITE: @@ -2053,14 +1980,6 @@ def _dissect_value_bytes(fld, val_bytes): self._unknown_field.set_val(pkt, unknown_pairs) return CBORParseResult(remaining=remaining, items=1) - 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 1 @@ -2105,8 +2024,8 @@ def name(self): """Map/schema key identity comes from the tagged value field.""" return self.inner_field.name - def _parse_tag_head(self, s, require_match=True): - # type: (bytes, bool) -> Tuple[int, bytes] + def _parse_tag_head(self, s): + # type: (bytes) -> Tuple[int, bytes] try: major_type, tag_num, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: @@ -2114,7 +2033,7 @@ def _parse_tag_head(self, s, require_match=True): if major_type != int(CBOR_MajorTypes.TAG): raise CBOR_Type_Mismatch( "Expected major type 6 (semantic tag), got %d" % major_type) - if require_match and tag_num != self.tag_num: + if tag_num != self.tag_num: raise CBOR_Type_Mismatch( "Expected tag %d, got %d" % (self.tag_num, tag_num)) return tag_num, remaining @@ -2145,10 +2064,6 @@ def dissect_result(self, pkt, s): "Semantic tag content must be exactly one CBOR item") return CBORParseResult(remaining=inner.remaining, items=1) - def dissect(self, pkt, s): - # type: (CBOR_Packet, bytes) -> bytes - return self.dissect_result(pkt, s).remaining - def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult inner = self.inner_field.build_result(pkt) @@ -2157,10 +2072,6 @@ def build_result(self, pkt): "Semantic tag content must be exactly one CBOR item") return CBORBuildResult(self._encode_tagged(inner.data), 1) - def build(self, pkt): - # type: (CBOR_Packet) -> bytes - return self.build_result(pkt).data - def parse_value(self, pkt, s): # type: (CBOR_Packet, bytes) -> CBORParseResult _tag_num, remaining = self._parse_tag_head(s) @@ -2220,15 +2131,27 @@ def max_items(self, pkt): class CBORF_optional(CBORF_element): """ - Wrapper making a CBOR schema element optional. + Wrapper making a CBOR field or semantic-tag field optional. - 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). + Accepts :class:`CBORF_field` or :class:`CBORF_SEMANTIC_TAG` (presence + methods required). Absence is recorded as ``CBOR_ABSENT`` on every path + (lookahead mismatch, exhausted parent array, missing map key). If the + next item matches but decoding fails, the error propagates. """ def __init__(self, field): - # type: (CBORF_element) -> None + # type: (Union[CBORF_field[Any], CBORF_SEMANTIC_TAG]) -> None + for attr in ( + "is_absent", + "is_empty", + "matches_next_item", + "mark_absent", + ): + if not callable(getattr(field, attr, None)): + raise TypeError( + "CBORF_optional requires a field-like element with %s(); " + "got %r" % (attr, type(field).__name__) + ) self._field = field def __getattr__(self, attr): @@ -2250,14 +2173,6 @@ def dissect_result(self, pkt, s): 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 - return self.dissect_result(pkt, s).remaining - def min_items(self, pkt): # type: (CBOR_Packet) -> int return 0 @@ -2300,14 +2215,6 @@ def dissect_result(self, pkt, s): return self.fld.dissect_result(pkt, s) return CBORParseResult(remaining=s, items=0) - 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 if self._evalcond(pkt): diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index da3b78a4372..cc71cfbdc13 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2316,7 +2316,11 @@ assert bytes(pkt) == b"\xa2\x01\x61i\xf5\x61b" = Large binary64 values do not crash the deterministic scanner import struct -from scapy.cbor.cborcodec import cbor_find_non_deterministic +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic # RFC 8949 Appendix A example: 1.0e+300 as binary64 wire = bytes.fromhex("fb7e37e43c8800759c") @@ -2326,7 +2330,11 @@ wire = struct.pack(">B", 0xfb) + struct.pack(">d", -1e300) assert cbor_find_non_deterministic(wire) == [] = Indefinite maps require bytewise lexicographic key order -from scapy.cbor.cborcodec import cbor_find_non_deterministic +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic assert not cbor_find_non_deterministic( bytes.fromhex("bf616101616202ff"), @@ -2338,7 +2346,11 @@ assert cbor_find_non_deterministic( ) = NaN preferred width uses the original payload bit pattern -from scapy.cbor.cborcodec import cbor_find_non_deterministic +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic # binary64 NaN with a low payload bit cannot shorten to binary16/32 assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000000000001")) == [] @@ -2347,10 +2359,12 @@ assert cbor_find_non_deterministic(bytes.fromhex("fb7ff8000000000001")) == [] assert cbor_find_non_deterministic(bytes.fromhex("fb7ffc000000000000")) = Non-det walker enforces nesting depth and indefinite string chunks -from scapy.cbor.cborcodec import ( - MAX_CBOR_NESTING, - cbor_find_non_deterministic, -) +from scapy.cbor.cborcodec import MAX_CBOR_NESTING +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic # Malformed indefinite strings: checker must not raise RecursionError. bad_bstr = b"\x5f\x61a\xff" # byte string with a text chunk @@ -2582,7 +2596,11 @@ 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 importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic from scapy.cbor.cborfields import CBORF_MAP class ReverseDeclaredMap(CBOR_Packet): @@ -3214,7 +3232,11 @@ 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 +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic issues = cbor_find_non_deterministic(b"\xff") assert issues and "break" in issues[0][1].lower() @@ -3891,10 +3913,12 @@ assert CBORcodec_Object.encode_cbor_item_deterministic(value) == bytes.fromhex(" = Deterministic NaN encoding preserves sign and preferred width from scapy.cbor.cbor import CBOR_FLOAT, _cbor_key_equivalent -from scapy.cbor.cborcodec import ( - CBORcodec_Object, - cbor_find_non_deterministic, -) +from scapy.cbor.cborcodec import CBORcodec_Object +from importlib.machinery import SourceFileLoader +cbor_find_non_deterministic = SourceFileLoader( + "cbor_test_utils", + scapy_path("/test/scapy/layers/cbor_test_utils.py"), +).load_module().cbor_find_non_deterministic enc = CBORcodec_Object.encode_cbor_item_deterministic diff --git a/test/scapy/layers/cbor_test_utils.py b/test/scapy/layers/cbor_test_utils.py new file mode 100644 index 00000000000..ae6f56d6c4a --- /dev/null +++ b/test/scapy/layers/cbor_test_utils.py @@ -0,0 +1,263 @@ +# SPDX-License-Identifier: GPL-2.0-only +# This file is part of Scapy +# See https://scapy.net/ for more information +"""Test helpers for CBOR deterministic-encoding checks.""" + +import struct +from typing import List, Tuple, Union + +from scapy.cbor.cbor import ( + CBOR_AdditionalInfo, + CBOR_FloatAI, + CBOR_MajorTypes, +) +from scapy.cbor.cborcodec import ( + CBOR_BREAK_BYTE, + CBOR_Codec_Decoding_Error, + CBOR_INDEFINITE, + CBOR_decode_head, + MAX_CBOR_NESTING, + _cbor_float_from_bits, + _cbor_nan_components, + _cbor_nan_preferred_ai, + _cbor_preferred_float_ai, + cbor_is_break, +) + + +def cbor_find_non_deterministic(s, allow_indefinite=False, base_offset=0): + # type: (bytes, bool, int) -> List[Tuple[int, str]] + """Scan one top-level CBOR item for non-core-deterministic encodings. + + Walks a single top-level item (and nested contents). Trailing bytes after + that item are ignored. Returns ``(absolute_offset, message)`` issues. + Indefinite-length items are rejected by default; protocols that permit + them may pass ``allow_indefinite=True``. + """ + issues = [] # type: List[Tuple[int, str]] + index = [0] + + def _argument_is_shortest(ai, value): + # type: (int, Union[int, CBOR_INDEFINITE]) -> bool + if value is CBOR_INDEFINITE: + return ai == int(CBOR_AdditionalInfo.INDEFINITE) + if ai < int(CBOR_AdditionalInfo.ONE_BYTE): + return True + if ai == int(CBOR_AdditionalInfo.ONE_BYTE): + return int(value) >= int(CBOR_AdditionalInfo.ONE_BYTE) + if ai == int(CBOR_AdditionalInfo.TWO_BYTES): + return int(value) >= 256 + if ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + return int(value) >= 65536 + if ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + return int(value) >= (1 << 32) + return ai == int(CBOR_AdditionalInfo.INDEFINITE) + + def _walk(depth=0): + # type: (int) -> None + if depth > MAX_CBOR_NESTING: + raise CBOR_Codec_Decoding_Error( + "Maximum CBOR nesting depth exceeded", + remaining=s[index[0]:]) + start = index[0] + if start >= len(s): + raise CBOR_Codec_Decoding_Error( + "Empty CBOR data", remaining=s[start:]) + initial = s[start] + if initial == CBOR_BREAK_BYTE: + issues.append(( + base_offset + start, + "Standalone break byte (0xff)", + )) + index[0] = start + 1 + return + major = initial >> 5 + ai = initial & 0x1f + pos = start + 1 + if ai < int(CBOR_AdditionalInfo.ONE_BYTE): + value = ai # type: Union[int, CBOR_INDEFINITE] + elif ai == int(CBOR_AdditionalInfo.ONE_BYTE): + if pos + 1 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 1-byte value", remaining=s[start:]) + value = s[pos] + pos += 1 + elif ai == int(CBOR_AdditionalInfo.TWO_BYTES): + if pos + 2 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 2-byte value", remaining=s[start:]) + value = struct.unpack(">H", s[pos:pos + 2])[0] + pos += 2 + elif ai == int(CBOR_AdditionalInfo.FOUR_BYTES): + if pos + 4 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 4-byte value", remaining=s[start:]) + value = struct.unpack(">I", s[pos:pos + 4])[0] + pos += 4 + elif ai == int(CBOR_AdditionalInfo.EIGHT_BYTES): + if pos + 8 > len(s): + raise CBOR_Codec_Decoding_Error( + "Not enough bytes for 8-byte value", remaining=s[start:]) + value = struct.unpack(">Q", s[pos:pos + 8])[0] + pos += 8 + elif ai == int(CBOR_AdditionalInfo.INDEFINITE): + value = CBOR_INDEFINITE + elif ai in ( + int(CBOR_AdditionalInfo.RESERVED_28), + int(CBOR_AdditionalInfo.RESERVED_29), + int(CBOR_AdditionalInfo.RESERVED_30), + ): + raise CBOR_Codec_Decoding_Error( + "Reserved additional info: %d" % ai, remaining=s[start:]) + else: + raise CBOR_Codec_Decoding_Error( + "Invalid additional info: %d" % ai, remaining=s[start:]) + index[0] = pos + + # Major type 7: simple values and floats. Check float preferred width. + if major == int(CBOR_MajorTypes.SIMPLE_AND_FLOAT): + if ( + ai == int(CBOR_AdditionalInfo.ONE_BYTE) + and isinstance(value, int) + and value < 32 + ): + issues.append(( + base_offset + start, + "Non-shortest CBOR simple value encoding " + "(AI=24, value=%d)" % value, + )) + if ai in ( + int(CBOR_FloatAI.HALF), + int(CBOR_FloatAI.SINGLE), + int(CBOR_FloatAI.DOUBLE), + ) and value is not CBOR_INDEFINITE: + comps = _cbor_nan_components(ai, int(value)) + if comps is not None: + preferred = _cbor_nan_preferred_ai(ai, int(value)) + else: + preferred = _cbor_preferred_float_ai( + _cbor_float_from_bits(ai, int(value)) + ) + if preferred < ai: + issues.append(( + base_offset + start, + "Non-shortest CBOR float encoding (AI=%d, preferred AI=%d)" + % (ai, preferred), + )) + return + + if value is CBOR_INDEFINITE: + if not allow_indefinite: + issues.append(( + base_offset + start, + "Indefinite-length item is not allowed", + )) + if major in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + chunk_start = index[0] + chunk_major, chunk_len, rem = CBOR_decode_head(s[chunk_start:]) + consumed = len(s) - chunk_start - len(rem) + if chunk_major != major: + raise CBOR_Codec_Decoding_Error( + "Indefinite string chunk must be major type %d, " + "got %d" % (major, chunk_major), + remaining=s[chunk_start:]) + if chunk_len is CBOR_INDEFINITE: + raise CBOR_Codec_Decoding_Error( + "Nested indefinite string", + remaining=s[chunk_start:]) + chunk_ai = s[chunk_start] & 0x1f + if not _argument_is_shortest(chunk_ai, chunk_len): + issues.append(( + base_offset + chunk_start, + "Non-shortest CBOR argument encoding " + "(AI=%d, value=%r)" % (chunk_ai, chunk_len), + )) + if len(rem) < int(chunk_len): + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string chunk", + remaining=s[chunk_start:]) + index[0] = chunk_start + consumed + int(chunk_len) + if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=s[index[0]:]) + index[0] += 1 + return + if major == int(CBOR_MajorTypes.ARRAY): + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + _walk(depth + 1) + if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=s[index[0]:]) + index[0] += 1 + return + if major == int(CBOR_MajorTypes.MAP): + key_encodings = [] # type: List[bytes] + while index[0] < len(s) and not cbor_is_break(s[index[0]:]): + key_start = index[0] + _walk(depth + 1) + key_encodings.append(bytes(s[key_start:index[0]])) + _walk(depth + 1) + if index[0] >= len(s) or not cbor_is_break(s[index[0]:]): + raise CBOR_Codec_Decoding_Error( + "Expected break byte (0xff)", remaining=s[index[0]:]) + index[0] += 1 + if key_encodings != sorted(key_encodings): + issues.append(( + base_offset + start, + "CBOR map keys are not in bytewise lexicographic order", + )) + return + raise CBOR_Codec_Decoding_Error( + "Indefinite length not allowed for major type %d" % major, + remaining=s[start:], + ) + + if not _argument_is_shortest(ai, value): + issues.append(( + base_offset + start, + "Non-shortest CBOR argument encoding (AI=%d, value=%r)" + % (ai, value), + )) + + if major in ( + int(CBOR_MajorTypes.BYTE_STRING), + int(CBOR_MajorTypes.TEXT_STRING), + ): + length = int(value) + if index[0] + length > len(s): + raise CBOR_Codec_Decoding_Error( + "Truncated byte/text string", remaining=s[start:]) + index[0] += length + return + if major == int(CBOR_MajorTypes.ARRAY): + for _ in range(int(value)): + _walk(depth + 1) + return + if major == int(CBOR_MajorTypes.MAP): + key_encodings = [] # type: List[bytes] + for _ in range(int(value)): + key_start = index[0] + _walk(depth + 1) + key_encodings.append(bytes(s[key_start:index[0]])) + _walk(depth + 1) + if key_encodings != sorted(key_encodings): + issues.append(( + base_offset + start, + "CBOR map keys are not in bytewise lexicographic order", + )) + return + if major == int(CBOR_MajorTypes.TAG): + _walk(depth + 1) + return + + try: + _walk() + except CBOR_Codec_Decoding_Error: + # Malformed input is reported by normal decoding, not this checker. + pass + return issues + From 6a4345544fe72a1ea3297e19d8b968c0303798e0 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 16:02:43 +0200 Subject: [PATCH 42/48] cbor: reject unknown map keys that collide with schema names Treat all fixed-map member names as reserved on encode, even when optional/conditional fields omit them, and tidy copy/optional helpers. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 34 +++++++++------------- scapy/cborpacket.py | 12 ++------ test/scapy/layers/cbor.uts | 59 +++++++++++++++++++++++++++++++++----- 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 64d4607ab1a..198889dc519 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -1718,7 +1718,7 @@ def build_result(self, pkt): return CBORBuildResult(data, 1) -class CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): +class _CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): """Per-map storage for unknown text-key extension pairs. Not a CBOR wire field by itself: owning :class:`CBORF_MAP` instances read @@ -1744,13 +1744,13 @@ def is_empty(self, pkt): def encode_value(self, x): # type: (Any) -> bytes raise CBOR_Encoding_Error( - "CBORF_MAP_UNKNOWN is not encoded as a standalone CBOR item" + "_CBORF_MAP_UNKNOWN is not encoded as a standalone CBOR item" ) def m2i(self, pkt, s): # type: (CBOR_Packet, bytes) -> Tuple[Any, bytes] raise CBOR_Decoding_Error( - "CBORF_MAP_UNKNOWN is not decoded as a standalone CBOR item" + "_CBORF_MAP_UNKNOWN is not decoded as a standalone CBOR item" ) @@ -1816,7 +1816,7 @@ def __init__(self, *seq, **kwargs): "CBORF_MAP unknown_field %r collides with a known member" % (unknown_field,) ) - self._unknown_field = CBORF_MAP_UNKNOWN(unknown_field, []) + self._unknown_field = _CBORF_MAP_UNKNOWN(unknown_field, []) def __repr__(self): # type: () -> str @@ -1838,7 +1838,6 @@ 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]] - seen = set() # type: set[str] for fld in self.seq: value_result = fld.build_result(pkt) if value_result.items == 0: @@ -1849,7 +1848,8 @@ def build_result(self, pkt): % fld.name ) pairs.append((self._encoded_keys[fld.name], value_result.data)) - seen.add(fld.name) + known_names = set(self._field_by_name) + seen_unknown = set() # type: set[str] unknown = pkt.getfieldval(self._unknown_field.name) or [] for key, value in unknown: if not isinstance(key, str): @@ -1857,11 +1857,11 @@ def build_result(self, pkt): "CBOR map unknown key must be a text string, got %r" % (key,) ) - if key in seen: + if key in known_names or key in seen_unknown: raise CBOR_Encoding_Error( "Duplicate CBOR map key: %r" % (key,) ) - seen.add(key) + seen_unknown.add(key) key_bytes = CBORcodec_TEXT_STRING.enc(key) value_bytes = CBORcodec_Object.encode_cbor_item_deterministic(value) pairs.append((key_bytes, value_bytes)) @@ -2141,17 +2141,11 @@ class CBORF_optional(CBORF_element): def __init__(self, field): # type: (Union[CBORF_field[Any], CBORF_SEMANTIC_TAG]) -> None - for attr in ( - "is_absent", - "is_empty", - "matches_next_item", - "mark_absent", - ): - if not callable(getattr(field, attr, None)): - raise TypeError( - "CBORF_optional requires a field-like element with %s(); " - "got %r" % (attr, type(field).__name__) - ) + if not isinstance(field, (CBORF_field, CBORF_SEMANTIC_TAG)): + raise TypeError( + "CBORF_optional requires CBORF_field or CBORF_SEMANTIC_TAG; " + "got %r" % (type(field).__name__,) + ) self._field = field def __getattr__(self, attr): @@ -2160,8 +2154,6 @@ def __getattr__(self, attr): def build_result(self, pkt): # type: (CBOR_Packet) -> CBORBuildResult - if self._field.is_absent(pkt): - return CBORBuildResult(b"", 0) if self._field.is_empty(pkt): return CBORBuildResult(b"", 0) return self._field.build_result(pkt) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 1ba4b79e296..f8e0543eb68 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -183,15 +183,7 @@ def copy(self): clone._cbor_raw_cache_items = ( # type: ignore[attr-defined] self._cbor_raw_cache_items ) - 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) + if f.holds_packets and f.name in clone.fields: + clone.fields[f.name] = f.any2i(clone, clone.fields[f.name]) return clone diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index cc71cfbdc13..d301e266bdf 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -932,13 +932,11 @@ from scapy.cbor.cborfields import ( try: CBORF_ARRAY( - CBORF_optional( - CBORF_CONDITIONAL( - CBORF_optional( - CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) - ), - lambda pkt: True, - ) + CBORF_CONDITIONAL( + CBORF_optional( + CBORF_SEQUENCE_OF("vals", [], CBORF_UNSIGNED_INTEGER) + ), + lambda pkt: True, ), CBORF_UNSIGNED_INTEGER("tail", 0), ) @@ -2123,6 +2121,53 @@ except CBOR_Encoding_Error: pass += Fixed-schema maps reject unknown keys colliding with absent optional members +from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER, CBORF_optional +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class OptMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("a", 0), + CBORF_optional(CBORF_UNSIGNED_INTEGER("opt", None)), + ) + +pkt = OptMap(a=1) +pkt._cbor_unknown = [("opt", CBOR_UNSIGNED_INTEGER(42))] +try: + bytes(pkt) + assert False, "encode accepted unknown key colliding with absent optional" +except CBOR_Encoding_Error: + pass + + += Fixed-schema maps reject unknown keys colliding with false conditional members +from scapy.cbor.cborfields import ( + CBORF_CONDITIONAL, + CBORF_MAP, + CBORF_UNSIGNED_INTEGER, +) +from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER +from scapy.cborpacket import CBOR_Packet + +class CondMap(CBOR_Packet): + CBOR_root = CBORF_MAP( + CBORF_UNSIGNED_INTEGER("flag", 0), + CBORF_CONDITIONAL( + CBORF_UNSIGNED_INTEGER("cond", 9), + lambda pkt: pkt.getfieldval("flag") == 1, + ), + ) + +pkt = CondMap(flag=0) +pkt._cbor_unknown = [("cond", CBOR_UNSIGNED_INTEGER(42))] +try: + bytes(pkt) + assert False, "encode accepted unknown key colliding with false conditional" +except CBOR_Encoding_Error: + pass + + = Fixed-schema maps reject duplicate unknown text keys on encode from scapy.cbor.cborfields import CBORF_MAP, CBORF_UNSIGNED_INTEGER from scapy.cbor.cbor import CBOR_Encoding_Error, CBOR_UNSIGNED_INTEGER From 75c53e6d684984de496eef5f20c6008f99ed1587 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:41:48 +0200 Subject: [PATCH 43/48] cbor: optional semantic tags treat None defaults as empty Delegate CBORF_SEMANTIC_TAG.is_empty to the inner field so optional tagged scalars with default=None omit the tag on fresh build. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 14 +------------- test/scapy/layers/cbor.uts | 16 ++++++++-------- 2 files changed, 9 insertions(+), 21 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 198889dc519..2c21b839171 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -2100,21 +2100,9 @@ def mark_absent(self, pkt): # type: (CBOR_Packet) -> None self.inner_field.mark_absent(pkt) - def is_absent(self, pkt): - # type: (CBOR_Packet) -> bool - return self.inner_field.is_absent(pkt) - def is_empty(self, pkt): # type: (CBOR_Packet) -> bool - return self.is_absent(pkt) - - def set_val(self, pkt, val): - # type: (CBOR_Packet, Any) -> None - # Presence bookkeeping for optional wrappers targets the inner value. - if val is CBOR_ABSENT: - self.mark_absent(pkt) - return - self.inner_field.set_val(pkt, val) + return self.inner_field.is_empty(pkt) def min_items(self, pkt): # type: (CBOR_Packet) -> int diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index d301e266bdf..67b95a1a67f 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -2061,27 +2061,27 @@ assert bytes(pkt) == b"\x82\x00\x02" + Additional CBOR API blind spots -= Optional semantic tag honors an absent default instead of forcing tag presence += Optional semantic tag with default=None builds an empty array from scapy.cbor.cborfields import ( CBORF_ARRAY, CBORF_SEMANTIC_TAG, CBORF_UNSIGNED_INTEGER, CBORF_optional, - CBOR_ABSENT, ) from scapy.cborpacket import CBOR_Packet -class OptionalSemanticTagDefault(CBOR_Packet): +class OptionalTagged(CBOR_Packet): CBOR_root = CBORF_ARRAY( CBORF_optional( - CBORF_SEMANTIC_TAG(1, CBORF_UNSIGNED_INTEGER("value", 0)) + CBORF_SEMANTIC_TAG( + 1, + CBORF_UNSIGNED_INTEGER("value", None), + ) ) ) -OptionalSemanticTagDefault.CBOR_root.seq[0]._field.inner_field.default = CBOR_ABSENT - -pkt = OptionalSemanticTagDefault() -assert pkt.getfieldval("value") is CBOR_ABSENT +pkt = OptionalTagged() +assert pkt.getfieldval("value") is None assert bytes(pkt) == b"\x80" = Fixed-schema maps reject duplicate known keys instead of silently taking the last value From 2b6e208b14f6972400867b04b4c39c28c6b5e9ca Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:43:19 +0200 Subject: [PATCH 44/48] cbor: copy ismutable defaults in Packet, drop getter materialization Teach Packet.prepare_cached_fields/do_init_cached_fields to honor ismutable via Field.do_copy, and stop mutating CBOR packets from getters. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cborpacket.py | 58 +++++++++++--------------------------- scapy/packet.py | 14 ++++----- test/scapy/layers/cbor.uts | 10 +++++++ 3 files changed, 33 insertions(+), 49 deletions(-) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index f8e0543eb68..8cd87056478 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -42,8 +42,9 @@ class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): """CBOR packet with root-schema build/dissect and cache integration. 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. + Scapy's mutation detection and per-instance default copying. This class + re-parents nested packet defaults and stores parsed root item counts for + exact-wire rebuilds. """ CBOR_root = None # type: Optional[Any] @@ -88,46 +89,21 @@ def do_init_cached_fields(self, for_dissect_only=False): ) if for_dissect_only: return - # Packet only deep-copies list/dict/set defaults; deepen ismutable. + # Packet copies ismutable defaults into fields; promote leftovers and + # re-parent nested packet defaults onto this instance. 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 _materialize_cbor_default(self, attr): - # type: (str) -> Optional[Tuple[Any, Any]] - """Copy mutable/packet defaults into ``fields`` on first access.""" - if attr in self.fields or attr not in self.default_fields: - return None - fld = self.get_field(attr) - if fld is None or not ( - getattr(fld, "ismutable", False) or fld.holds_packets - ): - return None - 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] - - def getfield_and_val(self, attr): - # type: (str) -> Tuple[Any, Any] - materialized = self._materialize_cbor_default(attr) - if materialized is not None: - return materialized - return super(CBOR_Packet, self).getfield_and_val(attr) - - def getfieldval(self, attr): - # type: (str) -> Any - materialized = self._materialize_cbor_default(attr) - if materialized is not None: - return materialized[1] - return super(CBOR_Packet, self).getfieldval(attr) + if f.name in self.fields: + if f.holds_packets: + self.fields[f.name] = f.any2i(self, self.fields[f.name]) + continue + if not ( + getattr(f, "ismutable", False) or f.holds_packets + ) or f.name not in self.default_fields: + continue + val = f.do_copy(self.default_fields[f.name]) + if f.holds_packets: + val = f.any2i(self, val) + self.fields[f.name] = val def _raw_packet_cache_field_value(self, fld, val, copy=False): # type: (Any, Any, bool) -> Optional[Any] diff --git a/scapy/packet.py b/scapy/packet.py index 8afb483c94b..97ccd1b9fb9 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -370,12 +370,8 @@ def do_init_cached_fields(self, for_dissect_only=False): # Deepcopy default references for fname in Packet.class_default_fields_ref[cls_name]: value = self.default_fields[fname] - try: - self.fields[fname] = value.copy() - except AttributeError: - # Python 2.7 - list only - self.fields[fname] = value[:] - + fld = self.fieldtype[fname] + self.fields[fname] = fld.do_copy(value) def prepare_cached_fields(self, flist): # type: (Sequence[AnyField]) -> None """ @@ -406,8 +402,10 @@ def prepare_cached_fields(self, flist): if f.holds_packets: class_packetfields.append(f) - # Remember references - if isinstance(f.default, (list, dict, set, RandField, Packet)): + # Remember references that need a per-instance copy + if getattr(f, "ismutable", False) or isinstance( + f.default, (list, dict, set, RandField, Packet) + ): class_default_fields_ref.append(f.name) # Apply diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 67b95a1a67f..2ad22bed66c 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -1667,6 +1667,16 @@ a.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(1)) assert len(b.value.val[0].val) == 1 assert b.value.val[0].val[0].val == 0 += Mutable defaults are isolated without reading the sibling first +class RRMutableDefaultNoPeerRead(CBOR_Packet): + CBOR_root = CBORF_ANY("value", [[0]]) + +a = RRMutableDefaultNoPeerRead() +a.value.val[0].val.append(CBOR_UNSIGNED_INTEGER(1)) +b = RRMutableDefaultNoPeerRead() +assert len(b.value.val[0].val) == 1 +assert b.value.val[0].val[0].val == 0 + = Mutable semantic-tag defaults are isolated between packet instances class RRMutableTagDefault(CBOR_Packet): CBOR_root = CBORF_ANY("value", CBOR_SEMANTIC_TAG((1, CBOR_ARRAY([])))) From bbc76e4c4dea18d1ba2a5582ea0fe00c286132c7 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:43:53 +0200 Subject: [PATCH 45/48] cbor: share Packet raw-cache validity with CBOR_Packet Extract _raw_packet_cache_is_valid from Packet.self_build so CBOR can reuse the same fingerprinting and only clear its item-count sidecar. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cborpacket.py | 11 ++--------- scapy/packet.py | 30 +++++++++++++++++++----------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index 8cd87056478..b71ad2266e4 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -52,16 +52,9 @@ class CBOR_Packet(Packet, metaclass=CBORPacket_metaclass): def _raw_cache_is_valid(self): # type: () -> bool """Return True if ``raw_packet_cache`` still matches nested field state.""" - if self.raw_packet_cache is None or self.raw_packet_cache_fields is None: + if not super(CBOR_Packet, self)._raw_packet_cache_is_valid(): + self._cbor_raw_cache_items = None # type: ignore[attr-defined] return False - for fname, fval in self.raw_packet_cache_fields.items(): - fld, val = self.getfield_and_val(fname) - if self._raw_packet_cache_field_value(fld, val) != fval: - self.raw_packet_cache = None - self.raw_packet_cache_fields = None - self._cbor_raw_cache_items = None # type: ignore[attr-defined] - self.wirelen = None - return False return True def cbor_build_result(self): diff --git a/scapy/packet.py b/scapy/packet.py index 97ccd1b9fb9..6540de0c223 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -764,22 +764,30 @@ def clear_cache(self): fsubval.clear_cache() self.payload.clear_cache() + def _raw_packet_cache_is_valid(self): + # type: () -> bool + """Return True if ``raw_packet_cache`` still matches nested field state. + + On mismatch, clear the cache fingerprints and ``wirelen``. + """ + if self.raw_packet_cache is None or self.raw_packet_cache_fields is None: + return False + for fname, fval in self.raw_packet_cache_fields.items(): + fld, val = self.getfield_and_val(fname) + if self._raw_packet_cache_field_value(fld, val) != fval: + self.raw_packet_cache = None + self.raw_packet_cache_fields = None + self.wirelen = None + return False + return True + def self_build(self): # type: () -> bytes """ Create the default layer regarding fields_desc dict """ - if self.raw_packet_cache is not None and \ - self.raw_packet_cache_fields is not None: - for fname, fval in self.raw_packet_cache_fields.items(): - fld, val = self.getfield_and_val(fname) - if self._raw_packet_cache_field_value(fld, val) != fval: - self.raw_packet_cache = None - self.raw_packet_cache_fields = None - self.wirelen = None - break - if self.raw_packet_cache is not None: - return self.raw_packet_cache + if self._raw_packet_cache_is_valid(): + return self.raw_packet_cache p = b"" for f in self.fields_desc: val = self.getfieldval(f.name) From 6d3fb8e1069cc25dcb4a1ff8910197df7046efbd Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:46:23 +0200 Subject: [PATCH 46/48] cbor: make counted build/dissect private compound bookkeeping Rename result types and counted hooks to underscore names, keep public build/dissect ASN.1-like on leaves, and reserve item accounting for compounds and CBOR_Packet raw-cache fidelity. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 246 +++++++++++++++++++++---------------- scapy/cborpacket.py | 12 +- test/scapy/layers/cbor.uts | 34 ++--- 3 files changed, 163 insertions(+), 129 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 2c21b839171..77cd7e5e7b8 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -8,7 +8,7 @@ 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 +``_build_counted`` / ``_dissect_counted`` so unframed sequences and array budgeting can return an item count for raw-cache fidelity; callers outside this module should prefer ``build`` / ``dissect``. """ @@ -95,14 +95,14 @@ class CBOR_Type_Mismatch(CBOR_Decoding_Error): @dataclass(frozen=True) -class CBORBuildResult(object): +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): +class _CBORParseResult(object): """Decoded value, unconsumed input, and items consumed.""" value: Any = None remaining: bytes = b"" @@ -138,8 +138,8 @@ def _encode_exactly_one_cbor_item(val, context="value"): 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 hasattr(val, "_cbor_build_counted"): + result = val._cbor_build_counted() if result.items != 1: raise CBOR_Encoding_Error( "%s must encode exactly one top-level CBOR item, " @@ -173,23 +173,28 @@ def _cbor_attach_parent(parent, child): class CBORF_element(object): - """Base class for CBOR packet field elements.""" + """Base class for CBOR packet field elements. - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + Public API is ``build`` / ``dissect`` (bytes in, bytes out). Item + cardinality for compound budgeting lives in ``_build_counted`` / + ``_dissect_counted``. + """ + + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult raise NotImplementedError - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult raise NotImplementedError def build(self, pkt): # type: (CBOR_Packet) -> bytes - return self.build_result(pkt).data + return self._build_counted(pkt).data def dissect(self, pkt, s): # type: (CBOR_Packet, bytes) -> bytes - return self.dissect_result(pkt, s).remaining + return self._dissect_counted(pkt, s).remaining def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -279,7 +284,7 @@ def i2m(self, pkt, x): ) return data # Do not special-case None here: for CBORF_ANY, None is CBOR null. - # Absent/optional skipping is handled in build_result(). + # Absent/optional skipping is handled in _build_counted(). return self.encode_value(x) @staticmethod @@ -318,32 +323,49 @@ def any2i(self, pkt, x): x = self._object_to_python(x) return self.h2i(pkt, x) - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + """Encode this field's value from *pkt* (ASN.1-style leaf build).""" val = pkt.getfieldval(self.name) if val is None: if self.allows_none: - return CBORBuildResult(b"", 0) + return b"" raise CBOR_Encoding_Error( "Required field %r is None" % self.name) - return CBORBuildResult(self.i2m(pkt, val), 1) + return self.i2m(pkt, val) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def dissect(self, pkt, s): + # type: (CBOR_Packet, bytes) -> bytes + """Decode one item from *s* into *pkt* (ASN.1-style leaf dissect).""" val, remain = self.m2i(pkt, s) self.set_val(pkt, val) - return CBORParseResult(remaining=remain, items=1) + return remain + + def _build_counted(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_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult + remain = self.dissect(pkt, s) + return _CBORParseResult(remaining=remain, items=1) - def parse_value(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + 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) + return _CBORParseResult(value=val, remaining=remain, items=1) - def build_value(self, pkt, value): - # type: (CBOR_Packet, Any) -> CBORBuildResult + def _build_value(self, pkt, value): + # type: (CBOR_Packet, Any) -> _CBORBuildResult """Encode *value* without reading it from *pkt* fields.""" - return CBORBuildResult( + return _CBORBuildResult( data=self.i2m(pkt, self.any2i(pkt, value)), items=1, ) @@ -584,12 +606,19 @@ def any2i(self, pkt, x): return x return self.python_to_cbor_object(x) - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def build(self, pkt): + # type: (CBOR_Packet) -> bytes val = pkt.getfieldval(self.name) if val is CBOR_ABSENT: - return CBORBuildResult(b"", 0) - return CBORBuildResult(self.i2m(pkt, val), 1) + return b"" + return self.i2m(pkt, val) + + def _build_counted(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] @@ -967,11 +996,11 @@ def encode_value(self, x): # type: (Any) -> bytes return CBOR_NULL().enc() - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(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) + return _CBORBuildResult(b"", 0) + return _CBORBuildResult(self.encode_value(None), 1) def is_empty(self, pkt): # type: (CBOR_Packet) -> bool @@ -1024,11 +1053,11 @@ def encode_value(self, x): # type: (Any) -> bytes return CBOR_UNDEFINED().enc() - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(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) + return _CBORBuildResult(b"", 0) + return _CBORBuildResult(self.encode_value(None), 1) def is_empty(self, pkt): # type: (CBOR_Packet) -> bool @@ -1153,7 +1182,7 @@ def _build_children(self, pkt): parts = [] # type: List[bytes] total_items = 0 for field in self.seq: - result = field.build_result(pkt) + result = field._build_counted(pkt) parts.append(result.data) total_items += result.items return b"".join(parts), total_items @@ -1220,20 +1249,20 @@ def _dissect_children_budgeted(self, pkt, s, count): and field._field.matches_next_item(pkt, remaining) ): # Validate without constructing a throwaway packet. - field._field.parse_value(pkt, remaining) + field._field._parse_value(pkt, remaining) self._mark_absent(pkt, field) continue if isinstance(field, CBORF_SEQUENCE_OF): - result = field.dissect_result( + result = field._dissect_counted( 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) + result = field._dissect_counted(pkt, remaining) else: - result = field.dissect_result(pkt, remaining) + result = field._dissect_counted(pkt, remaining) if result.items > items_left: raise CBOR_Decoding_Error( "CBOR field consumed more items than remaining" @@ -1269,13 +1298,13 @@ def __init__(self, *seq, **kwargs): super(CBORF_SEQUENCE, self).__init__(*seq, **kwargs) self._reject_ambiguous_unbounded_sequences() - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult data, total_items = self._build_children(pkt) - return CBORBuildResult(data, total_items) + return _CBORBuildResult(data, total_items) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(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. try: @@ -1285,7 +1314,7 @@ def dissect_result(self, pkt, s): except CBOR_Codec_Decoding_Error as e: raise CBOR_Decoding_Error(str(e)) remaining = self._dissect_children_budgeted(pkt, s, item_count) - return CBORParseResult(remaining=remaining, items=item_count) + return _CBORParseResult(remaining=remaining, items=item_count) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1323,8 +1352,8 @@ def __init__(self, *seq, **kwargs): super(CBORF_ARRAY, self).__init__(*seq, **kwargs) self._reject_ambiguous_unbounded_sequences() - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult items_data, total_items = self._build_children(pkt) if self.encode_indefinite: data = ( @@ -1337,10 +1366,10 @@ def build_result(self, pkt): else: data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, total_items) data += items_data - return CBORBuildResult(data, 1) + return _CBORBuildResult(data, 1) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult try: major_type, count, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: @@ -1373,7 +1402,7 @@ def dissect_result(self, pkt, s): remaining = self._dissect_children_budgeted( pkt, remaining, count ) - return CBORParseResult(remaining=remaining, items=1) + return _CBORParseResult(remaining=remaining, items=1) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1402,6 +1431,11 @@ class _CBORF_HOMOGENEOUS(CBORF_field[List[Any]]): """Shared machinery for homogeneous CBOR collections.""" islist = 1 + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + # Collections are not leaf encoders; use counted compound build. + return self._build_counted(pkt).data + def __init__(self, name, # type: str default, # type: Any @@ -1497,7 +1531,7 @@ def _decode_element(self, pkt, s, values=None): except Exception as exc: raise CBOR_Decoding_Error(str(exc)) return child, remaining - result = self.item_field.parse_value(pkt, s) + result = self.item_field._parse_value(pkt, s) if result.items != 1: raise CBOR_Decoding_Error( "%s element must consume exactly one item" @@ -1511,7 +1545,7 @@ def _encode_element(self, pkt, item): return _encode_exactly_one_cbor_item( item, context="%s element" % self.__class__.__name__ ) - result = self.item_field.build_value(pkt, item) + result = self.item_field._build_value(pkt, item) if result.items != 1: raise CBOR_Encoding_Error( "%s element must emit exactly one item" @@ -1615,22 +1649,22 @@ def m2i(self, pkt, s): 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 + def _dissect_counted(self, pkt, s, max_items=None): + # type: (CBOR_Packet, bytes, Optional[int]) -> _CBORParseResult values, remaining, consumed = self._decode_items( pkt, s, max_items=max_items ) self.set_val(pkt, values) - return CBORParseResult(remaining=remaining, items=consumed) + return _CBORParseResult(remaining=remaining, items=consumed) - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult val = pkt.getfieldval(self.name) if val is None: raise CBOR_Encoding_Error( "Required collection field %r is None" % self.name) parts = [self._encode_element(pkt, item) for item in val] - return CBORBuildResult(b"".join(parts), len(val)) + return _CBORBuildResult(b"".join(parts), len(val)) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -1706,8 +1740,8 @@ def m2i(self, pkt, s): lst.append(item) return lst, s - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult val = pkt.getfieldval(self.name) if val is None: raise CBOR_Encoding_Error( @@ -1715,7 +1749,7 @@ def build_result(self, pkt): parts = [self._encode_element(pkt, item) for item in val] data = CBOR_encode_head(CBOR_MajorTypes.ARRAY, len(val)) data += b"".join(parts) - return CBORBuildResult(data, 1) + return _CBORBuildResult(data, 1) class _CBORF_MAP_UNKNOWN(CBORF_field[List[Tuple[str, Any]]]): @@ -1834,12 +1868,12 @@ def get_fields_list(self): for child in field.get_fields_list() ] + [self._unknown_field] - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult # Emit pairs sorted by encoded key bytes (RFC 8949 core deterministic). pairs = [] # type: List[Tuple[bytes, bytes]] for fld in self.seq: - value_result = fld.build_result(pkt) + value_result = fld._build_counted(pkt) if value_result.items == 0: continue if value_result.items != 1: @@ -1871,10 +1905,10 @@ def build_result(self, pkt): parts.append(key_bytes) parts.append(value_bytes) data = CBOR_encode_head(CBOR_MajorTypes.MAP, len(pairs)) + b"".join(parts) - return CBORBuildResult(data, 1) + return _CBORBuildResult(data, 1) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult try: major_type, count, remaining = CBOR_decode_head(s) except CBOR_Codec_Decoding_Error as e: @@ -1940,7 +1974,7 @@ def _dissect_value_bytes(fld, val_bytes): value_fld = fld.fld else: value_fld = fld - result = value_fld.dissect_result(pkt, val_bytes) + result = value_fld._dissect_counted(pkt, val_bytes) if result.items != 1 or result.remaining: raise CBOR_Decoding_Error( "Map value for %r must contain exactly one item" @@ -1978,7 +2012,7 @@ def _dissect_value_bytes(fld, val_bytes): "Required map field %r is missing" % fld.name ) self._unknown_field.set_val(pkt, unknown_pairs) - return CBORParseResult(remaining=remaining, items=1) + return _CBORParseResult(remaining=remaining, items=1) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -2055,41 +2089,41 @@ def matches_next_item(self, pkt, s): and tag_num == self.tag_num ) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult _tag_num, remaining = self._parse_tag_head(s) - inner = self.inner_field.dissect_result(pkt, remaining) + inner = self.inner_field._dissect_counted(pkt, remaining) if inner.items != 1: raise CBOR_Decoding_Error( "Semantic tag content must be exactly one CBOR item") - return CBORParseResult(remaining=inner.remaining, items=1) + return _CBORParseResult(remaining=inner.remaining, items=1) - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult - inner = self.inner_field.build_result(pkt) + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult + inner = self.inner_field._build_counted(pkt) if inner.items != 1: raise CBOR_Encoding_Error( "Semantic tag content must be exactly one CBOR item") - return CBORBuildResult(self._encode_tagged(inner.data), 1) + return _CBORBuildResult(self._encode_tagged(inner.data), 1) - def parse_value(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + 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) + 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( + return _CBORParseResult( value=inner.value, remaining=inner.remaining, items=1 ) - def build_value(self, pkt, value): - # type: (CBOR_Packet, Any) -> CBORBuildResult - inner = self.inner_field.build_value(pkt, value) + 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) + return _CBORBuildResult(data=self._encode_tagged(inner.data), items=1) def get_fields_list(self): # type: () -> List[CBORF_field[Any]] @@ -2140,18 +2174,18 @@ def __getattr__(self, attr): # type: (str) -> Any return getattr(self._field, attr) - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult if self._field.is_empty(pkt): - return CBORBuildResult(b"", 0) - return self._field.build_result(pkt) + return _CBORBuildResult(b"", 0) + return self._field._build_counted(pkt) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(self, pkt, s): + # type: (CBOR_Packet, bytes) -> _CBORParseResult if not self._field.matches_next_item(pkt, s): self._field.mark_absent(pkt) - return CBORParseResult(remaining=s, items=0) - return self._field.dissect_result(pkt, s) + return _CBORParseResult(remaining=s, items=0) + return self._field._dissect_counted(pkt, s) def min_items(self, pkt): # type: (CBOR_Packet) -> int @@ -2183,17 +2217,17 @@ def __repr__(self): def owners(self): return self.fld.owners - def build_result(self, pkt): - # type: (CBOR_Packet) -> CBORBuildResult + def _build_counted(self, pkt): + # type: (CBOR_Packet) -> _CBORBuildResult if self._evalcond(pkt): - return self.fld.build_result(pkt) - return CBORBuildResult(b"", 0) + return self.fld._build_counted(pkt) + return _CBORBuildResult(b"", 0) - def dissect_result(self, pkt, s): - # type: (CBOR_Packet, bytes) -> CBORParseResult + def _dissect_counted(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) + return self.fld._dissect_counted(pkt, s) + return _CBORParseResult(remaining=s, items=0) def min_items(self, pkt): # type: (CBOR_Packet) -> int diff --git a/scapy/cborpacket.py b/scapy/cborpacket.py index b71ad2266e4..3da0fae8320 100644 --- a/scapy/cborpacket.py +++ b/scapy/cborpacket.py @@ -57,21 +57,21 @@ def _raw_cache_is_valid(self): return False return True - def cbor_build_result(self): + def _cbor_build_counted(self): # type: () -> Any - """Return ``CBORBuildResult`` for this packet's root schema. + """Return ``_CBORBuildResult`` for this packet's root schema. 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. """ - from scapy.cbor.cborfields import CBORBuildResult + from scapy.cbor.cborfields import _CBORBuildResult if self._raw_cache_is_valid(): 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) + return _CBORBuildResult(self.raw_packet_cache, items) + result = self.CBOR_root._build_counted(self) self._cbor_raw_cache_items = result.items # type: ignore[attr-defined] return result @@ -118,7 +118,7 @@ def self_build(self): def do_dissect(self, s): # type: (bytes) -> bytes from scapy.cbor.cborfields import CBOR_ABSENT - result = self.CBOR_root.dissect_result(self, s) + result = self.CBOR_root._dissect_counted(self, s) remain = result.remaining self.raw_packet_cache = s[:-len(remain)] if remain else s self._cbor_raw_cache_items = result.items # type: ignore[attr-defined] diff --git a/test/scapy/layers/cbor.uts b/test/scapy/layers/cbor.uts index 2ad22bed66c..347049019b0 100644 --- a/test/scapy/layers/cbor.uts +++ b/test/scapy/layers/cbor.uts @@ -375,7 +375,7 @@ class OptionalTaggedUnsigned(CBOR_Packet): pkt = OptionalTaggedUnsigned() try: - OptionalTaggedUnsigned.CBOR_root.dissect_result( + OptionalTaggedUnsigned.CBOR_root._dissect_counted( pkt, b"\xc1\x61x", ) @@ -402,7 +402,7 @@ class OptionalTruncatedTaggedUnsigned(CBOR_Packet): pkt = OptionalTruncatedTaggedUnsigned() try: - OptionalTruncatedTaggedUnsigned.CBOR_root.dissect_result(pkt, b"\xc1") + OptionalTruncatedTaggedUnsigned.CBOR_root._dissect_counted(pkt, b"\xc1") assert False, "A truncated present tag was treated as an absent field" except CBOR_Decoding_Error: pass @@ -1551,9 +1551,9 @@ assert map_data[CBOR_FLOAT(1.0)].val == "f" = CBORF_PACKET builds a child root exactly once class RRCountingArray(CBORF_ARRAY): calls = 0 - def build_result(self, pkt): + def _build_counted(self, pkt): type(self).calls += 1 - return super().build_result(pkt) + return super()._build_counted(pkt) class RRCountedChild(CBOR_Packet): CBOR_root = RRCountingArray(CBORF_UNSIGNED_INTEGER("value", 1)) @@ -1979,7 +1979,7 @@ except ValueError: else: raise AssertionError("ambiguous separated unbounded sequences were accepted") -+ Finding 8 - Nested cbor_build_result must preserve a valid child raw cache ++ Finding 8 - Nested _cbor_build_counted must preserve a valid child raw cache = Parent rebuild preserves untouched child wire representation from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_PACKET, CBORF_UNSIGNED_INTEGER @@ -2003,7 +2003,7 @@ assert bytes(pkt.child) == b"\x18\x01" # 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) +assert pkt.child._cbor_build_counted().data == bytes(pkt.child) = Parent rebuild preserves an untouched child encoded as an indefinite array from scapy.cbor.cborfields import CBORF_ARRAY, CBORF_PACKET, CBORF_UNSIGNED_INTEGER @@ -2464,7 +2464,7 @@ 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() +result = child._cbor_build_counted() assert result.data == overlong assert result.items == 2 assert result.data == bytes(child) @@ -2474,12 +2474,12 @@ assert result.data == bytes(child) # and expect serialization to succeed). fld = CBORF_PACKET("x", None, pkt_cls=SeqChild) try: - fld.build_value(None, child) + fld._build_value(None, child) assert False, "multi-item child must be rejected by CBORF_PACKET" except CBOR_Encoding_Error: pass -= CBORF_PACKET build_value enforces one-item cardinality like build_result += CBORF_PACKET _build_value enforces one-item cardinality like _build_counted from scapy.cbor.cborfields import ( CBORF_PACKET, CBORF_SEQUENCE, @@ -2498,13 +2498,13 @@ class OneItemChild(CBOR_Packet): CBOR_root = CBORF_UNSIGNED_INTEGER("a", 1) fld = CBORF_PACKET("x", None, pkt_cls=OneItemChild) -ok = fld.build_value(None, OneItemChild(a=7)) +ok = fld._build_value(None, OneItemChild(a=7)) assert ok.items == 1 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" + fld2._build_value(None, TwoItemChild()) + assert False, "multi-item child must be rejected by _build_value" except CBOR_Encoding_Error: pass @@ -2518,27 +2518,27 @@ fld = CBORF_PACKET("x", None, pkt_cls=CBOR_Packet) # Two valid CBOR integers must not be reported as one item try: - fld.build_value(None, Raw(b"\x01\x02")) + fld._build_value(None, Raw(b"\x01\x02")) assert False, "two CBOR items must be rejected" except CBOR_Encoding_Error: pass # Illegal standalone break try: - fld.build_value(None, Raw(b"\xff")) + fld._build_value(None, Raw(b"\xff")) assert False, "bare break must be rejected" except CBOR_Encoding_Error: pass # Truncated CBOR try: - fld.build_value(None, Raw(b"\x18")) + fld._build_value(None, Raw(b"\x18")) assert False, "truncated CBOR must be rejected" except CBOR_Encoding_Error: pass # Exactly one valid item is accepted via the Raw fallback -ok = fld.build_value(None, Raw(b"\x01")) +ok = fld._build_value(None, Raw(b"\x01")) assert ok.items == 1 assert ok.data == b"\x01" @@ -3259,7 +3259,7 @@ 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 +remain = TwoInts.CBOR_root._dissect_counted(TwoInts(), b"\x01\x02\x03").remaining assert remain == b"\x03" = CBORF_SEMANTIC_TAG rejects the wrong tag number From ee01e7185e893d1f8bd7c1ef8729ddeaafbee068 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:48:39 +0200 Subject: [PATCH 47/48] cbor: drop dead field scaffolding and tighten nested encode/decode Remove allows_none and redundant min/max_items, give NULL/UNDEFINED real leaf builds, skip re-decode for trusted CBOR_Packet encodes, and honor conf.debug_dissector for nested packet construction errors. AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/cbor/cborfields.py | 110 +++++++++++++-------------------------- 1 file changed, 37 insertions(+), 73 deletions(-) diff --git a/scapy/cbor/cborfields.py b/scapy/cbor/cborfields.py index 77cd7e5e7b8..0fadee454a8 100644 --- a/scapy/cbor/cborfields.py +++ b/scapy/cbor/cborfields.py @@ -135,10 +135,12 @@ 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. + Trusted :class:`CBOR_Packet` values use their counted build contract. + Raw/bytes/generic Packet fallbacks are fully decoded to prove well-formed + single-item cardinality. """ - if hasattr(val, "_cbor_build_counted"): + from scapy.cborpacket import CBOR_Packet + if isinstance(val, CBOR_Packet): result = val._cbor_build_counted() if result.items != 1: raise CBOR_Encoding_Error( @@ -146,9 +148,8 @@ def _encode_exactly_one_cbor_item(val, context="value"): "but encoded %d" % (getattr(type(val), "__name__", context), result.items) ) - data = result.data - else: - data = bytes(val) + return result.data + data = bytes(val) try: _obj, remaining = CBORcodec_Object.decode_cbor_item(data) except Exception as exc: @@ -221,7 +222,6 @@ class CBORF_field(CBORF_element, Generic[_I]): holds_packets = 0 islist = 0 ismutable = False - allows_none = False CBOR_tag = None # type: Optional[Any] def __init__(self, @@ -328,8 +328,6 @@ def build(self, pkt): """Encode this field's value from *pkt* (ASN.1-style leaf build).""" val = pkt.getfieldval(self.name) if val is None: - if self.allows_none: - return b"" raise CBOR_Encoding_Error( "Required field %r is None" % self.name) return self.i2m(pkt, val) @@ -343,13 +341,7 @@ def dissect(self, pkt, s): def _build_counted(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) + return _CBORBuildResult(self.build(pkt), 1) def _dissect_counted(self, pkt, s): # type: (CBOR_Packet, bytes) -> _CBORParseResult @@ -370,14 +362,6 @@ def _build_value(self, pkt, value): items=1, ) - 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 x is CBOR_ABSENT or x is CBOR_NO_ITEM: @@ -961,7 +945,6 @@ def randval(self): 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 @@ -996,29 +979,27 @@ def encode_value(self, x): # type: (Any) -> bytes return CBOR_NULL().enc() + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + if pkt.getfieldval(self.name) is CBOR_ABSENT: + return b"" + return self.encode_value(None) + def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult - if pkt.getfieldval(self.name) is CBOR_ABSENT: + data = self.build(pkt) + if not data: return _CBORBuildResult(b"", 0) - return _CBORBuildResult(self.encode_value(None), 1) + return _CBORBuildResult(data, 1) def is_empty(self, pkt): # type: (CBOR_Packet) -> bool 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 field (major type 7, simple value 23).""" CBOR_tag = CBOR_MajorTypes.SIMPLE_AND_FLOAT - allows_none = True def __init__(self, name, # type: str @@ -1053,24 +1034,23 @@ def encode_value(self, x): # type: (Any) -> bytes return CBOR_UNDEFINED().enc() + def build(self, pkt): + # type: (CBOR_Packet) -> bytes + if pkt.getfieldval(self.name) is CBOR_ABSENT: + return b"" + return self.encode_value(None) + def _build_counted(self, pkt): # type: (CBOR_Packet) -> _CBORBuildResult - if pkt.getfieldval(self.name) is CBOR_ABSENT: + data = self.build(pkt) + if not data: return _CBORBuildResult(b"", 0) - return _CBORBuildResult(self.encode_value(None), 1) + return _CBORBuildResult(data, 1) def is_empty(self, pkt): # type: (CBOR_Packet) -> bool 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). @@ -1404,14 +1384,6 @@ def _dissect_counted(self, pkt, s): ) return _CBORParseResult(remaining=remaining, items=1) - 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.""" @@ -1458,8 +1430,10 @@ def __init__(self, self.holds_packets = 1 elif pkt_cls is None: raise ValueError("Provide pkt_cls or next_cls_cb") - elif isinstance(pkt_cls, type) and issubclass(pkt_cls, CBORF_field) or \ - isinstance(pkt_cls, CBORF_field): + elif ( + (isinstance(pkt_cls, type) and issubclass(pkt_cls, CBORF_field)) + or isinstance(pkt_cls, CBORF_field) + ): if isinstance(pkt_cls, type): self.item_field = pkt_cls("_item", None) # type: ignore else: @@ -1480,7 +1454,9 @@ def _require_packet_cls(pkt_cls): and hasattr(pkt_cls, "CBOR_root") ): return cast("Type[CBOR_Packet]", pkt_cls) - raise ValueError("pkt_cls must be a CBORF_field or CBOR_Packet") + raise ValueError( + "pkt_cls must be a CBOR_Packet subclass with CBOR_root" + ) def _list_limit(self): # type: () -> int @@ -1529,6 +1505,8 @@ def _decode_element(self, pkt, s, values=None): except CBOR_Decoding_Error: raise except Exception as exc: + if config.conf.debug_dissector: + raise raise CBOR_Decoding_Error(str(exc)) return child, remaining result = self.item_field._parse_value(pkt, s) @@ -2014,14 +1992,6 @@ def _dissect_value_bytes(fld, val_bytes): self._unknown_field.set_val(pkt, unknown_pairs) return _CBORParseResult(remaining=remaining, items=1) - 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_element): """ @@ -2138,14 +2108,6 @@ def is_empty(self, pkt): # type: (CBOR_Packet) -> bool return self.inner_field.is_empty(pkt) - def min_items(self, pkt): - # type: (CBOR_Packet) -> int - return 1 - - def max_items(self, pkt): - # type: (CBOR_Packet) -> int - return 1 - ############################## # Complex CBOR Fields # @@ -2271,6 +2233,8 @@ def m2i(self, pkt, s): except CBOR_Decoding_Error: raise except Exception as exc: + if config.conf.debug_dissector: + raise raise CBOR_Decoding_Error(str(exc)) return child, remain From 3536a59e42cff40fff89fd32f984f3349f29f169 Mon Sep 17 00:00:00 2001 From: Nils Weiss Date: Wed, 9 Sep 2026 20:49:57 +0200 Subject: [PATCH 48/48] cbor: fix Packet self_build typing after cache helper extract AI-Assisted: yes (Composer) Co-authored-by: Cursor --- scapy/packet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scapy/packet.py b/scapy/packet.py index 6540de0c223..0c63b811dbb 100644 --- a/scapy/packet.py +++ b/scapy/packet.py @@ -372,6 +372,7 @@ def do_init_cached_fields(self, for_dissect_only=False): value = self.default_fields[fname] fld = self.fieldtype[fname] self.fields[fname] = fld.do_copy(value) + def prepare_cached_fields(self, flist): # type: (Sequence[AnyField]) -> None """ @@ -787,7 +788,7 @@ def self_build(self): Create the default layer regarding fields_desc dict """ if self._raw_packet_cache_is_valid(): - return self.raw_packet_cache + return cast(bytes, self.raw_packet_cache) p = b"" for f in self.fields_desc: val = self.getfieldval(f.name)